And I wait for them to interrupt Me drinking from that broken cup And ask me to open up the gate for you. Bob Dylan Что стучишься в дверь моя, видишь – дома нет никто, если ты моя жена – заходи по одному... Присказка из недалёкого детства
Как вы, без сомнения, знаете, помимо процессора и оперативной памяти, в состав компьютера входят и другие полезные устройства, с которыми ОС так или иначе приходится взаимодействовать. И, естественно, этот факт нельзя было проигнорировать при разработке процессора.
Собственно способ взаимодействия тривиален: поскольку единственное, что умеет делать процессор – обработка данных, представленных набором битов/байтов/слов, «взаимодействие» сводится к чтению или записи во внутренние регистры устройства. От устройства зависит то, как именно доступны регистры (через заданные физические адреса в памяти [команда mov], либо через порты ввода/вывода [команды in и out]), что за данные передаются, и какого протокола нужно придерживаться, но это всё детали. Конечно, настоящая ОС должна уметь работать хоть с какими-то реальными устройствами, но к архитектуре процессора адреса/порты/протоколы имеют слабое отношение, и рассмотрение программирования конкретных устройств выходит за рамки курса.
А вот что действительно интересно – возможность начинать взаимодействие по инициативе устройства. В случае Intel x86 это называется внешним (external) или аппаратным (hardware generated) прерыванием (interrupt), а результатом прерывания является исполнения зарегистрированной в системе подпрограммы – обработчика прерывания (interrupt handler).
О внешних прерываниях, о том, для чего они используются, об их преимуществах и недостатках по сравнению с опросом готовности (polling; иногда называют иначе, но пусть будет «опрос готовности»), должно быть сказано в любом приличном курсе по архитектуре ОС (если не сказано – курс не приличный), надеюсь, вы прочитали/прослушали хотя бы один. Тем не менее, из-за исключительной важности темы, я попытаюсь ещё раз всё это объяснить. На всякий случай.
Скорее всего, все представляют себе, что такое внешнее прерывание. Описать можно примерно так: некоторое внешнее устройство (клавиатура, таймер, COM-порт, …) хочет что-то сообщить процессору (нажата клавиша, прошло 0.001 секунды, приняты новые данные, …), причём оно не ждёт, пока процессор обратит на него внимание, а самостоятельно его «дёргает». В результате возникает особая ситуация, называемая внешним прерыванием, и, если это прерывание не заблокировано тем или иным способом, вызывается обработчик прерывания. Как и программные, внешние прерывания тоже нумеруются, и их обработчики тоже регистрируются в IDT, причём почти так же.
Это всё хорошо и верно, но совершенно недостаточно, так как не отражает должным образом важность и уникальность механизма внешних прерываний. Нужно понимать, что они, и только они:
Внешние прерывания дают возможность диалога с процессором. Без них компьютер способен работать только в режиме «пакетной обработки заданий»: загрузили ящик данных, подумал, погудел, выдал результат. Готов принять следующий ящик… Ну, то есть, это некоторое преувеличение, система типа DOS сможет более-менее прожить без внешних прерываний (от некоторых возможностей, конечно, придётся отказаться, да и программировать будет не так удобно), но ни о какой ОС с вытесняющей многозадачностью речь не будет идти точно.
Основные области применения:
ПРИМЕЧАНИЕ Попробуйте придумать устройство, не попадающее ни в одну из категорий :) Но при этом нельзя сказать, что внешние прерывания – всегда оптимальное решение. В некоторых случаях это может оказаться чрезмерно дорого именно с точки зрения производительности: иногда дешевле доделать дело до конца, а потом переключиться на следующее, вместо того чтобы пытаться сделать оба одновременно, бегая туда-сюда. Тем более, если переключение тоже не бесплатное (о том, почему переключение может оказаться не бесплатным, написано в главе «Защита: передача управления»). Кроме того, если делать всё последовательно, алгоритм может получиться значительно проще. |
Неплохая получилась агитка? :) Всё, что написано выше – правда.. почти.
Полноценный аналог аппаратных прерываний элементарно реализуется при помощи исключения #DB (отладка) и флага TF (Trap Flag, трассировка, 8-й бит регистра EFLAGS). Если флаг TF установить в 1, то после выполнения каждой инструкции процессор будет генерировать исключение #DB, в обработчике можно проверять состояние всех необходимых устройств и, если надо, эмулировать возникновение аппаратного прерывания.
ПРИМЕЧАНИЕ Забегая на пару страниц вперёд: можно даже поддерживать приоритеты, маскирование прерываний и т.п., то есть полностью взять на себя работу процессора и ПКП (программируемый контроллер прерываний, вы познакомитесь с ним ниже) по обслуживанию аппаратных прерываний. |
Но результат окажется крайне медленным, может быть, даже медленнее виртуальной машины. На каждую инструкцию полезного кода приходится: передача управления туда, код обработчика #DB, передача управления обратно. Плюс, естественно, сбрасывается конвейер.
Поэтому, если вам не нужна какая-то уж очень необычная логика обработки прерываний, лучше остановиться на стандартной аппаратной реализации. Но идея красивая.
Другие варианты использования отладочных возможностей, а так же исключения #PF и #MC, тоже позволяют обработчику получить управление неожиданно для текущего кода, но они не позволяют делать это достаточно регулярно и гарантировано.
Элементом IDT, соответствующим внешнему прерыванию, может быть и дескриптор шлюза ловушки, но специально для этой цели предназначен дескриптор шлюза прерывания (interrupt gate descriptor). Он очень похож на дескриптор шлюза ловушки, единственное отличие – при входе в обработчик через дескриптор шлюза прерывания, автоматически сбрасывается флаг IF, маскируя внешние прерывания (как вы помните, в реальном режиме внешние прерывания маскируются при входе в обработчик любого прерывания). Форматы дескрипторов тоже практически одинаковые, отличается только один бит (0-й бит пятого байта), тем не менее, для полноты картины, привожу описание:
Положение | Название | Краткое описание |
---|---|---|
Нулевой и первый байты | Offset (part 1) | Младшие два байта 32-х битного поля Offset. Поле Offset содержит смещение обработчика прерывания. |
Второй и третий байты | Segment Selector | Селектор сегмента, содержащего обработчик прерывания. |
Четвёртый байт | 0 | 0, просто 0. Эта часть дескриптора не используется. |
0-й – 3-й биты пятого байта | Type | Дескриптору шлюза прерывания соответствует значение 1110b. Младший бит этого поля – единственное отличие от дескриптора шлюза ловушки по формату. |
4-й бит пятого байта | S | Это системный дескриптор, поэтому 0. |
5-й – 7-й биты пятого байта | ?? | Пока неважно, устанавливайте в 100b. |
Шестой и седьмой байты | Offset (part 2) | Старшие два байта поля Offset. |
То же самое на картинке:
И, конечно, пример:
db 04h ; Offset – два младших байта db 03h db 8 ; Segment selector db 0 db 0 ; 0 db 10001110b ; 10001110 – магическое число.. db 02h ; Offset – два старших байта db 01h |
Это дескриптор шлюза прерывания, селектор сегмента указывает на первый дескриптор GDT, смещение – 01020304h.
Никто не мешает использовать дескриптор шлюза прерывания для программных прерываний и исключений, и наоборот, дескриптор шлюза ловушки для внешних прерываний, это вполне возможно, а иногда может оказаться даже наиболее правильно.
Два типа дескрипторов существуют для того, чтобы можно было явно отмечать обработчики, которые могут быть прерваны асинхронным внешним прерыванием и обработчики, которые не должны быть прерваны практически никогда. С одной стороны, чем меньше в системе обработчиков второго типа, тем лучше, так как тем выше реактивность системы, но с другой стороны, написание прерываемых обработчиков может оказаться неоправданно сложным. Выбор остаётся за разработчиками ОС, задачей разработчиков процессора было предоставить им этот выбор.
Штирлиц и Борман стреляли по очереди. Очередь с криками разбегалась. Анекдот
Возникновение и обработка внешнего прерывания – сложный процесс, в котором задействовано несколько устройств, различные внутрипроцессорные структуры (регистры, флаги, стек) и собственно обработчик. Поэтому, чтобы не мешать всё в одну кучу, описание разделено на две части: «вне процессора» и «внутри процессора»; первая часть в основном посвящена сигналам, шинам и соединениям, вторая – более привычным вопросам: регистры, стек и обработчик.
ПРЕДУПРЕЖДЕНИЕ То, что написано ниже в этом разделе, как минимум, не совсем правда, особенно для современных процессоров. Это – простая и полезная схема, без которой очень сложно понимать и работать с внешними прерываниями, а «правда» вам и не нужна :) |
Примерно половину задач, возникающих при возникновении прерывания, берёт на себя программируемый контроллер прерываний (programmable interrupt controller, PIC; по-русски – ПКП). Схема подключения показана на Рисунке 2.
ПРИМЕЧАНИЕ Ниже описывается схема работы ПКП i8259A, использовавшегося в компьютерах на основе ранних версий x86 (более подробно он описан ниже, в соответствующем разделе). Не фиксируйтесь на деталях, сначала нужно представить себе круг задач, для которых предназначен любой ПКП, детали здесь приведены для того, чтобы описание не стало чересчур абстрактным. |
Последовательность действий при обработке прерывания:
В результате, ПКП отвечает за:
И всем этим значительно облегчает работу процессора.
После того, как ПКП передал процессору номер прерывания, в обработку включается собственно процессор. Тут всё как всегда, с минимальными отличиями:
На картинке:
Естественно, при желании, обработчик может возвращать управление вовсе не туда, откуда он был вызван. На этом основана реализация многозадачности.
Когда вошёл контролёр, Скорость перевалила за сто. Он даже не стал проверять билеты, Он лишь попросил снять пальто. Борис Гребенщиков
В ранних моделях x86 ПКП действительно был устроен так, как написано ниже. Но, начиная с Pentium, Intel озаботилась возможностью установки нескольких процессоров на одну материнскую плату, в связи с чем ПКП был основательно переработан. Тем не менее, из соображений обратной совместимости, с ним можно работать так же, как и раньше. Возможно, это не лучший вариант, но зато самый простой и наиболее полно описанный в литературе. Итак, перенесёмся в далёкие 80-е. Представьте: Гребенщиков молодой и худенький, Цой жив и только-только записал «45», а Intel использует для реализации ПКП микросхему i8259A.
Замечательно простое и функциональное устройство. Краткие ТТХ:
Управляется через два регистра размером по одному байту каждый, они доступны программисту через порты ввода-вывода (команды in и out).
ПРИМЕЧАНИЕ ОС имеет возможность запретить пользователям прямые обращения к портам, поэтому пользователь не сможет нарушить работу ОС, управляя ПКП напрямую. Подробнее, как обычно, в главах, посвященных защите. |
Поскольку собственных названий регистры не имеют, они будут обозначаться как нулевой и первый (о том, какие именно номера портов соответствуют регистрам – в следующем разделе). Ниже кратко перечислены наиболее полезные возможности.
ПРИМЕЧАНИЕ Описание, мягко говоря, не полное: во-первых, перечислено далеко не всё, во-вторых, даже перечисленное толком не объяснено (за всеми этими битовыми масками скрывается глубокий смысл!). На мой взгляд, более подробные знания не нужны, но если вы вдруг заинтересуетесь, обращайтесь к какой-нибудь хорошей книжке времён MS-DOS. Лично я рекомендую [2B ProGroup 1995]. Описание ПКП, приведённое в [Зубков 1999], не очень удачно: во-первых, встречаются ошибки, во-вторых, хотя там и перечислены все флаги, их смысл не всегда остаётся ясен. Возможно, в более новых изданиях ситуация улучшена. |
Пример маскирования всех прерываний, кроме, устройства, подключённого к ir1:
mov al, 11111101b out <первый>, al |
Каждый разряд числа соответствует входу микросхемы, разряды, установленные в 1 маскируют соответствующий им вход, разряды установленные в 0 демаскируют.
Для получения списка замаскированных прерываний нужно читать тот же регистр, результат возвращается в том же формате. Это даёт следующую возможность:
in al, <первый> or al, 00000001b out <первый>, al |
То есть замаскировать ir0, а все остальные оставить в том же состоянии.
В рамках курса – так:
mov al, 20h out <нулевой>, al |
ПРЕДУПРЕЖДЕНИЕ Напоминаю, это не просто «полезная возможность», это глубокая жизненная необходимость! Если не завершать прерывания, система просто не будет работать. |
ПРИМЕЧАНИЕ Это называется «неспецифичное завершение прерывания» (специфичное отличается тем, что нужно указывать номер входа irX, а этот вариант подходит для всех случаев). После выполнения этих команд ПКП удалит наиболее приоритетное прерывание из своего списка «обрабатываемые в данный момент». Если обрабатывается всего одно прерывание, оно и будет удалено, если их несколько, ситуация сложнее. Несколько прерываний может оказаться только в том случае, если менее приоритетное было прервано более приоритетным, и обработка более приоритетного ещё не закончилась. Таким образом (если программист нигде не нарушил правила, никто ведь не мешает вызвать эти команды несколько раз подряд в одном и том же обработчике, или наоборот пропустить их), завершая наиболее приоритетное прерывание из списка, ПКП завершает именно то прерывание, обработчик которого действительно выполняется процессором в данный момент. |
Для этого надо заново инициализировать ПКП. Всё в тех же рамках, это должно выглядеть так:
out <нулевой>, 00010001b out <первый>, <номер обработчика для ir0> ; должен быть кратен восьми out <первый>, 00000100b ; или 2, см. следующий раздел out <первый>, 00000001b |
ПРЕДУПРЕЖДЕНИЕ В [Зубков 1999] предложен другой вариант, который в корне неверен, хотя и работает :) Оригинальный ПКП i8259A был предназначен для широкого применения, его можно было гибко настраивать, но эти возможности никогда не использовались в PC. Современный ПКП, работающий в режиме эмуляции i8259A, не понимает и игнорирует примерно половину устанавливаемых при инициализации флагов. Если бы он вдруг начал их понимать, соответствующий код из [Зубков 1999] перестал бы работать. |
Команды, посылаемые ПКП при инициализации, называются ICW1 – ICW4 (Initialization Command Word). Команда ICW3 связана с каскадным соединением i8259A, более подробно она описана ниже.
Всего восемь устройств – маловато для серьёзной работы. Разработчики i8259A это понимали, поэтому предусмотрели возможность объединения нескольких микросхем в один ПКП. Грубо говоря, для этого нужно выход int одной микросхемы i8259A подать на вход irX другой, после чего они смогут работать «почти как одна», это и называется каскадным соединением.
ПРИМЕЧАНИЕ Для того чтобы говорить «не грубо», надо описать, что происходит с сигналами inta и D. Ну да, с ними действительно что-то происходит, в этом процессе задействовано ещё несколько сигналов, но, к счастью, это абсолютно не влияет на программную архитектуру, поэтому не должно вас интересовать. Хуже того, поскольку в современных ПКП микросхемы i8259A уже давно не используются, это неважно вообще. |
Таким способом можно соединить до девяти микросхем (одна ведущая и восемь ведомых, многоуровневое каскадирование не поддерживается), получив возможность работать с 64-мя устройствами. При этом на уровне сигналов int/intr/inta/D достигается полная прозрачность, то есть, с точки зрения «аппаратного уровня» ничего не меняется. Но на «программном уровне» появляются отличия: каждый i8259A имеет свои собственные управляющие регистры, их надо отдельно инициализировать (ведущий и ведомые по-разному) и ими надо по отдельности командовать.
Но это теория. Практически, в компьютерах на основе 80286 используется две микросхемы i8259A, ведомая подключена к входу ir2 ведущей. Схема показана на Рисунке 4, на нём же отображено соответствие irX – IRQX и несколько наиболее полезных прерываний (при желании вы можете найти «классический» список внешних прерываний в любой книжке по ассемблеру, например в [Зубков 1999]).
Что ещё можно сказать про ПКП:
И, наконец, правильная инициализация. Для ведущего:
out 20h, 00010001b out 21h, <номер обработчика для ir0> ; должен быть кратен восьми out 21h, 00000100b ; битовая маска, единицей отмечены входы, ; к которым подключены не обычные устройства, ; а ведомые ПКП. out 21h, 00000001b |
Для ведомого:
out A0h, 00010001b out A1h, <номер обработчика для ir0> ; должен быть кратен восьми out A1h, 2 ; номер ведомого ПКП, совпадает с номером входа ; ведущего ПКП, к которому подключен ведомый ПКП out A1h, 00000001b |
Точно так же, как и в случае с обработчиком исключения, непредсказуемый пользователь может попытаться вызвать через int обработчик внешнего прерывания. При этом, хотя стек вызова будет правильный, если не повезёт, результат может оказаться даже более эффектным – с аппаратурой лучше не шутить. Рекомендуемое решение проблемы стандартное – запретить пользователям вызывать обработчики напрямую, дескриптор шлюза прерывания такую возможность даёт, подробнее в главах про защиту.
Но если вам вдруг захочется «поиграть в демократию» (лучше не надо!) или подстраховаться от ошибок в самой ОС (а вот это может быть оправдано), есть ещё один вариант. Можно в обработчике запросить у ПКП список обрабатываемых в данный момент прерываний и проверить присутствие в списке «себя». Примерно так:
mov al, 00001011b ; 00001011b - команда запроса списка ; обрабатываемых прерываний out <нулевой>, al in al, <нулевой> ; Теперь в al маска, единицами отмечены ; обрабатываемые прерывания |
ПРЕДУПРЕЖДЕНИЕ В [Зубков 1999] в описании этой команды допущена ошибка: вместо чтения нулевого регистра, предлагается читать первый. Возможно, в более новых изданиях ошибка исправлена. |
Счастливые часов не наблюдают. Александр Сергеевич Грибоедов
Часы реального времени (Real Time Clock, RTC) – замечательное устройство, очень привлекательное в качестве примера:
Единственная проблема – просто так RTC работать не будет, его тоже надо немного программировать. К счастью, это не сложно. У RTC довольно много регистров, но для работы с ним используется только два порта: 70h и 71h. Порт 70h – индексный, в него записывается номер регистра RTC, который будет прочитан/записан при следующем обращении к порту 71h. Если подряд идет несколько обращений к одному и тому же регистру RTC (например, чтение – наложение маски – запись нового значение), переустанавливать значение индекса необязательно.
Ниже, с самыми минимальными пояснениями, приведён код, который используется в примере, за более подробной информацией обращайтесь, например, к [Зубков 1999].
Перевод RTC в режим периодических прерываний:
; Разрешение периодического прерывания RTC mov al, 0bh ; прочитать регистр 0Bh out 70h, al in al, 71h or al, 01000000b ; Установить шестой бит в 1 out 71h, al ; Записать обратно |
Аналогично, возврат в режим по умолчанию:
; Запрещение периодического прерывания RTC mov al, 0bh ; прочитать регистр 0Bh out 70h, al in al, 71h and al, 10111111b ; Сбросить шестой бит out 71h, al ; Записать обратно |
Настройка частоты прерываний:
; Установка частоты периодического прерывания RTC mov al, 0ah ; прочитать регистр 0Ah out 70h, al in al, 71h or al, 0fh ; Четыре младших бита определяют частоту. ; 1111 - 2 раза в секунду. out 71h, al ; Записать обратно |
После возникновения прерывания, RTC устанавливает флаги (вам не важно какие :)) в регистре 0Ch, при чтении этого регистра флаги сбрасываются. А пока они не сброшены, новых прерываний не происходит (аналог списка обрабатываемых прерываний ПКП и команды завершения обработки прерывания).
; Читаем из регистра 0Ch RTC, иначе прерываний больше не будет mov al, 0ch out 70h, al in al, 71h |
Программа последовательно выполняет следующие действия:
Код написан на основе примера int0.asm из главы про программные прерывания.
; int_rtc.asm ; Программа, устанавливающая и вызывающая обработчик прерывания RTC .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 Кб, базовый адрес равен ; адресу сегмента кода до переключения в защищённый режим. ; Сохраняем состояние масок ПКП in al, 021h mov old_mask1, al in al, 0A1h mov old_mask2, al ; Инициализация ПКП, в bl и bh - базовые номера прерываний ; Ведущий ПКП ставим с 20h, ведомый с 28h mov bl, 020h mov bh, 028h call initialize_pic mov al, 0fbh ; на ведущем маскируем всё, кроме IRQ2 out 021h, al mov al, 0feh ; на ведомом маскируем все, кроме IRQ8 (RTC) out 0A1h, al ; Установка частоты периодического прерывания RTC mov al, 0ah out 70h, al in al, 71h or al, 0fh ; 2 раза в секунду out 71h, al ; Разрешение периодического прерывания RTC mov al, 0bh out 70h, al in al, 71h or al, 01000000b out 71h, al mov ecx, 10 ; в ecx – счётчик прерываний call enable_interrupts ; в цикле ждём, пока прерывание произойдёт 10 раз и обнулит ecx test_end: cmp ecx, 0 jne test_end call disable_interrupts ; Запрещаем переодическое прерывание от RTC mov al, 0bh out 70h, al in al, 71h and al, 10111111b out 71h, al ; Возвращаем стандартные номера прерываний mov bl, 08h mov bh, 70h call initialize_pic mov al, old_mask1 ; снимаем маскировку out 021h, al mov al, old_mask2 ; снимаем маскировку out 0A1h, al call clear_PE ; Мы в реальном режиме, осталось разобраться с ; значением регистра cs ; 16-разрядный дальний переход. Перключает содержимое cs из нормального ; для защищённог режима (селектор) в нормальное для реальног (адрес). ; Адрес сегмента вычисляется и прописывается во время выполнения. db 0EAh ; код команды дальнего перехода dw $ + 4 ; смещение rm_cs dw 0 ; сегмент ; восстанавливаем IDTR реального режима lidt fword ptr old_idtr ; разрешаем прерывания call enable_interrupts ret ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ;; Обработчик прерывания ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Обработчик прерывания RTC, ведёт обратный отсчёт intRtc_handler: push eax push ebx dec ecx ; уменьшение ecx mov bh, 07h ; белый текст на чёрном фоне mov bl, '0' add bl, cl ; если cl меньше 10, то в bl соответствующая цифра mov eax, 80 * 24 * 2 mov word ptr es:[eax], bx call upscroll_screen ; прокручивает экран на строчку вверх ; читаем из регистра 0Ch RTC, иначе прерываний больше не будет mov al, 0ch out 70h, al in al, 71h mov al, 020h ; «неспецифичное» завершение прерывания out 020h, al ; на ведущем ПКП out 0A0h, al ; на ведомом ПКП pop ebx pop eax iretd ; 32-х разрядный возврат из прерывания ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ;; Данные ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Глобальная таблица дескрипторов 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 db 32 dup ( 8 dup (0)) ; 0 – 1Fh db 8 dup ( 8 dup (0)) ; 20h – 27h, ведущий ПКП ; Дескриптор шлюза прерывания. ; Обработчик прерывания находится в сегменте, соответствующем первому ; дескриптору GDT. Поскольку базовый адрес сегмента такой же, как ; в реальном режиме, смещение обработчика тоже совпадает. gate_descriptor <intRtc_handler, 8, 0, 8Eh, 0> ; Данные для загрузки в IDTR idtr table_register <$ - IDT - 1, 0> ; Место для хранения IDTR реального режима old_idtr table_register <> ; Место для хранения старых значений масок ПКП old_mask1 db 0 old_mask2 db 0 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ;; Служебные функции ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Инициализирует 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 ; Инициализация ПКП. ; bl – базовый номер прерывания ведущего ПКП ; bh - базовый номер прерывания ведомого ПКП initialize_pic: push eax mov al, 00010001b ; ICW1 out 020h, al out 0A0h, al mov al, bl ; ICW2, ведущий out 021h, al mov al, bh ; ICW2, ведомый out 0A1h, al mov al, 00000100b ; ICW3, ведущий out 021h, al mov al, 2 ; ICW3, ведомый out 0A1h, al mov al, 00000001b ; ICW4 out 021h, al out 0A1h, al pop eax ret ; Прокручивает экран на строчку вверх upscroll_screen: push eax push ecx push edx mov eax, 0 ; Текущий символ mov ecx, 80 * 24 ; Колическтво символов на экране ; (без последней строки, она отдельно) screen_loop: mov dx, word ptr es:[eax * 2 + 80*2] mov word ptr es:[eax * 2], dx ; Меняем символ inc eax loop screen_loop mov ecx, 80 ; Длинна последней строки mov dx, 0720h ; символ, которым заполняется строка last_line_loop: mov word ptr es:[eax * 2], dx ; Меняем символ inc eax loop last_line_loop pop edx pop ecx pop eax ret end start |