Программирование процессоров Intel x86 в защищённом режиме
Исключения

Автор: sergh
The RSDN Group

Версия текста: 1.0

Что, зачем и почему
Ошибки
Случилось страшное. Ну, и?..
Смотрим шире
Основы обработки исключений
Терминология Intel
Регистрация
Обработчики
Классификация исключений
Fault
Trap
Abort
Код ошибки
Табличка
Всё вместе
Fault
Abort
Случилось страшное. Ну, и?.. - II
Программные прерывания vs. Исключения
Пример
Обработчик #GP
Задания

Мне и больно, мне и сладко, что он не такой как все

Сергей Чиграков

Приложение выполнило недопустимую операцию и будет закрыто

Microsoft Windows

Человеку свойственно ошибаться

Истина

Очередная особенность защищённого режима – важность исключений. В отличие от реального режима, в котором исключения используются довольно слабо, для ОС защищённого режима они важны так же, как и остальные виды прерываний, то есть исключительно важны. Уважающая себя ОС защищённого режима просто вынуждена корректно обрабатывать все возможные исключения. Причины три:

Глава начинается с абстрактных (не относящихся к конкретному процессору) размышлений на тему «Исключения: что, зачем и почему?», продолжается описанием реализации обработки исключений на базе Intel x86 и завершается примером программы, обрабатывающей исключение #GP.

Что, зачем и почему

Знаете, почему стиральная машина лучше персонального компьютера? Вот почему:

И, если первая проблема решается разработкой соответствующих периферийных устройств, не представляет интереса и не рассматривается в курсе, то вторая напрямую связана с темой главы, зато, видимо, практически не решаема.

Завидная устойчивость стиральных машин к программным сбоям, связана не с тем, что они «проще» (это может быть не так, особенно если сравнивать с PC XT), а с тем, что они всегда работают по одному из нескольких предопределённых сценариев, и никогда не исполняют «пользовательское программное обеспечение». Примерно то же можно сказать и про холодильники, пылесосы, телефоны, автомобили и т.п.

В отличие от всей перечисленной техники, компьютеры задуманы относительно универсальными.

ПРИМЕЧАНИЕ

Можно попытаться сравнить возможности настройки. Средняя стиральная машина имеет три ручки с десятком возможных состояний каждая и три кнопки с двумя состояниями. Итого 10^3 * 2^3 – примерно 8000 состояний. Это, конечно, немало, но сравните с компьютером. «Настраиваемыми элементами» являются регистры, память и порты ввода-вывода (а ещё можно вспомнить про жесткий диск... но мы не будем). Каждый бит каждого регистра, байта ОЗУ, порта ввода-вывода, может быть 0 или 1. Так как регистров не больше сотни, портов 64 Кб, а памяти процессор умеет адресовать до 4 Гб, получим примерно 2^(количество битов ОЗУ), то есть что-то около 2^(32 миллиарда) состояний. Это число вам не удастся даже записать в десятичной системе счисления (оно содержит примерно 10 миллиардов символов), перебирать варианты можно и не начинать. Очевидно, что подходы к тестированию стиральной машины и компьютера должны отличаться.

На компьютере исполняются самые разные программы, написанные ещё более разными людьми, имевшими совершенно различные цели (иногда – деструктивные) и квалификацию (часто – недостаточную). И ни кому из них процессор ни в коем случае не должен доверять слишком сильно. Что характерно, защищённый режим всё только усложняет: в случае работы под управлением многозадачной ОС, процессор должен доверять пользовательским программам ещё меньше, так как некорректное поведение одной программы ставит под угрозу не только её, но и все остальные запущенные программы.

ПРИМЕЧАНИЕ

Вообще-то, это скорее относится к ОС, а не к процессору. Но поскольку ОС опирается на возможности, предоставляемые процессором (напоминаю, в первой главе мы договорились не использовать виртуальные машины из соображений производительности), если он не имеет механизма обработки ошибок, она будет бессильна.

Ошибки

Поскольку объекты «пользователь» и «процессор» обладают очень разными характеристиками, понятие «ошибка» для пользователя и для процессора – очень разные вещи. Поясняю на примере:

Отсюда можно сделать два вывода:

ПРИМЕЧАНИЕ

Если всё-таки хочется переложить поиск ошибок на процессор, нужно чтобы при несоблюдении некоторых условий, текущая команда рассматривалась процессором как ошибочная. Этого можно добиться либо меняя по условию поток выполнения (имеется ввиду что-то типа if (условие) {ошибочная команда}), либо добавив к процессору специальные команды, самостоятельно проверяющие условия. Поскольку рядовому программисту довольно сложно добавить к процессору команду, проверяющую нужное ему условие, на практике чаще применяется первый подход (примерно так реализованы assert-ы), но специальные команды тоже существуют.

Случилось страшное. Ну, и?..

Мама, мама, что мы будем делать,
Когда настанут зимние холода.

Из фильма «Кин-дза-дза»

Поскольку ошибки неизбежны, на них надо как-то разумно реагировать. Рассмотрим варианты (для определённости, предположим, что команда mov пытается писать в read-only сегмент).

[Лирическое отступление] Плохие варианты

а) Физическое самоуничтожение. В соответствии с кодексом чести самурая, процессор, не способный выполнить поставленную перед ним задачу, делает себе харакири (сеппуку, если получится). Без комментариев.

б) Автоматически перезагрузиться или, наоборот, зависнуть до аппаратного сброса. В некоторых ситуациях, такая реакция допустима для (почти?) любых процессоров. А какой-нибудь микроконтроллер вполне может «обрабатывать» так вообще все ошибки. Процессор компьютера общего назначения не должен использовать подобный подход слишком часто, так как пользователи хотят работать/играть/…, но уж точно не любоваться каждые пять минут на загрузку любимой ОС.

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

г) Проигнорировать команду. Представьте себе, что в вашей программе некоторые ассемблерные команды иногда (не стабильно) просто не исполняются. И никому об этом не сообщают… И все последующие команды полагают, что всё в порядке… Сомневаюсь, что такой процессор завоюет популярность. В некоторых ситуациях такая стратегия обработки ошибок работает, но, видимо, процессоры – не тот случай.

д) Проигнорировать команду и выставить флаг ошибки. Несколько лучше… Но теперь после каждого mov придётся проверять, успешно ли он выполнился. Печально? Это ещё цветочки, ведь ошибка может быть не только в mov. Например, push и pop могут выйти за границу сегмента, call и jmp могут попытаться передать управление по «неправильному» адресу, и т.п. Фактически, придётся проверять все команды, кроме тех, которые имеют дело только с регистрами общего назначения (с прочими регистрами тоже не всё просто). В принципе, с учётом того, что сейчас большая часть кода генерируется компиляторами, это допустимый вариант. Но у него слишком много недостатков:

е) Просто завершить процесс и переключиться на следующий (пусть у нас многозадачная ОС). Соблазнительно, но, во-первых, плохо, во-вторых, невозможно. Плохо, потому что некоторые ошибки всё-таки не фатальны. Плохо и невозможно, потому что, даже если ошибка фатальна, закрытие процесса должно сопровождаться некоторыми специальными действиями: освобождение занятых ресурсов, удаление процесса из списка запущенных процессов и т.п. Процессор «сам» не в состоянии это сделать, так как он ничего не знает ни про ресурсы, ни про списки, ни про любые другие структуры, которые может использовать ОС для управления процессами. Он даже не сможет корректно «переключиться на следующий».

Хороший вариант

Help me if you can, I'm feeling down
And I do appreciate you being round.
Help me, get my feet back on the ground,
Won't you please, please help me. 

John Lennon

Поскольку, как видите, сам по себе процессор практически не способен адекватно реагировать на ошибки, остаётся последний вариант:

ё) Обратиться за помощью к программисту. Примерно так:

Это называется обработкой исключений (exception handling), и именно этот прогрессивный подход используется в процессорах Intel x86, начиная с 8086. Поэтому особого выбора у разработчиков Intel 386 не было: обратная совместимость это святое. Но, конечно, защищённый режим всё расширил и углубил.

Смотрим шире

Понятно, что подобный алгоритм можно использовать в любой ситуации, когда необходимо прервать текущий поток выполнения и вызвать некоторый системный код. Можно выделить несколько частных случаев:

Всё это вместе, обычно, называется прерываниями (interrupts) и, соответственно, обработкой прерываний (interrupt handling). При этом аппаратные прерывания относят к отдельному классу (они будут обсуждаться в следующей главе), а всё остальное (обычно) называют исключениями.

Основы обработки исключений

К «основам» я отнёс то, что объединяет все типы исключений: базовую терминологию и общие сведения о регистрации/обработке.

ПРИМЕЧАНИЕ

Поскольку, с точки зрения поведения процессора, программные прерывания можно рассматривать как один из подтипов исключений, эти «сведения» в основном являются ссылками на главу «Программные прерывания».

Терминология Intel

Исключение (exception) это нештатная ситуация, вызванная обработкой текущей команды. Обычно это связано с невозможностью выполнить команду по причине некорректности самой команды, параметров, или состояния системных структур, но есть несколько специальных случаев (например, команды into, bound; подробнее – ниже). Говорят, что процессор генерирует (generate) исключение.

Подпрограмма, зарегистрированная в системе и вызываемая при возникновении исключения, называется обработчиком исключения (exception handler). В различных ситуациях процессоры Intel x86 генерируют различные исключения, в результате вызываются различные обработчики. Типы и причины исключений документированы, программисту остаётся только соответствующим образом написать и зарегистрировать обработчик.

Регистрация

Обработчики исключений регистрируются точно так же, как обработчики программных прерываний: в таблице дескрипторов прерываний, при помощи дескриптора шлюза ловушки. Для исключений в IDT предназначены дескрипторы с номерами от 0 до 31, кроме 2 (о нём – в приложении, в описании NMI).

ПРЕДУПРЕЖДЕНИЕ

[Intel 2004] не рекомендует использовать эти номера для других целей. Но в тестах, конечно, можно :)

Описание наиболее интересных, на мой взгляд, исключений приведено в таблице.

Номер Название Описание
0 #DE Divide error. Ошибка при исполнении команды div или idiv. Два варианта: либо делитель равен 0, либо результат не влезает в отведённый для него регистр. С делением на 0 всё понятно, а вот пример ошибки второго типа: команда div bl делит ax на bl и помещает результат в al; если, к примеру, ax равен 256, а bl – 1, результат в al не поместится, и будет выброшено исключение. На мой взгляд, такое поведение команд деления – ошибка, допущенная разработчиками 8086 при проектировании.
1 #DB Debug exception. Упоминавшееся выше событие отладки. Текущая команда может быть любой.
3 #BP Breakpoint. Точка останова отладчика, команда int 3. Некоторые детали о команде int 3 приведены в приложении, в разделе «int 3».
4 #OF Overflow. Арифметические команды (add, sub, inc, dec и некоторые другие) при переполнении устанавливают в 1 флаг OF. При желании, с помощью команды into можно проверить значение этого флага: если OF равен 1, процессор сгенерирует исключение #OF.
5 #BR Bound range exceeded. Для проверки попадания индекса в пределы массива (шире – числа в заданный диапазон) можно использовать банальные cmp, а можно продвинутую команду bound. В последнем случае, при неверном индексе, процессор сгенерирует исключение #BR.
6 #UD Undefined opcode (Invalid opcode). Непонятный процессору код команды или специальная команда ud2 (начиная с Pentium Pro).
8 #DF Double fault. Во время обработки первого исключения произошло второе, причём очень неудачно. Интересная, но отдельная тема, подробнее – в приложении, в разделе «Double fault».
10 #TS Invalid TSS. Пока не важно, о чём это :) Подробнее – в главе «Защита: передача управления».
12 #SS Stack-segment fault. Проблемы со стеком. Основное – попытка чтения/записи за пределы стека. Команды типа push, pop, call, int, ret, iret и даже mov, если с сегментом SS.
13 #GP General protection. Самые разные ошибки, связанные с памятью и защитой. Например, попытка записи в read-only сегмент.
14 #PF Page fault. «Страничная ошибка». Запомните его! Это исключение является одной из основ реализации виртуальной памяти, подробнее – в главе «Страничная адресация».
18 #MC Machine Check. Сигнализирует о том, что с компьютером произошло что-то ужасное. Более конкретную информацию об ошибке получить можно, но способ получения зависит от модели процессора, описанию различных способов в [Intel 2004] посвящена отдельная глава (глава 14, «Machine-check architecture»), в курсе эта тема не рассматривается. В отличие от всех остальных исключений, #MC никак не связано с текущей выполняемой командой, в этом смысле оно ближе к аппаратным прерываниям.
Таблица 1. Описание избранных исключений.

Обработчики

Обработчик исключения очень похож на обработчик прерывания: более-менее обычная подпрограмма, заканчивающаяся вызовом iretd (скорее всего, действия, выполняемые обработчиками, должны отличаться по смыслу, но сейчас это нас не касается). Некоторые отличия между обработчиками связаны с тем, что процессор их немного по-разному вызывает. К началу работы обработчика прерывания на вершине стека находится содержимое регистров CS, EIP и EFLAGS (по 4 байта на регистр, и того 12 байт). Это относится и к обработчикам исключений, но:

Ещё одно отличие связано с тем, что возникновение программного прерывания, обычно, планируется программистом заранее, а возникновение исключения, обычно, нет. Поэтому «обычный» обработчик исключения не предполагает, что в регистрах программист передал ему какие-то дополнительные параметры. Но, конечно, никто не мешает вам написать «необычный» обработчик и использовать его «необычным» образом.

Классификация исключений

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

Борис Гребенщиков

Выше, в разделе «Обработчики» описаны основные отличия обработчика исключения от обработчика программного прерывания. С технической точки зрения, именно они и являются основой классификации. Правда, к первому «отличию» (значение адреса возврата в стеке) мы подойдём с неожиданной стороны, так как оно является следствием общего признака классификации: существования трёх типов исключений

ПРИМЕЧАНИЕ

Названия типов исключений разные авторы переводят по-своему, поэтому, чтобы не добавлять сумятицы, я буду пользоваться английскими терминами.

Fault

ПРИМЕЧАНИЕ

В [Зубков 1999] и [Орловский 1992] переведено как «ошибка», в [Гук 1999] как «отказ», в [Григорьев 1993] – «нарушение».

Fault-исключение генерируется процессором до начала выполнения команды. Поэтому:

После возврата из fault-исключения (если обработчик не поменяет в стеке адрес возврата) процессор попытается ещё раз выполнить ту же самую команду. Это наиболее часто используемый вид исключений, практически в любой ошибочной ситуации процессор с неизменным оптимизмом предлагает «попробовать ещё разик».

Trap

ПРИМЕЧАНИЕ

Везде переведено как «ловушка». Сговорились…

Trap-исключение генерируется процессором сразу после исполнения команды. Соответственно:

К типу trap (на данный момент) относятся только исключения, вызванные успешно исполнившимися командами, например int 3 или into. Сюда же можно отнести и int n, то есть все программные прерывания. По каким-то причинам (видимо, особенности реализации) bound генерирует fault-исключение.

Abort

ПРИМЕЧАНИЕ

В [Зубков 1999] переведено как «отказ», в [Орловский 1992] как «неудача», в [Гук 1999] как «аварийное завершение», [Григорьев 1993] – «авария». Обратите внимание: по [Гук 1999] «отказ» это fault.

Abort-исключение генерируется процессором в самом крайнем случае: когда, с его точки зрения, восстановление и нормальная работа уже невозможны, и системе (ну, как минимум, задаче) остаётся только красиво уйти. Поскольку значение адреса возврата в стеке не определено, скорее всего, ОС придётся смириться с диагнозом процессора.

К счастью для разработчиков ОС, пользовательские приложения не способны самостоятельно вызвать abort-исключение. Причиной такого исключения может быть либо аппаратный сбой, либо ошибка в ОС, либо серьёзный недочёт в архитектуре/реализации ОС, дающий пользовательскому ПО лишние возможности.

Код ошибки

Он может либо присутствовать, либо отсутствовать. Это и является признаком классификации: к первой группе относятся исключения, которым в стеке передаётся дополнительная информация, ко второй группе исключения, обходящиеся адресом возврата.

ПРИМЕЧАНИЕ

Формат кода ошибки, кроме кода ошибки исключения #PF, в курсе не рассматривается. Формат кода ошибки #PF рассматривается в главе «Страничная адресация».

Табличка

Описанные выше исключения классифицируются следующим образом:

Номер Название Тип Код ошибки
0 #DE (Divide error) Fault Нет
1 #DB (Debug exception) Fault или Trap Нет
3 #BP (Breakpoint) Trap Нет
4 #OF (Overflow) Trap Нет
5 #BR (Bounds range exceeded) Fault Нет
6 #UD (Undefined opcode) Fault Нет
8 #DF (Double fault) Abort Есть
10 #TS (Invalid TSS) Fault Есть
12 #SS (Stack segment fault) Fault Есть
13 #GP (General protection) Fault Есть
14 #PF (Page fault) Fault Есть
18 #MC (Machine Check) Abort Есть или нет
Таблица 2. Классификация избранных исключений.

И несколько комментариев к ней:

Всё вместе

- Каждому исключению – уважительное отношение и полноценную обработку!
- Ура!!!
... 
Из популистского выступления разработчиков ОС
на собрании процессоров.

Комбинируя признаки классификации, можно насчитать шесть типов исключений:

Все они вызываются немножко по-разному, и все их можно было бы рассмотреть. Но, поскольку:

Рассмотрены только fault- и abort-исключения. Оговорки те же, что и в случае программных прерываний:

Fault

Примерно так:

  1. По номеру исключения в IDT отыскивается нужный дескриптор, из него извлекается селектор сегмента и смещение начала обработчика.
  2. Проверяется корректность селектора (указывает на дескриптор в пределах GDT, это дескриптор сегмента кода, и т.п.) и смещения (попадает в сегмент).
  3. В стеке сохраняются регистр EFLAGS и дальний указатель на команду, вызвавшую исключение.
  4. [Для исключений с кодом ошибки] В стеке сохраняется код ошибки.
  5. CS и EIP загружаются новыми значениями.
  6. Начинается выполнение обработчика.
  7. [Для исключений с кодом ошибки] Во время выполнения обработчик обязан вытолкнуть из стека код ошибки, иначе корректная передача управления обратно будет невозможна.
  8. После своего завершения обработчик возвращает управление командой iretd.
  9. Возврат к исходной программе. В регистры EFLAGS, CS и EIP загружается значение из стека, при этом происходит обращение к GDT и проверка корректности устанавливаемого селектора и смещения.
ПРИМЕЧАНИЕ

В отличие от обработчиков программных прерываний, которые (обычно) послушно возвращают управление по адресу, сохранённому процессором в стеке, обработчики fault-исключений поступают таким образом гораздо реже, поскольку часто это приведёт только к повторной генерации того же самого исключения и ещё одному вызову того же самого обработчика. Поэтому пункты 8-9 могут либо не выполняться вообще, либо выполняться не так. Подробнее на ту тему см. ниже в разделе «Случилось страшное. Ну и?.. - II».

И на картинке, на примере исключения #GP:


Рисунок 1. Алгоритм вызова обработчика fault-исключения на примере General Protection.

Abort

Так как корректный возврат даже и не планируется, алгоритм упрощается:

  1. По номеру исключения в IDT отыскивается нужный дескриптор, из него извлекается селектор сегмента и смещение начала обработчика.
  2. Проверяется корректность селектора (указывает на дескриптор в пределах GDT, это дескриптор сегмента кода, и т.п.) и смещения (попадает в сегмент).
  3. В стеке сохраняется EFLAGS и какие-то значения на месте адреса возврата.
  4. [Для исключений с кодом ошибки] В стеке сохраняется код ошибки.
  5. CS и EIP загружаются новыми значениями.
  6. Начинается выполнение обработчика.

Соответствующая картинка:


Рисунок 2. Алгоритм вызова обработчика abort-исключения на примере Double Fault.

Случилось страшное. Ну, и?.. - II

Итак, процессор обнаружил ошибку, сгенерировал исключение, нашёл в IDT адрес обработчика и передал ему управление… ну и что с ним делать дальше? Допустим, понятно, как обрабатывать исключения, не являющиеся ошибками (в списке это #DB, #BP и #PF, соответственно нужно либо передать управление отладчику, либо подкачивать в память требуемую страницу), но что делать с остальными? Мы уже выяснили, что процессор «сам» не в состоянии справиться с обработкой и просит помощи у ОС, но ведь и ОС тоже далеко не всегда может предложить что-то разумное (например, что вы будете делать, если произошло исключение #DE?). Однако у ОС есть преимущества:

ПРИМЕЧАНИЕ

Наиболее интересен, конечно, первый вариант, и именно он реализован в большинстве современных ОС. В соответствии со стандартом POSIX, реализация пользовательских обработчиков исключений основана на сигналах (signal; есть такое понятие в UNIX-ах), Microsoft придумала свой подход – структурную обработку исключений (Structured Exception Handling, SEH). Рассмотрение использования и реализации сигналов и SEH выходит за рамки курса.

Программные прерывания vs. Исключения

Остерегайтесь подделок!

Поскольку обработчики исключений регистрируются в той же таблице и по тем же правилам, что и обработчики программных прерываний, выполнение команды int n с соответствующим значением n приведёт к вызову обработчика исключения (кстати, пример из главы «Программные прерывания», обрабатывающий нулевое прерывание – по совместительству исключение #DE, именно так и поступает). А, поскольку, (насколько я знаю) простого способа определить причину вызова изнутри обработчика не существует, скорее всего, обработчик примет всё за чистую монету.

Это была завязка. А теперь представьте, что пользователь вызвал обработчик #GP через int 13. Как вы помните, #GP это fault-исключение с кодом ошибки, то есть обработчик рассчитывает увидеть в стеке код ошибки, адрес вызвавшей исключение команды и EFLAGS. К сожалению, ожидания не оправдаются: процессор, обрабатывающий команду int 13 как программное прерывание (trap-исключение без кода ошибки), положит в стек только адрес следующей команды и EFLAGS. В результате:

Так как обработчик #GP это часть ОС, скорее всего, всё это приведёт к падению системы. Печально? Это ещё что, ведь пользователь может вызвать int 8 и обработчик #DF, который просто завершит систему!

К счастью, разработчики процессора предусмотрели решение этой проблемы: дескриптор шлюза ловушки в IDT может запрещать пользователю явный вызов обработчика прерывания командой int n, но разрешать обработку соответствующего исключения (точнее, при генерации исключения проверка прав производиться не будет). При нарушении будет сгенерировано исключение #GP.

ПРИМЕЧАНИЕ

Команды int 3 (для читавших приложение – обе формы int 3) и into считаются «явным» вызовом, то есть тоже могут быть запрещены. Команда bound «явным» вызовом не считается.

Подробнее эта тема рассмотрена в главе «Теоретическое введение в защиту».

Пример

Обработчик #GP

Программа устанавливает обработчик исключения #GP и генерирует соответствующее исключение при попытке изменить сегмент кода. Код полностью повторяет пример int0.asm из предыдущей главы, кроме следующих отличий:

ПРЕДУПРЕЖДЕНИЕ

В нормальных программах так делать, конечно, нельзя: если для генерации исключения использовать команду с другим размером, такой обработчик не приведёт ни к чему хорошему (см. первое задание, последний вариант).

; gpf.asm
; Программа, устанавливающая и вызывающая обработчик исключения #GP

        .model tiny
        .code
        .386p
        org     100h

;;;;;;;;;;;;;;;;;;;;;;;;;;;        
;
; Структуры
;
;;;;;;;;;;;;;;;;;;;;;;;;;;;

; Сегментный дескриптор
segment_descriptor struct
    limit_low   dw      0    ; Младшие два байта поля Segment limit
    base_low    dw      0    ; Младшие два байта поля Base Address
    base_high0  db      0    ; Второй байт поля Base Address
    type_and_permit db  0    ; Флаги
    flags       db      0    ; Ещё одни флаги
    base_high1  db      0    ; Старший байт поля Base Address
segment_descriptor ends

; Дескриптор шлюза
gate_descriptor struct
    offset_low  dw      0    ; Два младших байта поля Offset
    selector    dw      0    ; Поле Segment Selector
    zero        db      0
    type_and_permit db  0    ; Флаги
    offset_high dw      0    ; Старшие байты поля Offset
gate_descriptor ends

; Регистр, описывающий таблицу
table_register struct
    limit       dw      0    ; Table Limit
    base        dd      0    ; Linear Base Address
table_register ends

;;;;;;;;;;;;;;;;;;;;;;;;;;
;
; Код
;
;;;;;;;;;;;;;;;;;;;;;;;;;;

start:
        ; Подготавливаем DS
        push    cs
        pop     ds

        ; В es - начало видеобуфера. Можно было сделать то же
        ; самое средствами защищённого режима, но так проще
        push    0b800h
        pop     es

        ; Устанавливаем правильный сегмент в long-jmp-to-RM
        mov     ax, cs
        mov     cs:rm_cs, ax

        ; Прописываем адрес начала cs в качестве базового адреса сегмента
        call    cs_to_eax
        mov     dsc64kb.base_low, ax
        shr     eax, 16
        mov     dsc64kb.base_high0, al

        ; Сохраняем IDTR реального режима
        sidt    fword ptr old_idtr

        ; Запретили прерывания
        call disable_interrupts

        ; Инициализируем GDT
        call initialize_gdt

        ; Инициализируем IDT
        call initialize_idt

        ; Переключаем режим
        call set_PE

        ; 16-разрядный дальний переход. Перключает содержимое cs из нормального
        ; для реального режима (адрес) в нормальное для защищённого (селектор).
        ; Базовый адрес целевого сегмента совпадает с cs,
        ; поэтому смещение можно прописать сразу
        db      0EAh   ; код команды дальнего перехода
        dw      $ + 4  ; смещение
        dw      8      ; селектор

        ; В данный момент сегмент кода - 64 Кб, базовый адрес равен
        ; адресу сегмента кода до переключения в защищённый режим.

        mov cs:[0], eax ; Вызываем обработчик исключения

        call clear_PE

        ; Мы в реальном режиме, осталось разобраться с
        ; значением регистра cs

        ; 16-разрядный дальний переход. Перключает содержимое cs из нормального
        ; для защищённог режима (селектор) в нормальное для реальног (адрес).
        ; Адрес сегмента вычисляется и прописывается во время выполнения.
        db      0EAh   ; код команды дальнего перехода
        dw      $ + 4  ; смещение
rm_cs   dw      0      ; сегмент

        ; восстанавливаем IDTR реального режима
        lidt    fword ptr old_idtr

        ; разрешаем прерывания
        call enable_interrupts

        ret

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; Обработчик прерывания
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

; Обработчик исключения #GP, меняет символы и их атрибуты по всему экрану,
; Перед завершением увеличивает адрес возврата в стеке 
; на размер команды mov cs:[0] (5 байт)
gpf_handler:

        ; В данный момент, в стеке:
        ; esp +  0 - error code
        ; esp +  4 - EIP
        ; esp +  8 - CS
        ; esp + 12 - EFLAGS

        push   eax
        push   ecx

        mov    eax, 0         ; Текущий символ
        mov    ecx, 80 * 25   ; Колическтво сиволов на экране

screen_loop:
        inc    byte ptr es:[eax]          ; Меняем символ
        inc    eax
        inc    byte ptr es:[eax]          ; Меняем атрибут
        inc    eax
        loop   screen_loop

        pop    ecx
        pop    eax

        ; Удаляем из стека код ошибки
        add    esp, 4             
        ; Добавляем к адресу возврата 5 – размер команды mov cs:[0], eax
        ; В результате возврат произойдёт на начало следующей команды
        add    dword ptr [esp], 5 

        iretd

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; Данные
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

; Глобальная таблица дескрипторов
GDT     label byte
        ; Нулевой дескриптор
        segment_descriptor <>
        ; Дескриптор сегмента кода, размер 64 Kb
dsc64kb segment_descriptor <0ffffh, 0, 0, 10011010b, 0, 0>
        		; 10011010b - 1001, C/D - 1, 0, R/W - 1, 0
        		; 0         - G - 0, 000, Limit - 0
       		
; Данные для загрузки в GDTR
gdtr    table_register <$ - GDT - 1, 0>


; Таблица дескрипторов прерываний
IDT     label byte

        gate_descriptor <>  ;0 - #DE
        gate_descriptor <>  ;1 - #DB
        gate_descriptor <>  ;2
        gate_descriptor <>  ;3 - #BP
        gate_descriptor <>  ;4 - #OF
        gate_descriptor <>  ;5 - #BR
        gate_descriptor <>  ;6
        gate_descriptor <>  ;7
        gate_descriptor <>  ;8 - #DF
        gate_descriptor <>  ;9 
        gate_descriptor <>  ;10
        gate_descriptor <>  ;11
        gate_descriptor <>  ;12 - #SS

        ; 13 - #GP
        ; Дескриптор шлюза ловушки. 
        ; Обработчик исключения находится в сегменте, соответствующем первому
        ; дескриптору GDT. Поскольку базовый адрес сегмента такой же, как
        ; в реальном режиме, смещение обработчика тоже совпадает.
        gate_descriptor <gpf_handler, 8, 0, 8Fh, 0>

; Данные для загрузки в IDTR
idtr    table_register <$ - IDT - 1, 0>

; Место для IDTR реального режима
old_idtr table_register <>  

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; Служебные функции
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

; Инициализирует IDT
initialize_idt:
        ; Вычисляем линейный адрес начала массива дескрипторов
        call    cs_to_eax
        add     eax, offset IDT
        ; Записываем его в структуру
        mov     idtr.base, eax

        ; Загружаем IDTR
        lidt    fword ptr idtr
        ret

; Инициализирует GDT
initialize_gdt:
        ; Вычисляем линейный адрес начала массива дескрипторов
        call    cs_to_eax
        add     eax, offset GDT
        ; Записываем его в структуру
        mov     gdtr.base, eax

        ; Загружаем GDTR
        lgdt    fword ptr gdtr
        ret

; Запрещает маскируемые и немаскируемые прерывания
disable_interrupts:
        cli              ; запретить прерывания
        in      al, 70h  ; индексный порт CMOS
        or      al, 80h  ; установка бита 7 в нем запрещает NMI
        out     70h, al
        ret

; Разрешает маскируемые и немаскируемые прерывания
enable_interrupts:
        in      al, 70h  ; индексный порт CMOS
        and     al, 7Fh  ; сброс бита 7 отменяет блокирование NMI
        out     70h, al
        sti              ; разрешить прерывания
        ret

; Устанавливает флаг PE
set_PE:
        mov     eax, cr0 ; прочитать регистр CR0
        or      al, 1    ; установить бит PE,
        mov     cr0, eax ; с этого момента мы в защищенном режиме
        ret

; Сбрасывает флаг PE
clear_PE:
        mov     eax, cr0 ; прочитать CR0
        and     al, 0FEh ; сбросить бит PE
        mov     cr0, eax ; с этого момента мы в реальном режиме
        ret

; Вычисляет линейный адрес начала сегмента кода
cs_to_eax:
        mov     eax, 0
        mov     ax, cs
        shl     eax, 4
        ret

        end     start

Задания


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