После того как в предыдущей заметке мы написали загрузчик, мы не ограничены в размере кода нашей программы. Вернемся теперь снова к логике повествования книги Руслана Аблязова — Программирование на ассемблере на платформе х86-64 — 2011. В сегодняшней заметке реализуем обработку прерываний в защищенном режиме, которая рассмотрена в разделе книги 2.2. Прерывания в защищенном режиме.
Что такое прерывание
Прерывания — это механизм, при помощи которого программа (вероятнее всего — операционная система), которую выполняет процессор, может реагировать на различные события. События бывают двух видов:
- Аппаратные прерывания, при помощи которых периферийные устройства сигнализируют о чем-то процессору.
- Исключения — ошибочные ситуации вроде деления на 0 или нарушения прав доступа к памяти.
Как только случается такое событие, процессор прерывает выполнение основной программы и переходит к обработке события, т. е. реагирует на это событие, выполняя определенную процедуру, которая называется обработчиком прерывания. Обработав прерывание, процессор возвращается к выполнению основной программы с того места, где она была прервана.
Всего может существовать до 256 различных прерываний. Они пронумерованы от 0 до 255 и для каждого, вообще говоря, должен существовать свой обработчик. Полный список прерываний смотрите в разделе 6.3.1 External Interrupts руководства Intel 64 and IA-32 Architectures Software Developer’s Manual — Volume 3. System Programming Guide.
Программные прерывания
Заметим, что обработчик любого прерывания может быть вызван программно при помощи команды int n (где n — номер прерывания), что аналогично вызову функции командой call. Однако некоторые прерывания вызывать программно не стоит, так как это непременно приведет к аварии. К таким прерываниям относятся те исключения, при возникновении которых процессор помещает в стек т. н. код ошибки — дополнительная информация о произошедшем исключении. Команда int не помещает в стек код ошибки, но «ничего не подозревающий» обработчик исключения ожидает, что код ошибки будет в стеке — поэтому и произойдет авария.
Команду int n могут выполнять как программы, работающие в режиме ядра (наиболее привилегированном), так и программы, работающие в пользовательском режиме (наименее привилегированном). Для пользовательских программ вызов программного прерывания — это способ воспользоваться услугами операционной системы — т. н. системными вызовами. Подробнее о системных вызовах читайте тут: Wikipedia — System call.
При программном вызове обработчика прерывания процессор проверяет, обладает ли вызывающий код достаточным уровнем привилегий для осуществления такого вызова. Каков этот достаточный уровень привилегий, определяет поле DPL дескриптора шлюза прерывания (об этом см. ниже). Если уровень привилегий у вызывающего кода недостаточен, то процессор генерирует исключение General Protection Fault. К примеру, для того, чтобы пользовательский код мог вызвать обработчик прерывания программно, дескриптор шлюза прерывания должен иметь поле DPL=3.
Среди программных прерываний команда int 3 занимает особое место и имеет короткий опкод 0xСС. Эта команда используется отладчиками для реализации точек останова. Подробнее об отладке читайте в статье Хабрахабр — Про брейкпойнты.
Какие прерывания мы будем обрабатывать
Самый наглядный пример обработки аппаратных прерываний — это обработка прерываний от клавиатуры. Когда пользователь нажимает или отпускает клавишу на клавиатуре, контроллер клавиатуры генерирует запрос на прерывание. Благодаря обработке этого прерывания пользователь сможет хотя бы на минимальном уровне взаимодействовать с нашей программой. Кроме того, в сегодняшней программе мы будем обрабатывать еще одно аппаратное прерывание — от системного таймера. Остальные аппаратные прерывания (IRQ — прерывания от периферийных устройств) тоже будут иметь обработчики, только эти обработчики не будут ничего делать, а будут только возвращать управление прерванной программе. Мы также будем обрабатывать одно исключение — исключение общей защиты (General Protection Fault). Остальные исключения не будут иметь обработчиков, поэтому при возникновении таких исключений будет происходить Triple Fault (авария и перезагрузка процессора).
Откуда берутся обработчики прерываний
Обработчики прерываний — это практически обычные функции. Они находятся в оперативной памяти. Как процессор находит обработчик для каждого прерывания?
В реальном режиме работы процессора в самом начале оперативной памяти находится т. н. таблица векторов прерываний, которая представляет собой массив из 256-ти 32-разрядных чисел. Каждое число — это пара 16-разрядных чисел сегмент:смещение, которая и является адресом обработчика прерывания в памяти. Номер прерывания является номером записи в таблице векторов прерываний.
В защищенном режиме все сложнее, но не намного. В оперативной памяти хранится таблица векторов прерываний — Interrupt Descriptor Table (IDT), которая представляет собой массив 64-разрядных чисел — дескрипторов шлюзов. Дескриптор шлюза содержит селектор сегмента кода, в котором находится обработчик прерывания и адрес (смещение) обработчика в этом сегменте (т. е. ту же информацию, что и вектор прерывания в реальном режиме). Перед тем как разрешить прерывания, необходимо адрес и размер таблицы IDT загрузить в регистр IDTR при помощи команды lidt (аналогично тому, как мы загружали адрес и лимит таблицы GDT, когда переходили в защищенный режим).
Типы шлюзов в таблице векторов прерываний
В таблице IDT могут находиться шлюзы трех разных типов (шлюз прерывания, шлюз ловушки и шлюз задачи), но я в этой заметке буду использовать только один из них — шлюз прерывания. Шлюз ловушки ничем не отличается от шлюза прерывания, кроме одного: если в таблице IDT находится шлюз прерывания, то при вызове обработчика этого прерывания процессор сбрасывает флаг разрешения прерываний IF в регистре EFLAGS (запрещая маскируемые прерывания); если же в таблице находится шлюз ловушки, то процессор не сбрасывает флаг IF. А вот что такое шлюз задачи, я вообще пока не знаю.
Формат шлюза прерывания
Формат дескрипторов шлюзов прерываний, ловушек и задач смотрите в разделе 6.11 IDT DESCRIPTORS руководства Intel 64 and IA-32 Architectures Software Developer’s Manual — Volume 3. System Programming Guide. Здесь я хочу привести только формат шлюза прерывания.
31 | — | 16 | 15 | 14 | 13 | 12 | — | 8 | 7 | — | 5 | 4 | — | 0 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Offset[31:16] | P | DPL | 0 D 1 1 0 | 0 0 0 | 4 | ||||||||||
Segment Selector | Offset[15:0] | 0 |
Segment Selector — это селектор сегмента кода, в котором находится обработчик прерывания. Offset — это смещение обработчика относительно базового адреса сегмента кода. О поле DPL я уже говорил — оно играет роль только при программном вызове обработчика прерывания командой int n. Бит D определяет т. н. разрядность шлюза прерывания. О роли этого бита читайте раздел 21.1 DEFINING 16-BIT AND 32-BIT PROGRAM MODULES руководства Intel 64 and IA-32 Architectures Software Developer’s Manual — Volume 3. System Programming Guide. Я буду использовать только 32-разрядные сегменты и шлюзы, поэтому у меня бит D всегда будет равен 1.
Исходный код программы
Поскольку программа становится все больше и больше, мне приходится усложнять ее структуру — разбивать исходный код на отдельные файлы. Ниже схематично показана структура программы boot_sector.asm. Эта часть программы вряд ли подвергнется изменениям в дальнейшем. Всё, что может измениться в дальнейшем, я поместил в файл protected_mode.inc, который включается директивой include в самый конец программы.
; ====== РЕАЛЬНЫЙ РЕЖИМ ======
include 'bios_services.inc' ; функции-обертки вокруг сервисов BIOS
; загрузка секторов с дискеты (bootloader)
; переход в защищенный режим
; ===== ЗАЩИЩЕННЫЙ РЕЖИМ =====
include 'descriptor_macros.inc' ; макросы для определения дескрипторов сегментов и шлюзов
; глобальная таблица дескрипторов (Global Descriptor Table)
include 'protected_mode.inc' ; файл, который содержит остальной код защищенного режима
Функции BIOS, загрузку секторов с диска, переход в защищенный режим и глобальную таблицу дескрипторов мы уже рассмотрели в предыдущих заметках:
В нынешнем файле boot_sector.asm используется как загрузчик, так и переход в защищенный режим, так что этот файл по сути является комбинацией одноименных файлов из двух упомянутых выше заметок. Плюс я добавил в него несколько мелких новшеств:
- Количество секторов для загрузки при помощи функции BIOS_LoadSectorsSmart вычисляется компилятором путем деления размера программы на 512 (512 байт — это размер одного сектора). Размер программы вычисляется как разность адресов, на которые ссылаются метки start и finish, расположенные соответственно в начале и в конце файла boot_sector.asm.
- Макрос для определения дескриптора сегмента SEGMENT_DESCRIPTOR, который упоминался в заметке про переход в защищенный режим, я поместил в файл descriptor_macros.inc. В этом же файле находятся макросы для определения дескрипторов шлюзов прерываний и ловушек.
- В глобальную таблицу дескрипторов я добавил еще один дескриптор, описывающий сегмент видеопамяти в текстовом режиме. Этот сегмент удобно использовать при выводе текстовых сообщений на экран. Селектор этого сегмента мы загружаем в сегментный регистр ES, поскольку именно этот сегментный регистр используется в Intel’овских командах, работающих со строками (stos, stosw и пр.). О строковых командах Intel читайте в Intel 64 and IA-32 Architectures Software Developer’s Manual — Volume 2. Instruction Set Reference, A-Z.
Итак, вот полный код файла boot_sector.asm:
; ============================================ REAL MODE =======================================================
use16 ; generate 16-bit code
org 7C00h ; the code starts at 0x7C00 memory address
start:
jmp far dword 0x0000:entr ; makes CS=0, IP=0x7c00
include 'bios_services.inc'
entr:
xor ax, ax ; ax = 0
mov ds, ax ; setup data segment ds=ax=0
cli ; when we set up stack we need disable interrupts because stack is involved in interrupts handling
mov ss, ax ; setup stack segment ss=ax=0
mov sp, 0x7C00 ; stack will grow starting from 0x7C00 memory address
sti ; enable interrupts
call BIOS_ClearScreen
jmp bootloader
; -------------------------------------------- BOOTLOADER ------------------------------------------------------
disk_id dw 0x0000 ; disk identifier (0...3 - floppy disks; greater or equal to 0x80 - other disk types)
bootloader_message db 'Starting bootloader...', 0x0D, 0x0A, 0
bootloader:
mov byte [disk_id], dl ; save disk identifier (it appears in dl after the start of PC)
; Print message
push bootloader_message
call BIOS_PrintString
; Reset disk subsystem
push [disk_id]
call BIOS_ResetDiskSubsystem
push [disk_id] ; push disk identifier
push dword 1 ; push LBA of the 1st sector to load
push (finish-start-1)/512 ; push number of sectors to load
push second_sector ; push memory address at which sectors are to be loaded
call BIOS_LoadSectorsSmart
jmp transition_to_pm
; -------------------------------------------- TRANSITION TO PROTECTED MODE ------------------------------------
transition_to_pm:
; Open gate A20 through the System Control Port A
in al, 0x92 ; read the content of port 0x92 (System Control Port A) into al
or al, 0x02 ; set 1st bit in al to 1
out 0x92, al ; write al into port 0x92
; Disable maskable interrupts
cli ; clear flag IF in EFLAGS register
; Disable non-maskable interrupts
in al, 0x70
or al, 0x80
out 0x70, al
lgdt [gdtr] ; load the address and size of Global Descriptor Table (GDT) into GDTR register
; Switch to protected mode
mov eax, cr0 ; read the content of register cr0 (Machine Status Word - MSW) into eax
or al, 0x01 ; set 0th bit to 1 (0th bit of cr0 is called PE - Protection Enable)
mov cr0, eax ; write eax to cr0
; Load protected mode entry point into CS:EIP
jmp far dword 0000000000001000b:pm_entry ; 0000000000001000b is a segment selector which is loaded into CS register
; -------------------------------------------- BOOT SECTOR SIGNATURE -------------------------------------------
end_of_boot_sector:
; The size of a disk sector is 512 bytes. Boot sector signature occupies the two last bytes.
; The gap between the end of the source code and the boot sector signature is filled with zeroes.
times 510-end_of_boot_sector+start db 0
db 55h, 0AAh ; boot sector signature
second_sector:
; ============================================ PROTECTED MODE ==================================================
use32 ; generate 32-bit code
; -------------------------------------------- GLOBAL DESCRIPTOR TABLE -----------------------------------------
include 'descriptor_macros.inc'
align 8 ; we align global descriptor table to the 8-bytes boundary (for the sake of processor's performance)
gdt:
NULL_SEG_DESCRIPTOR db 8 dup(0)
; Base Limit DPL Type
SEGMENT_DESCRIPTOR 0x00000000, 0xFFFFF, 00b, CODE_EXECUTE_READ ; code segment descriptor
SEGMENT_DESCRIPTOR 0x00000000, 0xFFFFF, 00b, DATA_READ_WRITE ; data and stack segment descriptor
SEGMENT_DESCRIPTOR 0x000B8000, 0x00008, 00b, DATA_READ_WRITE ; video memory in text mode segment descriptor
CODE_SELECTOR equ 0000000000001000b ; code segment selector: RPL = 0, TI = 0, Descriptor Index = 1
DATA_SELECTOR equ 0000000000010000b ; data segment selector: RPL = 0, TI = 0, Descriptor Index = 2
VIDEO_SELECTOR equ 0000000000011000b ; video segment selector: RPL = 0, TI = 0, Descriptor Index = 3
gdt_size equ $ - gdt
; data to be loaded to GDTR register
gdtr:
dw gdt_size - 1 ; 16-bit limit of the global descriptor table
dd gdt ; 32-bit base address of the global descriptor table
; -------------------------------------------- PROTECTED MODE CODE ---------------------------------------------
; Protected mode entry point
pm_entry:
; Initialize segment registers (except CS which is already litialized)
mov ax, DATA_SELECTOR
mov ds, ax
mov fs, ax
mov gs, ax
mov ss, ax
; Load the selector of the video memory segment into ES
; because ES is used in string operations like stos, stosw etc.
mov ax, VIDEO_SELECTOR
mov es, ax
include 'protected_mode.inc'
finish:
Далее нас будет интересовать только файл protected_mode.inc. Его структура такова:
jmp protected_mode_code
; разные данные: текстовые сообщения, счетчик для таймера, позиция курсора, скан-коды клавиатуры
; таблица дескрипторов шлюзов прерываний (Interrupt Descriptor Table)
include 'pm_utility_functions.inc' ; полезные функции (например, вывод текста на экран)
include 'keyboard.inc' ; функции и макроопределения для работы с клавиатурой
protected_mode_code:
; вывод на экран текстового сообщения о том, что мы в защищенном режиме
; инициализация контроллера прерываний
; разрешение прерываний
jmp $ ; бесконечный цикл
; обработчики прерываний
Рассмотрим каждую часть кода в отдельности.
Вывод на экран текстовых сообщений
Пока файл pm_utility_functions.inc содержит только функции для вывода текста на экран. Возможно в дальнейшем я добавлю в него еще какие-то функции. В текстовом режиме экран делится на 25 строк и 80 столбцов и содержит 25*80=2000 текстовых символов. Все 2000 символов представляют собой массив, который находится в видеопамяти, которая проецируется на адресное пространство процессора начиная с адреса 0xB800 (этот адрес, как вы помните, записан у нас в специальном дескрипторе сегмента). Каждый символ занимает два байта. Формат этих двух байт следующий:
Атрибут | Символ | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
Мерцание | Цвет фона | Цвет текста | ASCII-код символа |
Таким образом, чтобы вывести текстовую строку на экран, надо скопировать символы этой строки в видеопамять, не забыв добавить к каждому символу его атрибут. Вот это все и делает функция PrintString, код которой приведен ниже. Также у нас есть функция ClearScreen, которая заполняет весь экран нулевыми символами.
; <---- ClearScreen ---------------
ClearScreen:
push eax
push ecx
push edi
mov eax, 0x0700 ; symbol (0x00) and its attribute (0x07)
mov ecx, 2000 ; size of the screen in symbols
cld ; direction flag = 0 (means that edi will be incremented)
xor edi, edi ; edi = 0
rep stosw ; while(ecx != 0) { mov word [es:edi], ax; edi = edi + 2; }
pop edi
pop ecx
pop eax
ret
; -------------------------------->
; <---- PrintString ---------------
; Prints out a string. The string must be zero-terminated.
; Parameters: dword [ebp + 8] - address of a zero-terminated string
; word [ebp + 12] - column (0...79)
; word [ebp + 14] - row (0...24)
; Remarks: This function assumes that the video card is in the text mode (CGA-mode)
; where there are 80 columns * 25 rows each cell comprised of two bytes:
; 1st - the ASCII-code,
; 2nd - the attribute: four most significant bits define background color,
; four least significant bits define foreground color.
; The function also assumes that ES points to a segment descriptor which points to 0xB800 address
; which is the beginning of video memory in video text mode.
PrintString:
push ebp
mov ebp, esp
push eax
push edi
push esi
xor eax, eax ; eax = 0
mov al, 160 ; al = 80*2
mov ah, [ebp + 14] ; ah = row
mul ah ; ax = al * ah = 80*2 * row
add ax, [ebp + 12] ; ax = ax + column = 80*2 * row + column
add ax, [ebp + 12] ; ax = ax + column = 80*2 * row + column*2
mov esi, [ebp + 8]
mov edi, eax
cld
; Text-printing loop
@@:
lodsb ; load to al the next charachter located at [ds:esi] address in memory (esi is incremented automatically as DF = 0)
test al, al ; test al against zero
jz @f ; exit if al is zero
stosb ; otherwise move al to memory at address [es:edi] (edi is incremented automatically as DF = 0)
mov al, 0x07 ; attribute=0x07 (foreground=light grey, background=black)
stosb ; move al to memory at address [es:edi] (edi is incremented automatically as DF = 0)
jmp @b
@@:
pop esi
pop edi
pop eax
mov esp, ebp
pop ebp
ret 8
; -------------------------------->
Шлюзы прерываний
Макросы для определения дескрипторов сегментов и шлюзов я поместил в отдельный файл descriptor_macros.inc. Формат дескриптора шлюза прерывания мы уже рассмотрели. Написать после этого макрос для его определения несложно. Все пояснения — в комментариях.
; Segment descriptor format
; BITS | SIZE | FIELD
; ------+------+------
; 0-15 | 16 | Limit[0:15]
; 16-39 | 24 | Base[0:23]
; 40-47 | 8 | P DPL[0:1] S Type[0:3]
; 48-55 | 8 | G D/B L AVL Limit[16:19]
; 56-63 | 8 | Base[24:31]
; The following macro defines a code or a data segment descriptor.
; The following assumtions take place:
; P=1 (the segment is present in physical memory)
; S=1 (the segment is not a system segment)
; G=1 (the Limit of the segment is measured in 4-kiB pages)
; D/B=1 L=0 (the segment is a 32-bit segment)
; AVL=0
macro SEGMENT_DESCRIPTOR _Base, _Limit, _DPL, _Type
{
dw _Limit and 0xFFFF ; Limit[0:15]
dw _Base and 0xFFFF ; Base[0:15]
db (_Base shr 16) and 0xFF ; Base[15:23]
db _Type or (_DPL shr 5) or 10010000b ; P DPL[0:1] S Type[0:3]
db 11000000b or (_Limit shr 16) ; G D/B L AVL Limit[16:19]
db (_Base shr 24) ; Base[24:31]
}
; Segment descriptor types (see Intel 64 and IA-32 Architectures Software Developer’s Manual - 3.4.5.1 Code- and Data-Segment Descriptor Types).
; Can be used as the _Type argument of SEGMENT_DESCRIPTOR macro
DATA_READ_ONLY equ 0000b
DATA_READ_WRITE equ 0010b
DATA_READ_ONLY_EXPAND_DOWN equ 0100b
DATA_READ_WRITE_EXPAND_DOWN equ 0110b
CODE_EXECUTE_ONLY equ 1000b
CODE_EXECUTE_READ equ 1010b
CODE_EXECUTE_ONLY_CONFORMING equ 1110b
CODE_EXECUTE_READ_CONFORMING equ 1111b
; Interrupt gate descriptor format
; BITS | SIZE | FIELD
; ------+------+------
; 0-15 | 16 | Offset[0:15]
; 16-31 | 16 | Selector[0:15]
; 32-39 | 8 | reserved
; 40-47 | 8 | P DPL[0:1] 0 D 1 1 0
; 48-63 | 16 | Offset[16:31]
; The following macro defines an interrupt gate descriptor.
; The following assumtions take place:
; P=1 (the segment is present in physical memory)
; D=1 (the size of gate is 32 bit)
; DPL=0 (descriptor privilege level = 0)
macro INTERRUPT_GATE_DESCRIPTOR _Selector, _Offset
{
dw _Offset and 0xFFFF ; Offset[0:15]
dw _Selector ; Selector
db 0 ; reserved
db 10001110b ; P DPL[0:1] 0 D 1 1 0
dw _Offset shr 16 ; Offset[16:31]
}
; Trap gate descriptor format
; BITS | SIZE | FIELD
; ------+------+------
; 0-15 | 16 | Offset[0:15]
; 16-31 | 16 | Selector[0:15]
; 32-39 | 8 | reserved
; 40-47 | 8 | P DPL[0:1] 0 D 1 1 1
; 48-63 | 16 | Offset[16:31]
; The following macro defines a trap gate descriptor.
; The following assumtions take place:
; P=1 (the segment is present in physical memory)
; D=1 (the size of gate is 32 bit)
; DPL=0 (descriptor privilege level = 0)
macro TRAP_GATE_DESCRIPTOR _Selector, _Offset
{
dw _Offset and 0xFFFF ; Offset[0:15]
dw _Selector ; Selector
db 0 ; reserved
db 10001111b ; P DPL[0:1] 0 D 1 1 1
dw _Offset shr 16 ; Offset[16:31]
}
Контроллер прерываний
Аппаратные прерывания — это механизм, при помощи которого периферийные устройства могут сообщать процессору о неких событиях. Периферийных устройств довольно много, и каждому выделена линия прерывания, на которую устройство может выдавать цифровой сигнал. Все эти линии прерываний идут от периферийных устройств на специальную микросхему на материнской плате компьютера — контроллер прерываний (PIC — Programmable Interrupt Controller). До выхода процессора Intel Pentium этой микросхемой была i8259A (точнее, две соединенные друг с другом микросхемы i8259A). Начиная с процессора Pentium начал использоваться новый механизм обработки прерываний под названием APIC — Advanced Programmable Interrupt Controller, а в качестве контроллера прерываний стала использоваться микросхема i82093AA (i82093AA — это так называемый I/O APIC, а есть еще т. н. Local APIC — блок, встроенный в процессор). В модели APIC предусмотрена обратная совместимость со старой моделью (PIC), когда I/O APIC эмулирует работу двух микросхем i8259A. В этой заметке мы будем пользоваться старой моделью, а почитать подробнее о новой модели можно в следующих источниках:
- Intel 64 and IA-32 Architectures Software Developer’s Manual — Volume 3. System Programming Guide CHAPTER 10 ADVANCED PROGRAMMABLE INTERRUPT CONTROLLER (APIC).
- os developer — Advanced Programmable Interrupt Controller
- OSDev.org — APIC
Итак, рассматриваем старую модель (PIC — две микросхемы i8259A). О возникновении запроса на прерывание от периферийного устройства процессор узнает благодаря входу INTR (interrupt request). Линия INTR соединена с PIC. У PIC есть 16 линий IRQ, на которые периферийные устройства присылают свои запросы на прерывания. PIC состоит из двух микросхем i8259A, одна из них называется ведущей (master), другая — ведомой (slave). У каждой i8259A восемь входов IRQ — всего получается 16. Причем ведомая i8259A подключена ко входу IRQ2 ведущей i8259A. Когда периферийное устройство посылает контроллеру прерываний запрос на прерывание (устанавливая активный логический уровень на соответствующем входе IRQ), контроллер в свою очередь устанавливает активный уровень на входе INTR процессора, после чего процессор прерывает выполнение основной программы, получает по системной шине от контроллера прерываний номер вектора прерывания, находит вектор прерывания в таблице векторов прерываний и переходит к выполнению обработчика прерывания. Каждому входу IRQ контроллера прерываний соответствует номер вектора прерываний, и программист должен запрограммировать это соответствие при инициализации контроллера прерываний. Например, в программе я настраиваю PIC так, чтобы вход IRQ0 соответствовал вектору прерывания номер 32, а IRQ15 — вектору номер 47. Программирование PIC осуществляется при помощи портов ввода-вывода 0x20 (регистр iSr ведущего PIC), 0x21 (регистр iMr ведущего PIC), 0xA0 (регистр iSr ведомого PIC), 0xA1 (регистр iMr ведомого PIC). Подробности читайте в книге [Аблязов] раздел 2.2.6. Аппаратные прерывания.
У процессора есть еще один вход — NMI (non-maskable interrupt), который активируется при возникновении неких аппаратных сбоев (см. OSDev.org — Non Maskable Interrupt и раздел 6.7 NONMASKABLE INTERRUPT (NMI) руководства Intel 64 and IA-32 Architectures Software Developer’s Manual — Volume 3. System Programming Guide). Обработчик NMI имеет номер 2 в таблице векторов прерываний. Несмотря на название, немаскируемые прерывания все-таки можно отключить при помощи порта ввода-вывода 0x70 (CMOS controller).
Обработчики прерываний
Обработчик прерывания — это в общем обычная функция, впрочем есть пара особенностей. Когда возникает прерывание, процессор прерывает выполнение основной программы. При этом он сохраняет в стеке адрес машинной команды основной программы, на которой ему пришлось прерваться (пара CS:EIP), с тем, чтобы после обработки прерывания продолжить выполнение основной программы с этой команды. Еще в стеке сохраняется содержимое регистра EFLAGS. Команда возврата из обработчика прерывания должна восстановить значение регистра EFLAGS из стека и перепрыгнуть обратно на прерванную основную программу. Эта команда возврата называется IRETD.
Кроме того, при возникновении некоторых исключений в стек помещается так называемый код ошибки — его обработчик исключения должен вытолкнуть их стека перед выполнением команды IRETD. Какие исключения помещают в стек код ошибки, а какие нет — смотрите в Table 6-1. Protected-Mode Exceptions and Interrupts руководства [Intel-Volume3].
Обработчики аппаратных прерываний должны при своем завершении производить так называемый сброс обеих микросхем i8259A, сообщая им, что прерывание обработано. Это делается записью в их регистры iSr числа 0x20. Соответствующий код в программе помещен в функцию int_EOI, и все обработчики аппаратных прерываний в конце прыгают на эту функцию.
General Protection Fault
Исключение общей защиты возникает при нарушении кодом ограничений, задаваемых его уровнем привилегий. Это может быть например доступ к памяти ОС или выполнение команд in и out пользовательским кодом. В программе обработчик этого исключения просто выводит на экран сообщение «General Protection Exception». В этом исключении есть код ошибки, поэтому обработчик выталкивает его из стека (pop eax).
Прерывание от системного таймера
Прерывание от системного таймера происходит с частотой 18 раз в секунду. На нем можно построить измерение времени, как это сделано в книге [Аблязов], но я решил упростить дело и просто вывожу в правый верхний угол экрана косую черточку, каждую секунду меняя ее направление (с \ на / и обратно). Для подсчета прерываний от таймера используется глобальная переменная timer_counter.
Прерывание от контроллера клавиатуры
Тема работы с клавиатурой сама по себе достаточно большая, поэтому я написал об этом отдельную заметку.
Исходный код программы в защищенном режиме
Ниже приведен полный код файла protected_mode.inc:
jmp protected_mode_code
; -------------------------------------------- DATA ------------------------------------------------------------
pm_message db 'Hello from protected mode! You can type something. Pressing Esc clears the screen.', 0
gp_message db 'General Protection Exception', 0
; counter being incremented at each interrupt from the system timer
timer_counter dd 0
; cursor position (0...1999). initial value of 160 places the cursor to the 4th row on the screen.
cursor dd 240
; -------------------------------------------- INTERRUPT DESCRIPTOR TABLE --------------------------------------
align 8
idt:
dq 0 ; 0 #DE Fault Error code No Divide Error
dq 0 ; 1 #DB Fault/Trap Error code No Debug Exception (For Intel use only)
dq 0 ; 2 - Interrupt Error code No Nonmaskable external interrupt
dq 0 ; 3 #BP Trap Error code No Breakpoint
dq 0 ; 4 #OF Trap Error code No Overflow
dq 0 ; 5 #BR Fault Error code No BOUND Range Exceeded
dq 0 ; 6 #UD Fault Error code No Invalid Opcode (Undefined Opcode)
dq 0 ; 7 #NM Fault Error code No Device Not Available (No Math Coprocessor)
dq 0 ; 8 #DF Abort Error code Zero Double Fault
dq 0 ; 9 Fault Error code Yes Coprocessor Segment Overrun (reserved)
dq 0 ; 10 #TS Fault Error code Yes Invalid TSS
dq 0 ; 11 #NP Fault Error code Yes Segment Not Present
dq 0 ; 12 #SS Fault Error code Yes Stack-Segment Fault
INTERRUPT_GATE_DESCRIPTOR CODE_SELECTOR, exGP_handler; 13 #GP Fault Error code Yes General Protection
dq 0 ; 14 #PF Fault Error code Yes Page Fault
dq 0 ; 15 - Error code No Intel reserved. Do not use.
dq 0 ; 16
dq 0 ; 17 #MF Fault Error code No x87 FPU Floating-Point Error (Math Fault)
dq 0 ; 18 #MC Abort Error code No Machine Check
dq 0 ; 19 #XM Fault Error code No SIMD Floating-Point Exception
dq 0 ; 20 #VE Fault Error code No Virtualization Exception
dq 0 ; 21 - Intel reserved. Do not use.
dq 0 ; 22 - Intel reserved. Do not use.
dq 0 ; 23 - Intel reserved. Do not use.
dq 0 ; 24 - Intel reserved. Do not use.
dq 0 ; 25 - Intel reserved. Do not use.
dq 0 ; 26 - Intel reserved. Do not use.
dq 0 ; 27 - Intel reserved. Do not use.
dq 0 ; 28 - Intel reserved. Do not use.
dq 0 ; 29 - Intel reserved. Do not use.
dq 0 ; 30 - Intel reserved. Do not use.
dq 0 ; 31 - Intel reserved. Do not use.
; --- Master PIC ---
INTERRUPT_GATE_DESCRIPTOR CODE_SELECTOR, irq0_handler ; 32 IRQ 0 System timer
INTERRUPT_GATE_DESCRIPTOR CODE_SELECTOR, irq1_handler ; 33 IRQ 1 Keyboard controller
INTERRUPT_GATE_DESCRIPTOR CODE_SELECTOR, int_EOI ; 34 IRQ 2 Cascaded signals from IRQs 8–15 (from slave PIC)
INTERRUPT_GATE_DESCRIPTOR CODE_SELECTOR, int_EOI ; 35 IRQ 3 Serial port controller for serial port 2 (shared with serial port 4, if present)
INTERRUPT_GATE_DESCRIPTOR CODE_SELECTOR, int_EOI ; 36 IRQ 4 Serial port controller for serial port 1 (shared with serial port 3, if present)
INTERRUPT_GATE_DESCRIPTOR CODE_SELECTOR, int_EOI ; 37 IRQ 5 Parallel port 2 and 3 or sound card
INTERRUPT_GATE_DESCRIPTOR CODE_SELECTOR, int_EOI ; 38 IRQ 6 Floppy disk controller
INTERRUPT_GATE_DESCRIPTOR CODE_SELECTOR, int_EOI ; 39 IRQ 7 Parallel port 1. It is used for printers or for any parallel port if a printer is not present.
; --- Slave PIC ----
INTERRUPT_GATE_DESCRIPTOR CODE_SELECTOR, int_EOI ; 40 IRQ 8 Real-time clock (RTC)
INTERRUPT_GATE_DESCRIPTOR CODE_SELECTOR, int_EOI ; 41 IRQ 9 Advanced Configuration and Power Interface (ACPI) system control interrupt on Intel chipsets
INTERRUPT_GATE_DESCRIPTOR CODE_SELECTOR, int_EOI ; 42 IRQ 10 The Interrupt is left open for the use of peripherals
INTERRUPT_GATE_DESCRIPTOR CODE_SELECTOR, int_EOI ; 43 IRQ 11 The Interrupt is left open for the use of peripherals
INTERRUPT_GATE_DESCRIPTOR CODE_SELECTOR, int_EOI ; 44 IRQ 12 Mouse on PS/2 connector
INTERRUPT_GATE_DESCRIPTOR CODE_SELECTOR, int_EOI ; 45 IRQ 13 CPU co-processor or integrated floating point unit or inter-processor interrupt
INTERRUPT_GATE_DESCRIPTOR CODE_SELECTOR, int_EOI ; 46 IRQ 14 Primary ATA channel (ATA interface usually serves hard disk drives and CD drives)
INTERRUPT_GATE_DESCRIPTOR CODE_SELECTOR, int_EOI ; 47 IRQ 15 Secondary ATA channel
idt_size = $ - idt
idtr:
dw idt_size - 1 ; 16-bit limit of the interrupt descriptor table
dd idt ; 32-bit base address of the interrupt descriptor table
; -------------------------------------------- CODE ------------------------------------------------------------
include 'pm_utility_functions.inc'
include 'keyboard.inc'
protected_mode_code:
push word 1 ; row
push word 0 ; column
push pm_message
call PrintString
; Disable scan-code translation to set2
push word KEYBOARD_CONTROLLER_COMMAND_READ_COMMAND_BYTE
call Keyboard_SendCommand
call Keyboard_ReadOutputBuffer
and al, not KEYBOARD_COMMAND_BYTE_XLAT
push word KEYBOARD_CONTROLLER_COMMAND_WRITE_COMMAND_BYTE
call Keyboard_SendCommand
push ax
call Keyboard_WriteInputBuffer
; Disable keyboard
;push word KEYBOARD_COMMAND_DISABLE
;call Keyboard_WriteInputBuffer
;call Keyboard_ReadOutputBuffer
; Enable keyboard
;push word KEYBOARD_COMMAND_ENABLE
;call Keyboard_WriteInputBuffer
;call Keyboard_ReadOutputBuffer
; Load IDTR register
lidt [idtr]
; Initialize Programmable Interrupt Controller (PIC)
iSr_master equ 0x20
iMr_master equ 0x21
iSr_slave equ 0xA0
iMr_slave equ 0xA1
; <-------- master i8259A PIC initialization --------
mov al, 00010001b
out iSr_master, al
; Define interrupt vector for the 0th line of PIC
mov al, 0x20 ; (interrupt vector No 32)
out iMr_master, al
; Bit mask defines the line of master i8259A to which slave i8259A is connected
mov al, 00000100b ; (line No 2)
out iMr_master, al
mov al, 00000001b
out iMr_master, al
; -------------------------------------------------->
; <-------- slave i8259A PIC initialization ---------
mov al, 00010001b
out iSr_slave, al
; Define interrupt vector for the 0th line of PIC
mov al, 0x28 ; (interrupt vector No 40)
out iMr_slave, al
; Defines the line number through which slave i8259A is connected to master i8259A
mov al, 00000010b ; (line No 2)
out iMr_slave, al
mov al, 00000001b
out iMr_slave, al
; -------------------------------------------------->
; Enable non-maskable interrupts (NMIs)
in al, 0x70 ; CMOS register
and al, 0x7F
out 0x70, al
; Enable maskable interrupts
sti
; Infinite loop
jmp $
; -------------------------------------------- INTERRUPT HANDLERS ----------------------------------------------
; ---- End of Interrupt -----------
int_EOI:
push ax
; Reset interrupt controllers
mov al, 0x20
out iSr_master, al
out iSr_slave, al
pop ax
iretd
; ---- General Protection Fault ---
exGP_handler:
pop eax ; pop error code
push 0
push 0
push gp_message
call PrintString
iretd
; ---- System Timer ---------------
irq0_handler:
mov byte [es:159], 0x07
inc byte [timer_counter]
cmp byte [timer_counter], 18
jz @f
jmp int_EOI
@@:
mov byte [timer_counter], 0
cmp byte [es:158], '/'
jz @f
mov byte [es:158], '/'
jmp int_EOI
@@:
mov byte [es:158], '\'
jmp int_EOI
KEYBOARD_KEY_IS_SPECIAL_FLAG equ 00000001b
KEYBOARD_KEY_IS_BREAK_FLAG equ 00000010b
scan_code_flags db 0
; ---- Keyboard Controller --------
irq1_handler:
push ax
push edi
in al, 0x60
; <----------------------------------------------
;if(al == KEYBOARD_SPECIAL_KEY)
;{
; scan_code_flags |= KEYBOARD_KEY_IS_SPECIAL_FLAG;
; return;
;}
;else if(al == KEYBOARD_BREAK_KEY)
;{
; scan_code_flags |= KEYBOARD_KEY_IS_BREAK_FLAG;
; return;
;}
;else if((scan_code_flags & KEYBOARD_KEY_IS_SPECIAL_FLAG) != 0 || (scan_code_flags & KEYBOARD_KEY_IS_BREAK_FLAG) != 0)
;{
; scan_code_flags = 0;
; return;
;}
cmp al, KEYBOARD_SPECIAL_KEY
jnz @f
or byte [scan_code_flags], KEYBOARD_KEY_IS_SPECIAL_FLAG
jmp .exit
@@:
cmp al, KEYBOARD_BREAK_KEY
jnz @f
or byte [scan_code_flags], KEYBOARD_KEY_IS_BREAK_FLAG
jmp .exit
@@:
test [scan_code_flags], KEYBOARD_KEY_IS_SPECIAL_FLAG
jz @f
mov [scan_code_flags], 0
jmp .exit
@@:
test [scan_code_flags], KEYBOARD_KEY_IS_BREAK_FLAG
jz @f
mov [scan_code_flags], 0
jmp .exit
; ---------------------------------------------->
@@:
; convert scan-code to ASCII-code
dec al
movzx edi, al
mov al, [edi+SCAN_CODE_SET2]
cmp al, 0
jz .exit
; print symbol on the screen
mov ah, 0x07 ; symbol attribute (light gray on black)
mov edi, [cursor]
mov [es:edi*2], ax
; advance cursor position (if(++cursor >= 2000) cursor = 0;)
inc dword [cursor]
cmp dword [cursor], 2000
jb .exit
mov dword [cursor], 0
.exit:
pop edi
pop ax
jmp int_EOI
Makefile
Наконец, вот makefile для построения программы:
all: floppy.img boot_sector.lst
floppy.img : boot_sector.bin
dd if="/dev/zero" of="floppy.img" bs=1024 count=1440
dd if=boot_sector.bin of=floppy.img conv=notrunc
boot_sector.bin : boot_sector.asm bios_services.inc descriptor_macros.inc protected_mode.inc pm_utility_functions.inc keyboard.inc
fasm boot_sector.asm boot_sector.bin
boot_sector.lst: boot_sector.fas
listing -a boot_sector.fas boot_sector.lst
boot_sector.fas: boot_sector.asm bios_services.inc descriptor_macros.inc protected_mode.inc pm_utility_functions.inc keyboard.inc
fasm boot_sector.asm -s boot_sector.fas
Здравствуйте. С удовольствием прочел цикл Ваших статей, так как потребовалось «с нуля» написать нечто похожее. Я правильно понимаю, что на прерываниях и клавиатуре Вы и остановились?
Здравствуйте, Денис! Спасибо, рад, что вам понравились мои заметки. В общем, да, я на этом и остановился. Но может быть вернусь к теме ОС когда-нибудь. К OpenGL, например, я периодически возвращаюсь 🙂