Программирование процессоров Intel x86 в защищённом режиме
Внешние прерывания

Автор: sergh
The RSDN Group
Опубликовано: 01.11.2005
Версия текста: 1.0

Что и зачем
Что [немного наглядной агитации]
Зачем
… и как всё-таки обойтись без них :)
Дескриптор шлюза прерывания
Как [всё по очереди]
Вне процессора
Внутри процессора
Программируемый контроллер прерываний Intel x86
ПКП i8259A
Каскадное соединение ПКП i8259A в x86
Программные прерывания vs. Внешние
Пример
Часы реального времени
Обработчик прерывания RTC
Задания

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.
Таблица 1. Формат дескриптора шлюза прерывания.

То же самое на картинке:


Рисунок 1. Формат дескриптора шлюза прерывания.

И, конечно, пример:

    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 (более подробно он описан ниже, в соответствующем разделе). Не фиксируйтесь на деталях, сначала нужно представить себе круг задач, для которых предназначен любой ПКП, детали здесь приведены для того, чтобы описание не стало чересчур абстрактным.


Рисунок 2. Модель реализации внешних прерываний

Последовательность действий при обработке прерывания:

  1. Устройство посылает запрос на прерывание (Interrupt Request, IRQ) на вход irX ПКП.
  2. ПКП заносит прерывание в список «ожидающие обработки».
  3. Если это прерывание не замаскировано, и в списке «обрабатываемые в данный момент» нет прерываний с большим или равным приоритетом, ПКП устанавливает выходной сигнал int, который соединён с входом процессора intr (судя по всему, опять Interrupt Request).
  4. Если прерывания разрешены (флаг IF установлен), процессор периодически проверяет значение intr. Обнаружив запрос на прерывание, процессор отвечает установкой сигнала inta (Interrupt acknowledge).
  5. ПКП переводит прерывание из списка «ожидающие обработки» в список «обрабатываемые в данный момент» и выставляет на шину D соответствующий номер прерывания.
  6. Процессор считывает с шины D номер прерывания, находит по IDT обработчик, прерывает текущий поток исполнения, вызывает обработчик прерывания.
  7. Выполняется обработчик, что именно он делает – зависит от него, но, как минимум, он должен сообщить ПКП, что прерывание обработано.
  8. (Предполагаем, что обработчик написан корректно) ПКП удаляет прерывание из списка «обрабатываемые в данный момент». До тех пор, пока это не сделано, блокируется обработка прерываний с таким же и более низким приоритетом (см. пункт 3 этого же алгоритма), то есть некорректный обработчик может нарушить всю систему обработки прерываний.
  9. Управление возвращается к прерванной программе.

В результате, ПКП отвечает за:

И всем этим значительно облегчает работу процессора.

Внутри процессора

После того, как ПКП передал процессору номер прерывания, в обработку включается собственно процессор. Тут всё как всегда, с минимальными отличиями:

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

На картинке:


Рисунок 3. Обработка внешнего прерывания

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

Программируемый контроллер прерываний Intel x86

Когда вошёл контролёр, 
Скорость перевалила за сто.
Он даже не стал проверять билеты,
Он лишь попросил снять пальто.

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

В ранних моделях x86 ПКП действительно был устроен так, как написано ниже. Но, начиная с Pentium, Intel озаботилась возможностью установки нескольких процессоров на одну материнскую плату, в связи с чем ПКП был основательно переработан. Тем не менее, из соображений обратной совместимости, с ним можно работать так же, как и раньше. Возможно, это не лучший вариант, но зато самый простой и наиболее полно описанный в литературе. Итак, перенесёмся в далёкие 80-е. Представьте: Гребенщиков молодой и худенький, Цой жив и только-только записал «45», а Intel использует для реализации ПКП микросхему i8259A.

ПКП 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 в x86

Всего восемь устройств – маловато для серьёзной работы. Разработчики i8259A это понимали, поэтому предусмотрели возможность объединения нескольких микросхем в один ПКП. Грубо говоря, для этого нужно выход int одной микросхемы i8259A подать на вход irX другой, после чего они смогут работать «почти как одна», это и называется каскадным соединением.

ПРИМЕЧАНИЕ

Для того чтобы говорить «не грубо», надо описать, что происходит с сигналами inta и D. Ну да, с ними действительно что-то происходит, в этом процессе задействовано ещё несколько сигналов, но, к счастью, это абсолютно не влияет на программную архитектуру, поэтому не должно вас интересовать. Хуже того, поскольку в современных ПКП микросхемы i8259A уже давно не используются, это неважно вообще.

Таким способом можно соединить до девяти микросхем (одна ведущая и восемь ведомых, многоуровневое каскадирование не поддерживается), получив возможность работать с 64-мя устройствами. При этом на уровне сигналов int/intr/inta/D достигается полная прозрачность, то есть, с точки зрения «аппаратного уровня» ничего не меняется. Но на «программном уровне» появляются отличия: каждый i8259A имеет свои собственные управляющие регистры, их надо отдельно инициализировать (ведущий и ведомые по-разному) и ими надо по отдельности командовать.

Но это теория. Практически, в компьютерах на основе 80286 используется две микросхемы i8259A, ведомая подключена к входу ir2 ведущей. Схема показана на Рисунке 4, на нём же отображено соответствие irX – IRQX и несколько наиболее полезных прерываний (при желании вы можете найти «классический» список внешних прерываний в любой книжке по ассемблеру, например в [Зубков 1999]).


Рисунок 4. Каскадное соединение i8259A в x86, IRQ, наиболее интересные прерывания

Что ещё можно сказать про ПКП:

И, наконец, правильная инициализация. Для ведущего:

        out 20h, 00010001b 
        out 21h, <номер обработчика для ir0> ; должен быть кратен восьми
        out 21h, 00000100b ; битовая маска, единицей отмечены входы,
                           ; к которым подключены не обычные устройства, 
                           ; а ведомые ПКП. 
        out 21h, 00000001b 

Для ведомого:

        out A0h, 00010001b 
        out A1h, <номер обработчика для ir0> ; должен быть кратен восьми
        out A1h, 2 ; номер ведомого ПКП, совпадает с номером входа 
                   ; ведущего ПКП, к которому подключен ведомый ПКП
        out A1h, 00000001b

Программные прерывания vs. Внешние

Точно так же, как и в случае с обработчиком исключения, непредсказуемый пользователь может попытаться вызвать через 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

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

Программа последовательно выполняет следующие действия:

Код написан на основе примера 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

Задания


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