Обработка прерываний и исключений
До сих пор все наши программы работали в защищенном режиме с полностью отключенными прерываниями — ими нельзя было управлять с клавиатуры, они не могли работать с дисками и вообще не делали ничего, кроме чтения или записи в те или иные области памяти. Разумеется, ни одна программа не может сделать ничего серьезного в таком режиме — нам рано или поздно придется обрабатывать прерывания.
В реальном режиме адрес обработчика прерывания считывался процессором из таблицы, находящейся по адресу 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 в нашем примере не был корректным и многие исключения привели бы к немедленному зависанию системы.
Рассмотрим исключения в том виде, как они определены для защищенного режима.
Формат кода ошибки:
биты 15 – 3: биты 15 – 3 селектора, вызвавшего исключение
бит 2: TI — установлен, если причина исключения — дескриптор, находящийся в LDT, и сброшен, если в GDT
бит 1: IDT — установлен, если причина исключения — дескриптор, находящийся в IDT
бит 0: ЕХТ — установлен, если причина исключения — аппаратное прерывание
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).
Содержание раздела