Мне и больно, мне и сладко, что он не такой как все Сергей Чиграков Приложение выполнило недопустимую операцию и будет закрыто 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). При этом аппаратные прерывания относят к отдельному классу (они будут обсуждаться в следующей главе), а всё остальное (обычно) называют исключениями.
К «основам» я отнёс то, что объединяет все типы исключений: базовую терминологию и общие сведения о регистрации/обработке.
ПРИМЕЧАНИЕ Поскольку, с точки зрения поведения процессора, программные прерывания можно рассматривать как один из подтипов исключений, эти «сведения» в основном являются ссылками на главу «Программные прерывания». |
Исключение (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 никак не связано с текущей выполняемой командой, в этом смысле оно ближе к аппаратным прерываниям. |
Обработчик исключения очень похож на обработчик прерывания: более-менее обычная подпрограмма, заканчивающаяся вызовом iretd (скорее всего, действия, выполняемые обработчиками, должны отличаться по смыслу, но сейчас это нас не касается). Некоторые отличия между обработчиками связаны с тем, что процессор их немного по-разному вызывает. К началу работы обработчика прерывания на вершине стека находится содержимое регистров CS, EIP и EFLAGS (по 4 байта на регистр, и того 12 байт). Это относится и к обработчикам исключений, но:
Ещё одно отличие связано с тем, что возникновение программного прерывания, обычно, планируется программистом заранее, а возникновение исключения, обычно, нет. Поэтому «обычный» обработчик исключения не предполагает, что в регистрах программист передал ему какие-то дополнительные параметры. Но, конечно, никто не мешает вам написать «необычный» обработчик и использовать его «необычным» образом.
Есть люди у которых обращаются на Вы, Есть люди у которых сто четыре головы, Есть загадочные девушки с магнитными глазами, Есть большие пассажиры мандариновой травы... Борис Гребенщиков
Выше, в разделе «Обработчики» описаны основные отличия обработчика исключения от обработчика программного прерывания. С технической точки зрения, именно они и являются основой классификации. Правда, к первому «отличию» (значение адреса возврата в стеке) мы подойдём с неожиданной стороны, так как оно является следствием общего признака классификации: существования трёх типов исключений
ПРИМЕЧАНИЕ Названия типов исключений разные авторы переводят по-своему, поэтому, чтобы не добавлять сумятицы, я буду пользоваться английскими терминами. |
ПРИМЕЧАНИЕ В [Зубков 1999] и [Орловский 1992] переведено как «ошибка», в [Гук 1999] как «отказ», в [Григорьев 1993] – «нарушение». |
Fault-исключение генерируется процессором до начала выполнения команды. Поэтому:
После возврата из fault-исключения (если обработчик не поменяет в стеке адрес возврата) процессор попытается ещё раз выполнить ту же самую команду. Это наиболее часто используемый вид исключений, практически в любой ошибочной ситуации процессор с неизменным оптимизмом предлагает «попробовать ещё разик».
ПРИМЕЧАНИЕ Везде переведено как «ловушка». Сговорились… |
Trap-исключение генерируется процессором сразу после исполнения команды. Соответственно:
К типу trap (на данный момент) относятся только исключения, вызванные успешно исполнившимися командами, например int 3 или into. Сюда же можно отнести и int n, то есть все программные прерывания. По каким-то причинам (видимо, особенности реализации) bound генерирует fault-исключение.
ПРИМЕЧАНИЕ В [Зубков 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 | Есть или нет |
И несколько комментариев к ней:
- Каждому исключению – уважительное отношение и полноценную обработку! - Ура!!! ... Из популистского выступления разработчиков ОС на собрании процессоров.
Комбинируя признаки классификации, можно насчитать шесть типов исключений:
Все они вызываются немножко по-разному, и все их можно было бы рассмотреть. Но, поскольку:
Рассмотрены только fault- и abort-исключения. Оговорки те же, что и в случае программных прерываний:
Примерно так:
ПРИМЕЧАНИЕ В отличие от обработчиков программных прерываний, которые (обычно) послушно возвращают управление по адресу, сохранённому процессором в стеке, обработчики fault-исключений поступают таким образом гораздо реже, поскольку часто это приведёт только к повторной генерации того же самого исключения и ещё одному вызову того же самого обработчика. Поэтому пункты 8-9 могут либо не выполняться вообще, либо выполняться не так. Подробнее на ту тему см. ниже в разделе «Случилось страшное. Ну и?.. - II». |
И на картинке, на примере исключения #GP:
Так как корректный возврат даже и не планируется, алгоритм упрощается:
Соответствующая картинка:
Итак, процессор обнаружил ошибку, сгенерировал исключение, нашёл в IDT адрес обработчика и передал ему управление… ну и что с ним делать дальше? Допустим, понятно, как обрабатывать исключения, не являющиеся ошибками (в списке это #DB, #BP и #PF, соответственно нужно либо передать управление отладчику, либо подкачивать в память требуемую страницу), но что делать с остальными? Мы уже выяснили, что процессор «сам» не в состоянии справиться с обработкой и просит помощи у ОС, но ведь и ОС тоже далеко не всегда может предложить что-то разумное (например, что вы будете делать, если произошло исключение #DE?). Однако у ОС есть преимущества:
ПРИМЕЧАНИЕ Наиболее интересен, конечно, первый вариант, и именно он реализован в большинстве современных ОС. В соответствии со стандартом POSIX, реализация пользовательских обработчиков исключений основана на сигналах (signal; есть такое понятие в UNIX-ах), Microsoft придумала свой подход – структурную обработку исключений (Structured Exception Handling, SEH). Рассмотрение использования и реализации сигналов и SEH выходит за рамки курса. |
Остерегайтесь подделок!
Поскольку обработчики исключений регистрируются в той же таблице и по тем же правилам, что и обработчики программных прерываний, выполнение команды 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 и генерирует соответствующее исключение при попытке изменить сегмент кода. Код полностью повторяет пример 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 |