Рабочий пример инструкций CLI и STI в 16-битной сборке x86

Я хотел бы получить практический пример инструкций cli и sti в 16-битной ассемблере x86, то есть пример кода, написанного на этом языке, который позволит мне на практике узнать, для чего эти инструкции, и пойти дальше теории.

Я знаю, что в документации сказано, что cli отключает флаг прерывания, а sti включает его, и флаг прерывания не влияет не обрабатывает немаскируемые прерывания (NMI) или программные прерывания, сгенерированные инструкцией int.

В учебнике я следую, у меня есть этот код:

    mov ax, 0x8000
    cli
    mov ss, ax
    mov sp, 0xF000
    sti
  • Мои тесты заставляют меня сказать, что cli и sti бесполезны в примере, приведенном в курсе, проведя несколько тестов, я смог убедиться, что результаты всегда будут одинаковыми, ставлю ли я cli и sti или удаляю эти инструкции.

  • Объяснение полезности cli и sti докладчиками на разные темы для примера, приведенного в курсе, носит чисто теоретический характер. То есть вы должны поставить cli и sti для безопасности, чтобы избежать ошибок / сбоев. Спикер на дискорде говорит, что есть один шанс на миллион, что что-то пойдет не так, когда я инициализирую сегменты и смещение стека. Это означает, что он никогда не сможет проверить свое теоретическое объяснение самостоятельно, он просто принимает теорию, ему неинтересно идти дальше и экспериментировать само по себе, невозможно проверить на практике, поскольку вероятность возникновения проблемы составляет один из миллионов.

  • На различных документах/сайтах строго нет других практических примеров, которые действительно демонстрируют, что cli и sti делают и как это полезно, просто скопировали и вставили документацию без примера кода, т.е. cli устанавливает флаг прерывания в 0, а sti устанавливает его в 1 , Когда он отключен, аппаратные прерывания игнорируются. Нулевой пример использования, только теоретическое предложение и на практике ничто не позволяет протестировать такие вещи, действительно есть пример во французской документации, но этот пример также бесполезен, чем пример учебника, которому я следую, чтобы по-настоящему понять. То есть пример, который инициализирует сегмент и ставит cli и sti до и после строки кода, и если мы удалим cli и sti, результат будет таким же, несмотря ни на что (может быть, у нас есть шанс один на миллион что проблема возникнет, если мы удалим cli и sti, это хорошо, это позволяет мне никогда не проверять теорию на практике).

  • Другой спикер на дискорде говорит мне, что он немного экспериментировал со всем этим, что он некоторое время кодировал на ассемблере и что по своему опыту он понимает, почему нужно ставить cli и sti, потому что иначе могут возникнуть проблемы, поэтому приходится ставить это и все. Когда я прошу его привести мне практический пример (который должен быть как раз в его переулке, так как он практиковался), он не делает этого, потому что его нет дома, но, с другой стороны, снова дает мне теоретический блокнот, чтобы объяснить мне насколько это полезно, так очевидно, мы можем очень подробно объяснить, насколько это полезно, но никогда не продемонстрируем полезность на практическом примере в 16-битной сборке x86.

Уточняю, что не знаком с аппаратными прерываниями. Я тестировал только программные прерывания, которые можно вызывать с помощью int.

Я в режиме ядра, мне нужен практический пример, который вызывает проблемы с аппаратными прерываниями кода, а затем еще один пример с cli и sti, который может решить проблему.

Программные прерывания являются синхронными, и на них даже не влияет флаг. Кроме того, выполнение mov ss автоматически отключает прерывания для следующей инструкции, чтобы избежать проблемы, поэтому вам не нужно использовать пару cli/sti.

Jester 18.05.2023 15:51

Некоторые ранние сборки 8088/8086 не блокировали прерывание после записи в регистр ss. Вот почему люди ставят cli и sti вокруг изменения стека с двумя инструкциями. Если вы не использовали cli на этих ранних моделях, аппаратное прерывание могло произойти после того, как вы изменили ss, но до того, как вы изменили sp, что может легко повредить данные где-нибудь в вашем новом сегменте стека. Более поздние процессоры не нуждаются в этом при изменении стека из-за блокировки прерывания из ss инструкций записи, упомянутых @Jester. А на машине 386+ вы можете использовать lss sp для переключения стека в одной инструкции.

ecm 18.05.2023 16:08

Для лучшего примера того, для чего нужен cli, рассмотрите возможность изменения вектора int 1Ch в таблице векторов прерываний. Если вы используете программу, совместимую с 8086, для этого потребуются две инструкции, поскольку нет инструкции для одновременной записи 32 битов в произвольную ячейку памяти. (Предположим, что вы не вызываете DOS для этого, или DOS делает это с эквивалентом cli.) Int 1Ch вызывается по умолчанию с частотой около 18,2 Гц обработчиком ROM-BIOS int 08h (IRQ #0). . Если вы сначала обновите сегмент вашего вектора int 1Ch без cli и возникнет int 08h, поток управления рухнет.

ecm 18.05.2023 16:15

Обратите внимание, что, поскольку современный компьютер выполняет многие миллиарды инструкций в секунду, шанс один на миллион означает, что это может произойти до 1000 раз в секунду. Вы уверены, что хотите рискнуть?

fuz 18.05.2023 16:15
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
4
89
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Конечно, весь смысл cli/sti заключается в управлении обработкой аппаратных прерываний, поэтому вам необходимо некоторое понимание того, как вообще работают аппаратные прерывания.

Вот краткий обзор: аппаратное устройство, подключенное к ЦП, может инициировать аппаратное прерывание (в случае 8086, подключив высокое напряжение к выводу INTR на микросхеме ЦП и используя другие выводы, чтобы сигнализировать, какой вектор прерывания должен быть называется). Когда это происходит, если установлен флаг прерывания, то ЦП завершает выполнение инструкции, которая в данный момент выполняется, затем помещает в стек CS, IP и FLAGS и переходит к адресу, указанному в соответствующей записи таблицы векторов прерываний, что это младшие 1024 байта памяти (0000:0000-0000:0400). Программист должен был предварительно установить эту запись так, чтобы она указывала на блок кода (обработчик прерываний), который должен выполняться в ответ. Обработчик прерывания сделает все необходимое для обработки аппаратного прерывания, а затем выполнит IRET, чтобы вернуться к тому коду, который был прерван. Примерами устройств, вызывающих аппаратные прерывания, могут быть: нажатие клавиши на клавиатуре, поступление байта на последовательный порт, прерывание по таймеру (MS-DOS настраивает внешний таймер на генерацию прерывания с частотой 18,2 Гц, т. е. каждые 55 мс).

Если флаг прерывания не установлен, ничего не происходит, но обработчик прерывания будет вызван, когда флаг снова будет установлен.


Таким образом, вы должны сбрасывать флаг прерывания всякий раз, когда вы не хотите, чтобы прерывание происходило. Обычно это происходит из-за того, что вы работаете с некоторым ресурсом, который используется совместно текущим кодом и обработчиком прерывания, так что возникнет конфликт, если обработчик прерывания запустится в этот момент.

Например, рассмотрим прерывание по таймеру. Простой обработчик может ничего не делать, кроме как увеличивать счетчик, чтобы основной поток выполнения мог сказать, сколько времени прошло. (В 8086 не было другого встроенного оборудования часов.) Если 16-битного счетчика достаточно, вы могли бы просто иметь:

ticks DW 0
handler:
    inc word ptr [ticks]
    iret

main_code:
    mov ax, [ticks] ; now ax contains the number of ticks

Но при 18,2 Гц мы получаем очень близкое к 65536 тикам в час (думаю, поэтому было выбрано число 18,2), так что счетчик будет переполняться примерно каждый час. Это нехорошо, если вам нужно отслеживать более длинные временные интервалы, поэтому вместо этого мы должны использовать 32-битный счетчик. Поскольку x86-16 не имеет 32-битных арифметических инструкций, мы должны использовать пару ADD/ADC. Наш код может выглядеть так:

ticks DD 0
handler:
    add word ptr [ticks], 1
    adc word ptr [ticks+2], 0
    iret

main_code:
    mov ax, [ticks]
    ;;; BUG what if interrupt occurs here ???
    mov dx, [ticks+2]
    ; now dx:ax contains the 32-bit number of ticks

Но в этом коде есть ошибка. Если случайно произойдет прерывание таймера между инструкциями, помеченными BUG, основной код получит несинхронизированные младшее и старшее слова ticks. Предположим, например, что значение ticks равно 0x1234ffff. Основной код загружает младшее слово 0xffff в ax. Затем происходит прерывание таймера и увеличивается ticks, так что теперь это 0x12350000. Обработчик прерывания возвращается, и основной код выполняет mov dx, [ticks+2], получая значение 0x1235. Итак, теперь основной код загрузил значение 0x1235ffff, что очень неправильно: оно на целый час позже фактического времени.

Мы могли бы исправить это, используя cli/sti для отключения прерываний, чтобы прерывание не могло произойти на сайте, помеченном BUG. Исправленный код будет выглядеть так:

main_code:
    cli
    mov ax, [ticks]
    mov dx, [ticks+2]
    sti

В конкретном случае 32-битного счетчика есть другие способы решить эту проблему без отключения прерываний, но вы поняли идею. Вы можете представить себе более сложную структуру данных, которая может потребоваться как обработчику, так и основному коду: буфер ввода-вывода, некоторая более крупная структура с информацией о только что произошедшем событии ввода-вывода, связанный список и т. д.


Регистры процессора также являются общим ресурсом, как в примере с SS:SP, который вы заметили. Предположим, что стек в настоящее время находится на 1234:5678, и основной код хочет переключить его на 2222:4444. Вы бы подумали сделать:

switch_stack:
    mov ax, 0x2222
    mov ss, ax
    ;;; BUG: what if interrupt occurs here?
    mov sp, 0x4444

Если бы в строке BUG произошло прерывание, значение SS:SP было бы 2222:5678, и именно здесь ЦП передаст значения CS/IP/FLAGS перед переходом к обработчику. Это было бы очень плохо, так как это не правильное расположение ни старого, ни нового стека. По этому адресу могут быть важные данные, которые ЦП сейчас перезаписывает, и поэтому теперь у нас есть трудно воспроизводимая ошибка повреждения памяти.

Таким образом, мы также подумали бы, чтобы исправить это с помощью

switch_stack:
    mov ax, 0x2222
    cli
    mov ss, ax
    ;;; interrupt can't occur here!
    mov sp, 0x4444
    sti

Сейчас так получилось, что это на самом деле частный случай. Поскольку в такой ситуации было бы особенно неприятно забыть отключить прерывания, разработчики 8086 решили оказать небольшую услугу программистам. Инструкция mov ss, reg имеет особую функцию, которая автоматически отключает прерывания для одной инструкции. Так что на самом деле, если вы кодируете mov ss, ax, за которым сразу следует mov sp, 0x2222, прерывание не может произойти между ними, и код на самом деле безопасен без cli/sti.

Но еще раз подчеркну, что это уникальный частный случай. Я считаю, что только mov ss, reg и pop ss обладают такой функциональностью, поэтому такие примеры, как 32-битный счетчик тиков, действительно нуждаются в cli/sti. И на самом деле, если бы вы поменяли местами две инструкции и закодировали mov sp, 0x2222, а затем mov ss, ax (что на первый взгляд выглядело бы так же хорошо), у вас снова была бы ошибка, и обработчик прерывания мог бы быть вызван со стеком, указывающим на 1234:2222. Также, как отметил @ecm в комментарии, некоторые ранние чипы 8086/8088 имели аппаратную ошибку (?), из-за которой функция «отключить прерывания для одной инструкции» не работала, поэтому на таких чипах вам также пришлось бы используйте cli/sti. (Или, может быть, эта функция не была частью спецификации до более позднего времени?)

В 386 добавлена ​​инструкция lss для загрузки как сегмента стека, так и указателя стека в одной инструкции, что было более надежным способом решения этой проблемы. Это также было более важно в этом случае, потому что в виртуальном режиме 8086 cli/sti не выполнялся бы напрямую, а вместо этого перехватывал бы операционную систему, которая была очень медленной, и ее лучше избегать, если это возможно.


Вы предполагаете, что, возможно, это настолько маловероятно, что нам не стоит об этом беспокоиться. Давайте посмотрим на наш пример с 32-битным таймером и представим приложение с функцией «будильник». Выполняя другую работу, он периодически проверяет счетчик тиков, скажем, около 100 раз в секунду, чтобы узнать, прошло ли заданное время, и если да, то делает что-то, чтобы предупредить пользователя. Если вы опустите cli/sti, то, если там произойдет прерывание с младшим словом, равным 0xffff (что происходит раз в час), он будет думать, что время на один час позже, чем оно есть, и поэтому может выдать предупреждение до одного. час слишком рано. (Если вы хотите быть более драматичным, замените «подать сигнал тревоги» на «активировать опасную технику», «запустить ракеты» и т. д.)

Инструкция mov ax, mem на процессоре 8086 заняла 10 тактов, то есть 1000 тактов в секунду, когда мы уязвимы. Первоначальный IBM PC работал на частоте 4,7 МГц, поэтому у нас есть вероятность 1/4700 в начале каждого часа срабатывания ошибки. Если вы отправляете свое приложение 50 000 пользователей, и каждый из них использует его по 8 часов в день, то, немного подсчитав, вы можете получить 425 жалоб на эту ошибку в течение первой недели после выпуска. Ваш босс будет очень зол.

И помните, мы вернулись в середину 1980-х, и Интернета нет, поэтому вам придется отправить каждому из ваших 50 000 клиентов по почте дискету с патчем. Эта ошибка обошлась компании в пару долларов плюс почтовые расходы примерно в 100 000 долларов. Напротив, ваша зарплата программиста начального уровня в 1984 году составляла около 20 000 долларов в год. Как вы относитесь к своим шансам сохранить работу?

На самом деле тики составляют почти ровно 64 кило бинарных (65_536) тиков в час, не совсем так. В день бывает 18_00B0h или (в некоторых ROM-BIOS) 18_00B1h тиков, так что каждый час на несколько тиков больше, чем вы утверждаете. Это ни в коем случае не плохое приближение, но да. И комментарий о ранней ошибке процессора был написан мной, а не Питером Кордесом. И lds, и les существовали вплоть до 8086, только lss не было. И на самом деле, используя les, вы можете выполнять 32-битную загрузку произвольных данных из произвольного места в памяти; как вы заявили, «есть и другие способы решить эту проблему» в отношении загрузки тика.

ecm 18.05.2023 23:27

@ecm: частота тиков точно равна 18,2 Гц, а не 65536/3600 Гц = ~ 18,20444... Гц? Если это так, то 65520 тиков в час, цикличность каждые 1 час и 0,879 секунды. Интересный.

Peter Cordes 18.05.2023 23:59

@PeterCordes Я полагаю, что точная частота была выбрана для управления теми же часами IBM PC, что и какая-то видеосистема NTSC. PIT по умолчанию имеет 16-битный делитель 0 (действующий как 64-килобайтный двоичный делитель), поэтому почти, но не точно, 4 гигабитных двоичных такта PIT в час. «Intel 8253 PIT был оригинальным устройством синхронизации, используемым на совместимых с IBM PC. тактовой частоты процессора 4,77 МГц)" en.m.wikipedia.org/wiki/Programmable_interval_timer

ecm 19.05.2023 00:21

Ошибка в моем предыдущем комментарии: «18,206 в час» следует читать как «в секунду».

ecm 19.05.2023 00:37

Более подробная информация о деталях PIT на wiki.osdev.org/Programmable_Interval_Timer

ecm 19.05.2023 00:40

@ecm: Извините за неправильную атрибуцию - я думаю, у вас двоих похожие стили письма :)

Nate Eldredge 19.05.2023 07:17

Другие вопросы по теме