[Назад] [Далее]

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

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

В реальном режиме адрес обработчика прерывания считывался процессором из таблицы, находящейся по адресу 0 в памяти. В защищенном режиме эта таблица, называемая IDT — таблицей дескрипторов прерываний, может находиться где угодно. Достаточно того, чтобы ее адрес и размер были загружены в регистр IDTR. Содержимое этой таблицы — не просто адреса обработчиков, как это было в реальном режиме, а дескрипторы трех типов: шлюз прерывания, шлюз ловушки и шлюз задачи (форматы этих дескрипторов рассматривались в предыдущей главе).

Шлюзы прерываний и ловушек указывают точку входа обработчика, а также его разрядность и уровень привилегий. При передаче управления обработчику процессор помещает в стек флаги и адрес возврата, так же как и в реальном режиме, но для некоторых исключений после этого в стек помещается дополнительный код ошибки, так что не все обработчики можно завершать простой командой IRETD (или IRET для 16-битного варианта). Единственное различие между шлюзом прерывания и ловушки состоит в том, что при передаче управления через шлюз прерывания автоматически запрещаются дальнейшие прерывания, пока обработчик не выполнит IRETD. Этот механизм считается предпочтительным для обработчиков аппаратных прерываний, в то время как шлюз ловушки, который не запрещает прерывания на время исполнения обработчика, предпочтителен для обработки программных прерываний (которые фактически и являются исключениями типа ловушки). Кроме того, в защищенном режиме при вызове обработчика прерывания сбрасывается флаг трассировки ТF.

Сначала рассмотрим пример программы, обрабатывающей только аппаратное прерывание клавиатуры при помощи шлюза прерываний. Для этого надо составить IDT, загрузить ее адрес командой LIDT и не забыть загрузить то, что содержится в регистре IDTR в реальном режиме, — адрес 0 и размер 4 * 256, соответствующие таблице векторов прерываний реального режима.

; pm2.asm
; Программа, демонстрирующая обработку аппаратных прерываний в защищенном
; режиме, переключается в 32-битный защищенный режим и позволяет набирать
; текст при помощи клавиш от 1 до +. Нажатие Backspace стирает предыдущий
; символ, нажатие Esc - выход из программы.
;
; Компиляция TASM:
;   tasm /m /D_TASM_ pm2.asm
;   (или, для версий 3.x, достаточно tasm /m pm2.asm)
;   tlink /x /3 pm2.obj
; Компиляция WASM:
;   wasm /D pm2.asm
;   wlink file pm2.obj form DOS
;
; Варианты того, как разные ассемблеры записывают смещение из 32-битного
; сегмента в 16-битную переменную:
ifdef _TASM_
so      equ        small offset        ; TASM 4.x
else
so      equ        offset              ; WASM
endif
; для MASM, по-видимому, придется добавлять лишний код, который преобразует
; смещения, используемые в IDT

        .386р
RM_seg segment para public "CODE" use16
        assume cs:RM_seg,ds:PM_seg,ss:stack_seg
start:
; очистить экран
        mov        ax,3
        int        10h
; подготовить сегментные регистры
        push       PM_seg
        pop        ds
; проверить, не находимся ли мы уже в РМ
        mov        еах,cr0
        test       al,1
        jz         no_V86
; сообщить и выйти
        mov        dx,so v86_msg
err_exit:
        mov        ah,9
        int        21h
        mov        ah,4Ch
        int        21h
; может быть, это Windows 95 делает вид, что РЕ = О?
no_V86:
        mov        ax,1600h
        int        2Fh
        test       al,al
        jz         no_windows
; сообщить и выйти
        mov        dx,so win_msg
        jmp        short err_exit
; итак, мы точно находимся в реальном режиме
no_windows:
; вычислить базы для всех используемых дескрипторов сегментов
        xor        еах,еах
        mov        ax,RM_seg
        shl        eax,4
        mov        word ptr GDT_16bitCS+2,ax ; базой 16bitCS будет RM_seg
        shr        eax,16
        mov        byte ptr GDT_16bitCS+4,al
        mov        ax,PM_seg
        shl        eax,4
        mov        word ptr GDT_32bitCS+2,ax ; базой всех 32bit* будет
        mov        word ptr GDT_32bitSS+2,ax ; PM_seg
        mov        word ptr GDT_32bitDS+2,ax
        shr        eax,16
        mov        byte ptr GDT_32bitCS+4,al
        mov        byte ptr GDT_32bitSS+4,al
        mov        byte ptr GDT_32bitDS+4,al
; вычислить линейный адрес GDT
        xor        еах,еах
        mov        ax,PM_seg
        shl        eax,4
        push       eax
        add        eax,offset GDT
        mov        dword ptr gdtr+2,eax
; загрузить GDT
        lgdt       fword ptr gdtr
; вычислить линейный адрес IDT
        pop        eax
        add        eax,offset IDT
        mov        dword ptr idtr+2,eax
; загрузить IDT
        lidt       fword ptr idtr
; если мы собираемся работать с 32-битной памятью, стоит открыть А20
        in         al,92h
        or         al,2
        out        92h,al
; отключить прерывания,
        cli
; включая NMI,
        in         al,70h
        or         al,80h
        out        70h,al
; перейти в РМ
        mov        еах,cr0
        or         al,1
        mov        cr0,eax
; загрузить SEL_32bitCS в CS
        db         66h
        db         0EAh
        dd         offset PM_entry
        dw         SEL_32bitCS
RM_return:
; перейти в RM
        mov        eax,cr0
        and        al,0FEh
        mov        cr0,eax
; сбросить очередь и загрузить CS реальным числом
        db         0EAh
        dw         $+4
        dw         RM_seg
; установить регистры для работы в реальном режиме
        mov        ax,PM_seg
        mov        ds,ax
        mov        es,ax
        mov        ax,stack_seg
        mov        bx,stack_l
        mov        ss,ax
        mov        sp,bx
; загрузить IDTR для реального режима
        mov        ax,PM_seg
        mov        ds,ax
        lidt       fword ptr idtr_real
; разрешить NMI
        in         al,70h
        and        al,07FH
        out        70h,al
; разрешить прерывания
        sti
; и выйти
        mov        ah,4Ch
        int        21h
RM_seg  ends

; 32-битный сегмент
PM_seg segment para public "CODE" use32
        assume cs:PM_seg
; таблицы GDI и IDT должны быть выравнены, так что будем их размещать
; в начале сегмента
GDT     label  byte
               db    8 dup(0)
; 32-битный 4-гигабайтный сегмент с базой = 0
GDT_flatDS     db    0FFh,0FFh,0,0,0,10010010b,11001111b,0
; 16-битный 64-килобайтный сегмент кода с базой RM_seg
GDT_16bitCS    db    0FFh,0FFh,0,0,0,10011010b,0,0
; 32-битный 4-гигабайтный сегмент кода с базой PM_seg
GDT_32bitCS    db    0FFh,0FFh,0,0,0,10011010b,11001111b,0
; 32-битный 4-гигабайтный сегмент данных с базой PM_seg
GDT_32bitDS    db    0FFh,0FFh,0,0,0,10010010b,11001111b,0
; 32-битный 4-гигабайтный сегмент данных с базой stack_seg
GDT_32bitSS    db    0FFh,0FFh,0,0,0,10010010b,11001111b,0
gdt_size = $ - GDT
gdtr           dw    gdt_size-1        ; лимит GDT
               dd    ?                 ; линейный адрес GDT
; имена для селекторов
SEL_flatDS     equ   001000b
SEL_16bitCS    equ   010000b
SEL_32bitCS    equ   011000b
SEL_32bitDS    equ   100000b
SEL_32bitSS    equ   101000b

; таблица дескрипторов прерываний IDT
IDT     label  byte
; все эти дескрипторы имеют тип 0Eh - 32-битный шлюз прерывания
; INT 00 - 07
               dw    8 dup(so int_handler,SEL_32bitCS,8E00h,0)
; INT 08 (irq0)
               dw    so irq0_7_handler,SEL_32bitCS,8E00h,0
; INT 09 (irq1)
               dw    so irq1_handler,SEL_32bitCS,8E00h,0
; INT 0Ah - 0Fh (IRQ2 - IRQ8)
               dw    6 dup(so irq0_7_handler,SEL_32bitCS,8E00h,0)
; INT 10h - 6Fh
               dw    97 dup(so int_handler,SEL_32bitCS,8E00h,0)
; INT 70h - 78h (IRQ8 - IRQ15)
               dw    8 dup(so irq8_15_handler,SEL_32bitCS,8E00h,0)
; INT 79h - FFh
               dw    135 dup(so int_handler,SEL_32bitCS,8E00h,0)
idt_size = $ - IDT                     ; размер IDT
idtr           dw    idt_size-1        ; лимит IDT
               dd    ?                 ; линейный адрес начала IDT
; содержимое регистра IDTR в реальном режиме
idtr_real      dw    3FFh,0,0

; сообщения об ошибках при старте
v86_msg db     "Процессор в режиме V86 - нельзя переключиться в РМ$"
win_msg db     "Программа запущена под Windows - нельзя перейти в кольцо 0$"

; таблица для перевода 0Е скан-кодов в ASCII
scan2ascii     db    0,1Bh,'1','2','3','4','5','6','7','8','9','0','-','=',8
screen_addr    dd    0                 ; текущая позиция на экране

; точка входа в 32-битный защищенный режим
PM_entry:
; установить 32-битный стек и другие регистры
        mov        ax,SEL_flatDS
        mov        ds,ax
        mov        es,ax
        mov        ax,SEL_32bitSS
        mov        ebx,stack_l
        mov        ss,ax
        mov        esp,ebx
; разрешить прерывания
        sti
; и войти в вечный цикл
        jmp        short $

; обработчик обычного прерывания
int_handler:
        iretd
; обработчик аппаратного прерывания IRQ0 - IRQ7
irq0_7_handler:
        push       eax
        mov        al,20h
        out        20h,al
        pop        eax
        iretd
; обработчик аппаратного прерывания IRQ8 - IRQ15
irq8_15_handler:
        push       eax
        mov        al,20h
        out        0A1h,al
        pop        eax
        iretd
; обработчик IRQ1 - прерывания от клавиатуры
irq1_handler:
        push       eax           ; это аппаратное прерывание - сохранить регистры
        push       ebx
        push       es
        push       ds
        in         al,60h           ; прочитать скан-код нажатой клавиши,
        cmp        al,0Eh           ; если он больше, чем максимальный
        ja         skip_translate   ; обслуживаемый нами, - не обрабатывать,
        cmp        al,1             ; если это Esc,
        je         esc_pressed      ; выйти в реальный режим,
        mov        bx,SEL_32bitDS   ; иначе:
        mov        ds,bx            ; DS:EBX - таблица для перевода скан-кода
        mov        ebx,offset scan2ascii ; в ASCII
        xlatb                            ; преобразовать
        mov        bx,SEL_flatDS
        mov        es,bx            ; ES:EBX - адрес текущей
        mov        ebx,screen_addr  ; позиции на экране,
        cmp        al,8             ; если не была нажата Backspace,
        je         bs_pressed
        mov        es:[ebx+0B8000h],al     ; послать символ на экран,
        add        dword ptr screen_addr,2 ; увеличить адрес позиции на 2,
        jmp        short skip_translate
bs_pressed:                                ; иначе:
        mov        al,' '                  ; нарисовать пробел
        sub        ebx,2                   ; в позиции предыдущего символа
        mov        es:[ebx+0B8000h],al
        mov        screen_addr,ebx         ; и сохранить адрес предыдущего символа
skip_translate:                            ; как текущий
; разрешить работу клавиатуры
        in         al,61h
        or         al,80h
        out        61h,al
; послать EOI контроллеру прерываний
        mov        al,20h
        out        20h,al
; восстановить регистры и выйти
        pop        ds
        pop        es
        pop        ebx
        pop        eax
        iretd
; сюда передается управление из обработчика IRQ1, если нажата Esc
esc_pressed:
; разрешить работу клавиатуры, послать EOI и восстановить регистры
        in         al,61h
        or         al,80h
        out        61h,al
        mov        al,20h
        out        20h,al
        pop        ds
        pop        es
        pop        ebx
        pop        eax
; вернуться в реальный режим
        cli
        db         0EAh
        dd         offset RM_return
        dw         SEL_16bitCS
PM_seg  ends

; Сегмент стека. Используется как 16-битный в 16-битной части программы и как
; 32-битный (через селектор SEL_32bitSS) в 32-битной части
stack_seg segment para stack "STACK"
stack_start        db       100h dup(?)
stack_l = $ - stack_start       ; длина стека для инициализации ESP
stack_seg ends
        end        start

В этом примере обрабатываются только 13 скан-кодов клавиш для сокращения размеров программы — полную информацию для преобразования скан-кодов в ASCII можно получить, воспользовавшись таблицами, приведенными в приложении 1 (рис. 18, табл. 25 и 26). Кроме того, в этом примере курсор все время остается в нижнем левом углу экрана — для его перемещения можно воспользоваться регистрами 0Eh и 0Fh контроллера CRT (см. главу 5.10.4).

Как уже упоминалось в главе 5.8, кроме прерываний от внешних устройств процессор может вызывать исключения при различных внутренних ситуациях, механизм обслуживания которых похож на механизм обслуживания аппаратных прерываний. Номера прерываний, на которые отображаются аппаратные прерывания, вызываемые первым контроллером по умолчанию, совпадают с номерами некоторых исключений. Конечно, можно из обработчика опрашивать контроллер прерываний, чтобы определить, выполняется ли обработка аппаратного прерывания или это исключение, но Intel рекомендует перенастраивать контроллер прерываний (мы это делали в главе 5.10.10) так, чтобы никакие аппаратные прерывания не попадали на область от 0 до 1Fh. В нашем примере исключения не обрабатывались, но, если программа планирует запускать другие программы или задачи, без обработки исключений обойтись нельзя.

Часть исключений (исключения типа ошибки) передает в качестве адреса возврата команду, вызвавшую исключение, а часть — адрес следующей команды. Кроме того, некоторые исключения помещают в стек код ошибки, который нужно считать, прежде чем выполнять IRETD. Поэтому пустой обработчик из одной команды IRETD в нашем примере не был корректным и многие исключения привели бы к немедленному зависанию системы.

Рассмотрим исключения в том виде, как они определены для защищенного режима.

INT 00 — ошибка #DE «Деление на ноль»

Вызывается командами DIV или IDIV, если делитель — ноль или если происходит переполнение.


INT 01 — исключение #DB «Отладочное прерывание»

Вызывается как ловушка при пошаговой трассировке (флаг TF = 1), при переключении на задачу с установленным отладочным флагом и при срабатывании точки останова во время доступа к данным, определенной в отладочных регистрах.

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


INT 02 — прерывание NMI

Немаскируемое прерывание.


INT 03 — ловушка #ВР «Точка останова»

Вызывается однобайтной командой INT3.


INT 04 — ловушка #OF «Переполнение»

Вызывается командой INT0, если флаг OF = 1.


INT 05 — ошибка #ВС «Переполнение при BOUND»

Вызывается командой BOUND при выходе операнда за допустимые границы.


INT 06 — ошибка #UD «Недопустимая операция»

Вызывается, когда процессор пытается исполнить недопустимую команду или команду с недопустимыми операндами.


INT 07 — ошибка #NM «Сопроцессор отсутствует»

Вызывается любой командой FPU, кроме WAIT, если бит ЕМ регистра CR0 установлен в 1, и командой WAIT, если МР и TS установлены в 1.


INT 08 — ошибка #DF «Двойная ошибка»

Вызывается, если одновременно произошли два исключения, которые не могут быть обслужены последовательно. К таким исключениям относятся #DE, #TS, #NP, #SS, #GP и #РЕ

Обработчик этого исключения получает код ошибки, который всегда равен нулю.

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


INT 09 — зарезервировано

Эта ошибка вызывалась сопроцессором 80387, если происходило исключение #PF или #GP при передаче операнда команды FPU.


INT 0Ah — ошибка #TS «Ошибочный TSS»

Вызывается при попытке переключения на задачу с ошибочным TSS.

Обработчик этого исключения должен вызываться через шлюз задачи.

Обработчик этого исключения получает код ошибки.

Бит ЕХТ кода ошибки установлен, если переключение пыталось выполнить аппаратное прерывание, использующее шлюз задачи, индекс ошибки равен селектору TSS, если TSS меньше 67h байт, селектору LDT, если LDT отсутствует или ошибочен, селектору сегмента стека, кода или данных, если ими нельзя пользоваться (из-за нарушений защиты или ошибок в селекторе).


INT 0Bh — ошибка #NP «Сегмент недоступен»

Вызывается при попытке загрузить в регистр CS, DS, ES, FS или GS селектор сегмента, в дескрипторе которого сброшен бит присутствия сегмента (загрузка в SS вызывает исключение #SS), а также при попытке использования шлюза, помеченного как отсутствующий, или при загрузке такой таблицы локальных дескрипторов командой LLDT (загрузка при переключении задач приводит к исключению #TS).

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

Обработчик этого исключения получает код ошибки.

Бит ЕХТ кода ошибки устанавливается, если причина ошибки — внешнее прерывание, бит IDT устанавливается, если причина ошибки — шлюз из IDT, помеченный как отсутствующий. Индекс ошибки равен селектору отсутствующего сегмента.


INT 0Ch — ошибка #SS «Ошибка стека»

Это исключение вызывается при попытке выхода за пределы сегмента стека при выполнении любой команды, работающей со стеком, — как явно (POP, PUSH, ENTER, LEAVE), так и неявно (MOV AX,[BP + 6]), а также при попытке загрузить в регистр SS селектор сегмента, помеченного как отсутствующий (не только при выполнении команд MOV, POP и LSS, но и при переключении задач, вызове и возврате из процедуры на другом уровне привилегий).

Обработчик этого исключения получает код ошибки.

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


INT 0Dh — исключение #GP «Общая ошибка защиты»

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

Обработчик этого исключения получает код ошибки.

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


INT 0Eh — ошибка #PF «Ошибка страничной адресации»

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

Обработчик этого исключения получает код ошибки.

Код ошибки использует формат, отличный от кода ошибки для других исключений:

бит 0: 1, если причина ошибки — нарушение привилегий;
0, если было обращение к отсутствующей странице
бит 1: 1, если выполнялась операция записи,
0, если чтения
бит 2: 1, если операция выполнялась из CPL = 3,
0, если CPL < 3
бит 3: 0, если ошибку вызвала попытка установить зарезервированный бит в каталоге страниц
  остальные биты зарезервированы

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

Исключение #PF — основное исключение для создания виртуальной памяти с использованием механизма страничной адресации.


INT 0Fh — зарезервировано


INT 10h — ошибка #MF «Ошибка сопроцессора»

Вызывается, только если бит NE в регистре CR0 установлен в 1 при выполнении любой команды FPU, кроме управляющих команд и WAIT/FWAIT, если в FPU произошло одно из исключений FPU (см. главу 2.4.3).


INT 11h — ошибка #АС «Ошибка выравнивания»

Вызывается, только если бит AM в регистре CR0 и флаг АС из EFLAGS установлены в 1, если CPL = 3 и произошло невыравненное обращение к памяти. (Выравнивание должно быть по границе слова при обращении к слову, к границе двойного слова, к двойному слову и т.д.)

Обработчик этого исключения получает код ошибки, равный нулю.


INT 12h — останов #МС «Машинно-зависимая ошибка»

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


INT 13h – 1Fh — зарезервировано Intel для будущих исключений


INT 20h – FFh — выделены для использования программами


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


п»ї
"target=_blank><\/a>") //-->