| [Назад] [Далее] | ![]() |
До сих пор все наши программы работали в защищенном режиме с полностью отключенными прерываниями — ими нельзя было управлять с клавиатуры, они не могли работать с дисками и вообще не делали ничего, кроме чтения или записи в те или иные области памяти. Разумеется, ни одна программа не может сделать ничего серьезного в таком режиме — нам рано или поздно придется обрабатывать прерывания.
В реальном режиме адрес обработчика прерывания считывался процессором из таблицы, находящейся по адресу 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).