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

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

Общие слова
Таблица дескрипторов прерываний
Дескриптор шлюза ловушки
[Лирическое отступление] Классификация дескрипторов
Всё вместе
Вызов обработчика прерывания
Переключение режимов: инициализация IDT
Пример
Обработчик int 0
Задания


Any time at all, any time at all, any time at all,
All you've gotta do is call and I'll be there.

John Lennon

Конечно же, вы знаете, что такое прерывание (interrupt), слышали о таблице векторов прерываний (interrupt vector table) и вообще довольно неплохо представляете, как прерывания обрабатываются в реальном режиме :)

СОВЕТ

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

Но, тем не менее, пару общих слов сказать нужно, так как тема важная и непростая.

Общие слова

Программные прерывания (software generated interrupts) вызываются инструкциями int x, где x – любое число от 0 до 255 (int 3 несколько отличается от прочих, подробнее эта тема освещена в следующей главе). Это, в общем-то, почти и не прерывания вовсе, так как ничего непонятного в них нет :) Фактически это просьба к процессору вызвать определённую подпрограмму, которая должна быть специальным образом зарегистрирована в системе, она называется обработчик прерывания (interrupt handler)

ПРИМЕЧАНИЕ

В защищённом режиме это не обязательно будет подпрограмма, есть ещё один вариант, он будет обсуждаться отдельно, в главе «Многозадачность».

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

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

Аналог таблицы векторов прерываний, существующий в защищённом режиме, называется таблицей дескрипторов прерываний (Interrupt Descriptor Table, IDT).

ПРИМЕЧАНИЕ

Это термин из официального руководства Intel. В книге Дэвида Соломона и Марка Руссиновича «Внутреннее устройство Windows 2000» (Питер, Русская Редакция, 2001) упоминается IDT, но, почему-то, там она названа таблица диспетчеризации прерываний (interrupt dispatch table), причём это не ошибка русских переводчиков, в английском оригинале используется такое же название. На мой взгляд, следует придерживаться терминологии Intel.

По формату и способу инициализации IDT практически идентична GDT. Основные отличия:

Таким образом, слегка модифицируя код из предыдущей главы, получаем пример инициализации IDT:

; Вычисляем линейный адрес начала массива дескрипторов
        mov     eax, 0
        mov     ax, ds
        shl     eax, 4                
        add     eax, offset IDT
        ; Записываем его в структуру 
        mov     dword ptr offset idtr + 2, eax

        ; Загружаем IDTR. fword ptr – указатель на шестибайтную структуру
        lidt    fword ptr idtr
        …

; Interrupt Descriptor Table
IDT     label   byte
        db; Дескриптор #0db; Дескриптор #N

idt_len equ     $ - IDT      ; размер IDT

idtr    dw      idt_len – 1  ; 16-битный размер IDT – 1
        dd      ?            ; Место для 32-х битного базового адреса IDT
ПРИМЕЧАНИЕ

Точно так же, как и в случае с lgdt, нормальные пользовательские приложения не имеют доступа к инструкции lidt. Этот факт вместе с возможностью поместить IDT в недоступную приложениям область памяти позволяет ОС контролировать обработку прерываний. Подробнее, как обычно, в главе про защиту.

Несложно заметить, что та же самая структура table_register подходит и для загрузки в IDTR.

Дескриптор шлюза ловушки

Дескриптор – не шлюза, а порядочная девушка!

Надпись маркером в лифте.

Как уже говорилось в предыдущей главе, дескриптор это структура, описывающая некую системную сущность. Очередным типом дескрипторов, с которым вам придется познакомиться, будет дескриптор шлюза ловушки (trap gate descriptor; в [Гук 1999] слово «gate» переводится как «вентиль», это стандартный перевод для схемотехники, программисты обычно говорят «шлюз»), он предназначен для IDT и имеет следующий формат:

Положение Название Краткое описание
Нулевой и первый байты Offset (part 1) Младшие два байта 32-х битного поля Offset. Поле Offset содержит смещение обработчика прерывания.
Второй и третий байты Segment Selector Селектор сегмента, содержащего обработчик прерывания.
Четвёртый байт 0 0, просто 0. Эта часть дескриптора не используется.
0-й – 3-й биты пятого байта ?? Для дескриптора шлюза ловушки значение должно быть равно 1111b. Подробности про это и следующее поле в разделе «[Лирическое отступление] Классификация дескрипторов».
4-й бит пятого байта ?? Устанавливайте в 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      10001111b   ; 10001111 – магическое число..
    db      02h         ; Offset – два старших байта
    db      01h

Это дескриптор шлюза ловушки, селектор сегмента указывает на первый дескриптор GDT, смещение – 01020304h. В виде структуры дескриптор шлюза выглядит так:

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

[Лирическое отступление] Классификация дескрипторов

Сегмента-кода-или-данных/системный

Первый признак классификации дескрипторов – значение 4-го бита пятого байта дескриптора. Полное название этого бита – флаг «descriptor type», краткое – флаг «S» (довольно странное сокращение, но везде применяется именно оно). В соответствии с этим признаком, дескрипторы делятся на два класса:

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

Системные дескрипторы имеют различный формат и единственное общее отличие их формата от формата дескрипторов сегмента кода/данных – младшие четыре бита пятого байта. Для обоих видов дескрипторов это поле называется «Type» и уточняет тип дескриптора, но при этом используются разные подходы:

Сегмента/шлюза

Второй признак классификации – характер сущности, описываемой дескриптором. Это может быть:

Дескрипторы, описывающие похожие сущности, имеют близкие форматы. Так, любой дескриптор сегмента по формату похож на дескриптор сегмента кода/данных, а любой дескриптор шлюза – на дескриптор шлюза ловушки.

Всё вместе

- Каждому прерыванию по шлюзу!
- Ура!!!
- Каждому шлюзу по обработчику!
- Ура!!!
- Каждому обработчику по прерыванию!
[вопрос с места]
- А правда, что прерываний только 255 и их не хватит всем пятистам обработчикам?
- Нет!
- Ура!!!
...
Популистское выступление продолжается. 
Докладчику пока удаётся не врать...

Объединим всё, уже известное про прерывания и добавим недостающие детали.

Вызов обработчика прерывания

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

  1. По номеру прерывания в IDT отыскивается нужный дескриптор, из него извлекается селектор сегмента и смещение начала обработчика.
  2. Проверяется корректность селектора (указывает на дескриптор в пределах GDT, это дескриптор сегмента кода, и т.п.) и смещения (попадает в сегмент).
  3. В стеке сохраняются регистр EFLAGS и дальний указатель на команду, следующую за int x, то есть регистр CS и значение регистра EIP, увеличенное на размер команды int x. На каждый из регистров в стеке выделяется по 4 байта, рядом с CS находятся два «лишних» байта.
  4. CS и EIP загружаются новыми значениями.
  5. Начинается выполнение обработчика. Дескриптор шлюза ловушки не предназначен для обработки внешних (аппаратных) прерываний, поэтому аппаратные прерывания не маскируются автоматически при входе в обработчик.
  6. После своего завершения обработчик возвращает управление командой iretd. В отличии от iret, она предназначена для случая, когда в стеке сохранены 32-х разрядные EIP, CS и EFLAGS, а не 16-ти разрядные IP, CS и FLAGS (подробнее про 16-ти и 32-х разрядность см. в приложении).
  7. Возврат к исходной программе. В регистры EFLAGS, CS и EIP загружается значение из стека, при этом происходит обращение к GDT и проверка корректности устанавливаемого селектора и смещения.
ПРЕДУПРЕЖДЕНИЕ

Ещё раз обращаю ваше внимание: независимо от префиксов команд, разрядности сегмента стека, разрядности целевого и исходного сегмента кода и т.п. (всё это будет обсуждаться в приложении в разделе про разрядность), под EFLAGS, CS, EIP в стеке выделяется 12 байт, по 4 байта на каждый регистр.

Это связано с тем, что тип 1111b соответствует 32-х разрядному дескриптору шлюза ловушки. Существует полностью аналогичный 16-и разрядный, но он в курсе не рассматривается.

Упрощённая версия алгоритма в виде комикса:


Рисунок 2. Немного упрощенный алгоритм вызова обработчика прерывания.

Ключевые признаки, отделяющие «наш» случай от «не нашего»:

Переключение режимов: инициализация IDT

Добавим к алгоритмам переключения ещё несколько шагов. Из реального режима в защищённый:

  1. Запретить маскируемые и немаскируемые прерывания.
  2. Инициализировать GDT и загрузить её адрес в GDTR.
  3. Инициализировать IDT и загрузить её адрес в IDTR.
  4. Установить флаг PE (младший бит регистра CR0).
  5. Выполнить дальний переход (jmp или call). Это нужно для перезагрузки регистра CS.
  6. Перезагрузить все сегментные регистры.
ПРЕДУПРЕЖДЕНИЕ

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

ПРИМЕЧАНИЕ

Поскольку обрабатывать аппаратные прерывания мы пока не планируем, разрешать прерывания будет преждевременно.

Обратное переключение:

  1. Сделать текущим сегментом кода какой-либо доступный для чтения сегмент с пределом FFFFh байт.
  2. Загрузить во все сегментные регистры селекторы дескрипторов доступных для записи сегментов данных с пределом FFFFh.
  3. Сбросить флаг PE
  4. Выполнить дальний переход (jmp или call).
  5. Перезагрузить сегментные регистры.
  6. Восстановить в IDTR значение для реального режима.
  7. Разрешить прерывания.

Сохранение значения IDTR

Для того, чтобы при переключении в реальный режим восстановить значение IDTR, при старте программы его необходимо сохранить. Для этого предназначена команда sidt.

         sidt	pointer_to_idtr

Пример использования:

; Сохраняем IDTR
         sidt    fword ptr old_idtr
         …

old_idtr table_register <>

Пример

Обработчик int 0

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

ПРИМЕЧАНИЕ

Программа написана на основе примера cs64kb.asm из предыдущей главы.

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

        .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     cs_dsc.base_low, ax
        shr     eax, 16
        mov     cs_dsc.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 Кб, базовый адрес равен
        ; адресу сегмента кода до переключения в защищённый режим.

        int 0 ; вызываем прерывание

        call clear_PE

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

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

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

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

        ret

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

; Обработчик прерывания int 0, меняет символы и их атрибуты по всему экрану
; Предпологает, что база es - начало видеобуфера
int0_handler:

        ; В данный момент, в стеке:
        ; esp + 0 - EIP
        ; esp + 4 – CS и два байта мусора
        ; esp + 8 - 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
        iretd                 ; 32-х разрядный возврат из прерывания

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

; Глобальная таблица дескрипторов
GDT     label byte
        ; Нулевой дескриптор
        segment_descriptor <>
        ; Дескриптор сегмента кода, размер 64 Kb
cs_dsc  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
        ; Дескриптор шлюза ловушки. 
        ; Обработчик прерывания находится в сегменте, соответствующем первому
        ; дескриптору GDT. Поскольку базовый адрес сегмента такой же, как
        ; в реальном режиме, смещение обработчика тоже совпадает.
        gate_descriptor <int0_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

Задания


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