Изучаем архитектуру Intel x86-64 при помощи ассемблера (Часть 5 — Прерывания)

После того как в предыдущей заметке мы написали загрузчик, мы не ограничены в размере кода нашей программы. Вернемся теперь снова к логике повествования книги Руслана Аблязова — Программирование на ассемблере на платформе х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 в самый конец программы.

; boot_sector.asm
; ====== РЕАЛЬНЫЙ РЕЖИМ ======
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:

; 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. Его структура такова:

; 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, которая заполняет весь экран нулевыми символами.

; pm_utility_functions.inc

; <---- 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. Формат дескриптора шлюза прерывания мы уже рассмотрели. Написать после этого макрос для его определения несложно. Все пояснения — в комментариях.

; 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. В этой заметке мы будем пользоваться старой моделью, а почитать подробнее о новой модели можно в следующих источниках:

Итак, рассматриваем старую модель (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:

; 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 для построения программы:

# 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

2 комментария для “Изучаем архитектуру Intel x86-64 при помощи ассемблера (Часть 5 — Прерывания)”

  1. Здравствуйте. С удовольствием прочел цикл Ваших статей, так как потребовалось «с нуля» написать нечто похожее. Я правильно понимаю, что на прерываниях и клавиатуре Вы и остановились?

  2. Здравствуйте, Денис! Спасибо, рад, что вам понравились мои заметки. В общем, да, я на этом и остановился. Но может быть вернусь к теме ОС когда-нибудь. К OpenGL, например, я периодически возвращаюсь 🙂

Добавить комментарий

Ваш адрес email не будет опубликован.