在實模式(Real Mode)下,中斷的處理是透過 中斷向量表(IVT, Interrupt Vector Table)來完成的。
[segment:offset] = (segment << 4) + offset
這種「段基址 + 偏移量」的表示方法,使得 16 位元暫存器可以透過拼接將記憶體訪問範圍從 64 KB 延伸到 1MB(每個 segment 一樣只能訪問 64 KB)。
當中斷觸發時將會觸發以下步驟:
雖然 IVT 提供了 256 個向量,在 PC 架構中,並不是所有中斷都由軟體 INT n 觸發。有一部分來自外部硬體。
外部裝置的 IRQ 訊號需要經過中斷控制器轉換如 8259 / APIC,才能映射到對應的向量號。
在 X86 架構下 0 ~ 0x13 的向量號是保留給 CPU 觸發 EXECPTION 的,而 0X14~0X1F 則為保留,為避免衝突我們可定義 8259A 的中斷從 0X20 開始。
cli
mov al, 0x11
out 0x20, al ; ICW1 master
out 0xA0, al ; ICW1 slave
mov al, 0x20
out 0x21, al ; ICW2 master: IRQ0 起始於 INT 0x20
mov al, 0x28
out 0xA1, al ; ICW2 slave: IRQ8 起始於 INT 0x28
mov al, 0x04
out 0x21, al ; ICW3 master: IR2 接 slave
mov al, 0x02
out 0xA1, al ; ICW3 slave: 連接在 master IR2
mov al, 0x01
out 0x21, al ; ICW4 master: 8086 模式
out 0xA1, al
; 僅允許 IRQ0
mov al, 0xFE
out 0x21, al
mov al, 0xFF
out 0xA1, al
這樣一來,我們就能把定時器的 IRQ0 映射到 INT 0x20。
PIT (Programmable Interval Timer) 是早期 PC 的定時器,頻率為 1.193182 MHz。透過設定除數,可以產生週期性中斷。
以下範例設定 PIT channel 0,以產生約 290 Hz 的中斷頻率
mov al, 0x36
out 0x43, al ; Ch0, LSB+MSB, Mode2 (rate generator)
mov al, 0x00
out 0x40, al ; divisor low
mov al, 0x10
out 0x40, al ; divisor high → 除數 = 0x1000 = 4096
觸發頻率為 1193182 / 4096 ~ 290 Hz。
當 IRQ0 發生時,PIC 會觸發 INT 0x20,CPU 會查找 IVT[0x20],跳到我們設置的 ISR:
irq0_handler:
push ax
push dx
push bx
mov dx, 0x00E9 ; debug port
mov bx, 0x0500
inc byte ptr [bx] ; counter++
mov al, [bx]
cmp al, 10
jb 1f
mov byte ptr [bx], 0
mov al, 0
1:
add al, '0'
out dx, al ; 觸發 VM-exit 輸出 '0' ... '9'
mov al, 0x20
mov dx, 0x0020
out dx, al ; EOI → 告訴 master PIC 中斷處理完畢
pop bx
pop dx
pop ax
iret
這段程式做了三件事:
在 Guest code 中只需要
cli
; 初始化 PIC + PIT
sti
halt_loop:
hlt
jmp halt_loop
cli 可屏蔽中斷,這裡用於保證 Guest code 的初始化行為。 sti 則用於恢復中斷。
在初始化過程中,可直接將 irq0_handler 的 CS:IP 地址寫入到 IVT 的 0x20 * 4。 PIT 觸發 IRQ0 時就會執行我們定義的 irq0_handler 並觸發 VM exit,將 IO 的資料在 host 中打印。執行結果大致如下: