深入理解 x86/x64 的中斷體系--IVT VS IDT
1. 真實模式下的中斷機制
x86 processor 在加電後被初始化為 real mode 也稱為 real-address mode,關於真實模式請詳見文章:http://www.mouseos.com/arch/001.html
processor 執行的第一條指標在 0xFFFFFFF0 處,這個地址經過 North Bridge(北橋)和 South ridge(南橋)晶片配合解碼,最終會訪問到固化的 ROM 塊,同時,經過別名機制對映在地址空間低端,實際上等於 ROM 被對映到地址空間最高階和低端位置。
此時在系統的記憶體裡其實並不存在 BIOS 程式碼,ROM BIOS 的一部分職責是負責安裝 BIOS 程式碼進入系統記憶體。
jmp far f000:e05b |
典型是這條指令就是 0xFFFFFFF0 處的 ROM BIOS 指令,執行後它將跳到 0x000FE05B 處,這條指令的作用很大:
- 更新 CS.base 使 processor 變成純正的 real mode
- 跳轉到低端記憶體,使之進入 1M 低端區域
前面說過,此時記憶體中也不存在 BIOS,也就是說 IVT(中斷向量表)也是不存在的,中斷系統此時是不可用的,那麼由 ROM BIOS 設定 IVT 。
1.1 中斷向量表(IVT)
IDTR.base 被初始化為 0,ROM BIOS 將不會對 IDTR.base 進行更改,因此如果真實模式 OS 不更改 IDTR.base 的值,這意味著 IVT
在保護模式下 IDTR.base 將向不再是中斷向量表,而是中斷描述符表。不再稱為 IVT 而是 IDT。那是因為:
- 在真實模式下,DITR.base 指向的表格項直接給出中斷服務例程(Interrupt Service Routine)的入口地址。
- 在保護模式下,並不直接給出入口地址,而是門描述符(Interrupt/Trap/Task gate),從這些門描述符間接取得中斷服務例程入口。
在 x86/x64 體系中允許有 256 箇中斷存在,中斷號從 0x00 - 0xff,共 256 箇中斷,如圖:
上面這個圖是真實模式下的 IVT 表,每個向量佔據 4 個位元組,中斷服務例程入口是以 segment:offset
1.2 改變中斷向量表地址
事實上,我們完全可以在真實模式下更改 IVT 的地址,下面的程式碼作為示例:
; **************************************************************** ; * boot.asm for interrupt demo(real mode) on x86 * ; * * ; * Copyright (c) 2009-2011 * ; * All rights reserved. * ; * mik * ; * visit web site : www.mouseos.com * ; * bug send email : [email protected] * ; * * ; * * ; * version 0.01 by mik * ; *************************************************************** BOOT_SEG equ 0x7c00 ; boot module load into BOOT_SEG ;---------------------------------------------------------- ; Now, the processor is real mode ;---------------------------------------------------------- bits 16 org BOOT_SEG ; for int 19 start: mov ax, cs mov ds, ax mov es, ax mov ss, ax mov sp, BOOT_SEG mov si, msg1 call printmsg sidt [old_IVT] ; save old IVT mov cx, [old_IVT] mov [new_IVT], cx ; limit of new IVT mov dword [new_IVT+2], 0x8000 ; base of new IVT mov si, [old_IVT+2] mov di, [new_IVT+2] rep movsb lidt [new_IVT] ; set new IVT mov si, msg2 call printmsg jmp $ ;----------------------------------- ; printmsg() - print message ;----------------------------------- printmsg: mov ah, 0x0e xor bh, bh print_loop: lodsb test al,al jz done int 0x10 jmp print_loop done: ret old_IVT dw 0 ; limit of IVT dd 0 ; base of IVT new_IVT dw 0 ; limit of IVT dd 0 ; base of IVT msg1 db 'Hi, print message with old IVT', 10,13, 0 msg2 db 'Now,pirnt message with new IVT', 13, 10, 0 times 510-($-$$) db 0 dw 0xaa55 ; end of boot.asm |
在 vmware 上這段程式碼的執行結果如圖:
這段程式碼在真實模式下將 IVT 表複製到 0x8000 位置上,然後將 IVT 地址設為 0x8000 上,這樣完全可以正常工作。正如程式碼上看到的,我做:
- 使用 sidt 指令取得 IDTR 暫存器的值,即 IVT 的 limit 和 base 值,儲存在 old_IVT 裡
- 設定 new_IVT 值,limit 等於 old_IVT 的 limit,base 設為 0x8000
- 將 IVT 表複製到 0x8000 處
- 使用 lidt 指令載入 IDTR 暫存器,即設 IVT 表在 0x8000 處
1.3 設定自己的中斷服務例程
在中斷向量表裡還有許多空 vector 是未使用的,我們可以在這些空白的向量裡設定自己的中斷服務例程,典型的如: DOS 作業系統中使用了 0x21 號向量作為 DOS 提供給使用者的系統呼叫!
在這裡我將展示,使用 0x40 向量作為自己的中斷服務例程向量,我所需要做的是:
- 寫一個自己的中斷服務例程,在本例中的 my_isr
- 設定向量 0x40 的 segment 和 offset 值
- 呼叫 int 0x40 進行測試
中斷服務例程 my_isr 很簡單,僅僅是列印資訊:
;------------------------------------------------ ; our Interrupt Service Routine: vector 0x40 ;------------------------------------------------- my_isr: mov si, msg3 call printmsg iret |
接下來設定 vector 0x40 的 segment 和 offset:
mov ax, cs mov bx, [new_IVT+2] ; base of IVT mov dword [bx+0x40*4], my_isr ; set offset 0f my_isr mov [bx+0x40*4+2], ax ; set segmet of my_isr |
記住 segment 在高位,offset 在低位,這個 segment 是我們當前的 CS,offset 是我們的 ISR 地址,直接寫入 IVT 表中就可以了
現在我們可以測試了:
int 0x40 |
結果如下:
我們的 ISR 能正常工作了,我提供了完整的示例原始碼和磁碟映像下載:interrupt_demo
2. 保護模式下的中斷機制
引入保護模式後,情形變得複雜多了,實施了許可權控制機制,為了支援許可權的控制增添了幾個重要的資料結構,下面是與中斷相關的結構:
- gate descriptor(門描述符):用於描述和控制 Interrupt Service Routine 的訪問,中斷可使用的 gate 包括:
- Interrupt-gate descriptor(中斷門描述符)
- Trap-gate descriptor(陷井門描述符)
- Task-gate descriptor(任務門描述符)-- 在 64 位模式下無效
- interrupt descriptor table(中斷描述符表):用於存在 gate descriptor 的表格
在 32 位保護模式下,每個 gate descriptor 是 8 bytes 寬,在 64 位模式下 gate descriptor 被擴充為 16 bytes, 同時 64 位模式下不存在 task gate descriptor,因此在 64 位下的 IDT 只允許存放 interrupt/trap gate descriptor。
當我們執行呼叫中斷時,processor 會在 IDT 表中尋找相應的 descriptor,並通過了許可權檢查轉入相應的 interrupt service routine(大多數時候被稱為 interrupt handler),在中斷體系裡分為兩種引發方式:
- 由硬體引發請求
- 由軟體執行呼叫
然而軟體上發起的中斷呼叫還可分為:
- 引發 exception(異常) 執行 interrupt handler
- 軟體請求執行 system service(系統服務),而執行 interrupt handler
硬體引發的中斷請求還可分為:
- maskable interrupt(可遮蔽中斷)
- non-maskable interrupt(不可遮蔽中斷)
無論何種方式,進入 interrupt handler 的途徑都是一樣的。
2.1 查詢 interrupt handler 入口
在發生中斷時,processor 在 IDTR.base 裡可以獲取 IDT 的地址,根據中斷號在 IDT 表裡讀取 descriptor,descriptor 的職責是給出 interrupt handler 的入口地址,processor 會檢查中斷號(vector)是否超越 IDT 的 limit 值。
上圖是 interrupt handler 的定位查詢圖。在 32 位模式下的過程是:
- 從 IDTR.base 得到 IDT 表地址
- 從 IDTR.base + vector * 8(每個 descriptor 為 8 bytes)處讀取 8 bytes 寬的 escriptor
- 對 descriptor 進行分析檢查,包括:
- descriptor 型別的檢查
- IDT limit 的檢查
- 訪問許可權的檢查
- 從 gate descriptor 裡讀取 selector
- 判斷 code segment descriptor 是存放在 GDT 表還是 LDT 表
- 使用 selector 從 descriptor table 讀取 code segment descriptor,當然這裡也要經過對 code segment descriptor 的檢查
- descriptor 型別檢查
- GDT limit 檢查
- 訪問許可權的檢查
- 從 code segment descriptor 中讀取 code segment base 值
- 從 gate descriptor 裡讀取 interrupt handler 的 offset 值
- 得取 interrupt handler 的入口地址:base + offset,轉入執行 interrupt handler
它的邏輯用 C 描述,類似下面:
long IDT_address; /* address of IDT */ long DT_address; /* GDT or LDT */ DESCRIPTOR gate_descriptor; /* gate descriptor */ DESCRIPTOR code_descriptor; /* code segment descriptor */ short selector; /* code segment selector */ IDT_address = IDTR.base; /* get address of IDT */ gate_descriptor = IDT_address + vector * 8; /* get descriptor */ selector = gate_descriptor.selector;DT_address = selector.TI ? LDTR.base : GDTR.base; /* address of GDT or LDT */ code_descriptor = GDT_address + selector * 8; /* get code segment descriptor */interrupt_handler = code_descriptor.base + gate_descripotr.offset; /* interrupt handler entry */ ((*(void))interrupt_handler)(); /* do interrupt_handler() */ |
上面的 C 程式碼顯示了 processor 定位 interrupt handler 的邏輯過程,為了清楚展示這個過程,這裡省略了各種的檢查機制!
2.2 IDT 表中 descriptor 型別的檢查
processor 會對 IDT 表中的 descriptor 型別進行檢查,這個檢查發生在:
當讀取 IDT 表中的 descriptor 時 |
在 IDT 中的 descriptor 型別要屬於:
- S = 0:屬於 system 類 descriptor
- descriptor 的 type 域應屬於:
- 1110:32-bit interrupt gate
- 1111:32-bit trap gate
- 0101:task gate
- 0110:16-bit interrupt gate
- 0111:16-bit trap gate
非上述所說的型別,都將會產生 #GP 異常。當 descriptor 的 S 標誌為 1 時,表示這是個 user 的 descriptor,它們是:code/data segment descriptor。可以看到在 32 位保護模式下 IDT 允許存在 interrupt/trap gate 以及 task gate
2.3 使用 16-bit gate descriptor
在 32 位保護模式下 interrupt handler 也能使用 16-bit gate descriptor,包括:
- 16-bit interrupt gate
- 16-bit trap gate
這是一個比較特別的現象,假如使用 16-bit gate 來構建中斷呼叫機制,實際上等於 interrupt handler 會從 32-bit 模式切換到 16-bit 模式執行。只要構建環境要素正確這樣切換執行當然是沒問題的。
這個執行環境要素需要注意的是:當使用 16-bit gate 時,也要相應使用 16-bit code segment descriptor。也就是在 gate descriptor 中的 selector 要使用 16-bit code segment selector。下面我寫了個使用 16-bit gate 構建 interrupt 呼叫的例子:
; set IDT vector mov eax, BP_handler mov [IDT+3*8], ax ; set offset 15-0 mov word [IDT+3*8+2], code16_sel ; 16-bit code selector mov word [IDT+3*8+4], 0xc600 ; DPL=3, 16-bit interrupt gate shr eax, 16 mov [IDT+3*8+8], ax ; offset 31-16 |
上面這段程式碼將 vector 3 設定為使用 16-bit interrupt gate,並且使用了 16-bit selector
下面是我的 interrupt handler 程式碼:
bits 16 ;----------------------------------------------------- ; INT3 BreakPoint handler for 16-bit interrupt gate ;----------------------------------------------------- BP_handler: jmp do_BP_handler BP_msg db 'I am a 16-bit breakpoint handler on 32-bit proected mode',0 do_BP_handler: mov edi, 10 mov esi, BP_msg call printmsg16 iret |
這個 interrupt handler 很簡單,只是列印一條資訊而已,值得注意的是,這裡需要使用 bits 16 來指示編譯為 16 位程式碼。
那麼這樣我們就可以使用 int3 指令來呼叫這個 16-bit 的 interrupt handler,執行結果如圖:
2.4 IDT 表的 limit 檢查
在 IDT 表中查詢索引 gate descriptor 時,processor 也會對 IDT 表的 limit 進行檢查,這個檢查的邏輯是:
gate_descriptor = IDTR.base + vector * 8; /* get gate descriptor */ if ((gate_descriptor + sizeof(DESCRIPTOR) - 1) > (IDTR.base + IDTR.limit)) { /* failure: #GP exception */ } |
我們看看下面這個圖:
當我們設:
- IDTR.base = 0x10000
- IDTR.limit = 0x1f
那麼 IDT 表的有效地址範圍是:0x10000 - 0x1001f,也就是:IDTR.base + IDTR.limit 這表示:
- vector 0:0x10000 - 0x10007
- vector 1:0x10008 - 0x1000f
- vector 2: 0x10010 - 0x10017
- vector 3: 0x10018 - 0x1001f
上面是這 4 個 vector 的有效範圍,因此:當設 IDTR.limit = 0x1e 時,如果訪問 vector 3 時(呼叫中斷3)processor 檢測到訪問 IDT 越界而出錯!
因此:訪問的 vector 地址在 IDTR.base 到 IDTR.base + IDTR.limit(含)之外,將會產生 #GP 異常。
2.5 請求訪問 interrupt handler 時的許可權檢查
訪問許可權的檢查是 x86/x64 體系中保護措施中非常重要的一環,它控制著訪問者是否有許可權進行訪問,在訪問 interrupt handler 過程許可權控制中涉及 3 個許可權類別:
- CPL:當前 processor 所處的許可權級別
- DPLg:代表 DPL of gate,也就是 IDT 中 gate descriptor 所要求的訪問許可權級別
- DPLs:代表 DPL of code segment,也就是 interrupt handler 的目標 code segment 所要求的訪問許可權級別
CPL 許可權級別代表著訪問者的許可權,也就是說當前正在執行程式碼的許可權,要理解許可權控制的邏輯,你需要明白下面兩點:
- 要呼叫 interrupt handler 那麼首先你必須要有許可權去訪問 IDT 表中的 gate,這表示:CPL 的許可權必須不低於 DPLg (gate 所要求的許可權),這樣你才有許可權去訪問 gate
- interrupt handler 會在高許可權級別裡執行,也就是說 interrupt handler 會在特權級別裡執行。這表示:interrupt handler 裡的許可權至少不低於訪問者的許可權
在呼叫 interrupt handler 中並不使用 selector 來訪問 gate 而是使用使用 vector 來訪問 gate,因此中斷許可權控制中並不使用 RPL 許可權類別,我們可以得知中斷訪問許可權控制的要素:
- CPL <= DPLg
- CPL >= DPLs
需同時滿足上面的兩個式子,在比較表示式中數字高的許可權低,數字低的許可權高!用 C 描述為:
DPLg = gate_descriptor.DPL; /* DPL of gate */ DPLs = code_descriptor.DPL; /* DPL of code segment */ if ((CPL <= DPLg) && (CPL >= CPLs)) { /* pass */ } else { /* failure: #GP exception */ } |
2.5.1 gate 的許可權設定
對於 gate 的權設定,我們應考慮 interrupt handler 呼叫上的兩個原則:
- interrupt handler 開放給使用者呼叫
- interrupt handler 在系統內部使用
由這兩個原則產生了 gate 權根設定的兩個設計方案:
- gate 的許可權設定為 3 級:這樣可以給使用者程式碼有足夠的許可權去訪問 gate
- gate 的許可權設定為 0 級:只允許核心程式碼訪問,使用者無權通過這個 gate 去訪問 interrupt handler
這是現代作業系統典型的 gate 許可權設定思路,絕大部分的 gate 都設定為高許可權,僅有小部分允許使用者訪問。很明顯:系統服務例程的呼叫入口應該設定為 3 級,以供使用者呼叫。
下面是很典型的設計:
- #BP 異常:BreakPoint(斷點)異常的 gate 應該設定為 3 級,使得使用者程式能夠使用斷點除錯程式。
- 系統呼叫:系統呼叫是 OS 提供給使用者訪問 OS 服務的介面,因此 gate 必須設定為 3 級。
系統呼叫在每個 OS 實現上可能是不同的,#BP 異常必定是 vector 3,因此對於 vector 3 所使用的 gate 必須使用 3 級許可權。
下面是在 windows 7 x64 作業系統上的 IDT 表的設定:
<bochs:2> info idt Interrupt Descriptor Table (base=0xfffff80004fea080, limit=4095): IDT[0x00]=64-Bit Interrupt Gate target=0x0010:fffff80003abac40, DPL=0 IDT[0x01]=64-Bit Interrupt Gate target=0x0010:fffff80003abad40, DPL=0 IDT[0x02]=64-Bit Interrupt Gate target=0x0010:fffff80003abaf00, DPL=0IDT[0x03]=64-Bit Interrupt Gate target=0x0010:fffff80003abb280, DPL=3 IDT[0x04]=64-Bit Interrupt Gate target=0x0010:fffff80003abb380, DPL=3 IDT[0x05]=64-Bit Interrupt Gate target=0x0010:fffff80003abb480, DPL=0 ... ... IDT[0x29]=64-Bit Interrupt Gate target=0x0010:fffff80003bf2290, DPL=0 IDT[0x2a]=64-Bit Interrupt Gate target=0x0010:fffff80003bf22a0, DPL=0 IDT[0x2b]=64-Bit Interrupt Gate target=0x0010:fffff80003bf22b0, DPL=0IDT[0x2c]=64-Bit Interrupt Gate target=0x0010:fffff80003abca00, DPL=3 IDT[0x2d]=64-Bit Interrupt Gate target=0x0010:fffff80003abcb00, DPL=3 IDT[0x2e]=64-Bit Interrupt Gate target=0x0010:fffff80003bf22e0, DPL=0 IDT[0x2f]=64-Bit Interrupt Gate target=0x0010:fffff80003b09590, DPL=0 IDT[0x30]=64-Bit Interrupt Gate target=0x0010:fffff80003bf2300, DPL=0 |
上面的粗體顯示 interrupt gate 被設定為 3 級,在 windows 7 x64 下 vector 0x2c 和 0x2d 被設定為系統呼叫介面。實際上這兩個 vector 的入口雖然不同,但是程式碼是一樣的。你可以通過 int 0x2c 和 int 0x2d 請求系統呼叫。
那麼對於系統內部使用的 gate 我們應該保持與使用者的隔離,絕大部分 interrupt handler 的 gate 許可權都是設定為 0 級的。
2.5.2 interrupt handler 的 code segment 許可權設定
前面說過:interrupt handler 的執行許可權應該至少不低於呼叫者的許可權,意味著 interrupt handler 需要在高許可權級別下執行。無論是系統提供給使用者的系統服務例程還是系統內部使用的 interrupt handler 我們都應該將 interrupt handler 設定為 0 級別的執行許可權(最高許可權),這樣才能保證 interrupt handler 能訪問系統的全部資源。
在許可權檢查方面,要求 DPLs 許可權(interrupt handler 的執行許可權)要高於或等於呼叫者的許可權,也就是 CPL 許可權,當數字上 DPLs 要小於等於 CPL(DPLs <= CPL)。
2.6 使用 interrupt gate
使用 interrupt gate 來構造中斷呼叫機制的,當 processor 進入 interrupt handler 執行前,processor 會將 eflags 值壓入棧中儲存並且會清 eflags.IF 標誌位,這意味著進入中斷後不允許響應 makeable 中斷(可遮蔽中斷)。它的邏輯 C 描述為:
*(--esp) = eflags; /* push eflags */ if (gate_descriptor.type == INTERRUPT_GATE) eflags.IF = 0; /* clear eflags.IF */ |
interrupt handler 使用 iret 指令返回時,會將棧中 eflags 值出棧以恢復原來的 eflags 值。
下面是 interrupt gate 的結構圖:
可以看到 interrupt gate 和 trap gate 的結構是完全一樣的,除了以 type 來區分 gate 外,interrupt gate 的型別是:
- 1110:32-bit interrupt gate
- 0110:16-bit interrupt gate
32 位的 offset 值提供了 interrupt handler 的入口偏移地址,這個偏移量是基於 code segment 的 base 值,selector 域提供了目標 code segment 的 selector,用來在 GDT 或 LDT 進行查詢 code segment descriptor。這些域的使用描述為:
if (gate_descriptor.selector.TI == 0) code_descriptor = GDTR.base + gate_descriptor.selector * 8; /* GDT */ else code_descriptor = LDTR.base + gate_descriptor.selector * 8; /* LDT */ interrupt_handler = code_descriptor.base + gate_descriptor.offset; /* interrupt handler entry */ |
注得注意的是:在 interrupt gate 和 trap gate 中的 selector 它的 RPL 是不起作用的,這個 selector.RPL 將被忽略。
在 OS 的實現中大部分的 interrupt handler 都是使用 interrupt gate 進行構建的。在 windows 7 x64 系統上全部都使用 interrupt gate 並沒有使用 trap gate
2.7 使用 trap gate
trap gate 在結構上與 interrupt gate 是完全一樣的,參見節 2.6 的那圖,trap gate 與 interrupt gate 不同的一點是:使用 trap gate 的,processor 進入 interrupt handler 前並不改變 eflags.IF 標誌,這意味著在 interrupt handler 裡將允許可遮蔽中斷的響應。
*(--esp) = eflags; /* push eflags */ if (gate_descriptor.type == TRAP_GATE) { /* skip: do nothing */ } else if (gate_descriptor.type == INTERRUPT_GATE){ eflags.IF = 0; /* clear eflags.IF */ } else if (gate_descriptor.type == TASK_GATE) { ... ... } |
2.8 使用 task gate
在使用 task gate 的情形下變得異常複雜,你需要為 new task 準備一個 task 資訊的 TSS,然而你必須事先要設定好當前的 TSS 塊,也就是說,系統中應該有兩個 TSS 塊:
- current TSS
- TSS of new task
當前的 TSS 是系統初始化設定好的,這個 TSS 的作用是:當發生 task 切換時儲存當前 processor 的狀態信(當前程序的 context 環境),新任務的 TSS 是通過 task gete 進行切換時使用的 TSS 塊,這個 TSS 是存放新任務的入口資訊。
tss_desc dw 0x67 ; seletor.SI = 3 dw TSS dd 0x00008900 tss_gate_desc dw 0x67 ; selector.SI = 4 dw TSS_TASKGATE dd 0x00008900 |
在上面的示例程式碼中,設定了兩個 TSS descriptor,一個供系統初始化使用(tss_desc),另一個是為新任務而設定(tss_task_gate),程式碼中必須設定兩個 TSS 塊:
- TSS
- TSS_TASKGATE
TSS 塊的內容是什麼在這個示例中無關緊要,然而 TSS_TASKGATE 塊中應該設定新任務的入口資訊,其中包括:eip 和 cs 值,以後必要的 DS 與 SS 暫存器值,還有 eflags 和 GPRs 值,下面的程式碼正是做這項工作:
; set TSS for task-gate mov dword [TSS_TASKGATE+0x20], BP_handler32 ; tss.EIP mov dword [TSS_TASKGATE+0x4C], code32_sel ; cs mov dword [TSS_TASKGATE+0x50], data32_sel ; ss mov dword [TSS_TASKGATE+0x54], data32_sel ; ds mov dword [TSS_TASKGATE+0x38], esp ; esp pushf pop eax mov dword [TSS_TASKGATE+0x24], eax ; eflags |
我將新任務的入口點設為 BP_handler32(),這個是 #BP 斷點異常處理程式,儲存當前的 eflags 值作為新任務的 eflags 值。
我們必須為 task gate 設定相應的 IDT 表項,正如下面的示例程式碼:
; set IDT vector: It's a #BP handler mov word [IDT+3*8+2], tss_taskgate_sel ; tss selector mov dword [IDT+3*8+4], 0xe500 ; type = task gate |
示例程式碼中,我為 vector 3(#BP handler)設定為 task-gate descirptor,當發生 #BP 異常時,就會通過 task-gate 進行任務切換到我們的新任務(BP_handler32)。
; load IDT into IDTR lidt [IDT_POINTER] ; load TSS mov ax, tss_sel ltr ax |
當然我們應該先設定好 IDT 表和載入當前的 TSS 塊,這個 TSS 塊就是我們所定義的第1個 TSS descirptor (tss_desc),這個 TSS 塊裡什麼內容都沒有,設定它的目的是為切換到新任務時,儲存當前任務的 context 環境,以便執行完新任務後切換回到原來的任務。
db 0xcc ; throw BreakPoint |
現在我們就可以測試我們的 BP_handler32(),通過 INT3 指令引發 #BP 異常,這個異常通過 task-gate 進行切換。
我們的 BP_handler32 程式碼是這樣的:
;----------------------------------------------------- ; INT3 BreakPoint handler for 32-bit interrupt gate ;----------------------------------------------------- BP_handler32: jmp do_BP_handler32 BP_msg32 db 'I am a 32-bit breakpoint handler with task-gate on 32-bit proected mode',0 do_BP_handler32: mov edi, 10 mov esi, BP_msg32 call printmsg clts ; clear CR0.TS flag iret |
它只是簡單的顯示一條資訊,在這個 BP_handler32 中,我們應該要清 CR0.TS 標誌位,這個標誌位是通過 TSS 進行任務切換時,processor 自動設定的,然而 processsor 不會清 CR0.TS 標誌位,需要程式碼中清除。
2.8.1 任務切換的情形
在本例中,我們來看看當進行任務切換時發生了什麼,processor 會設定一些標誌位:
- 置 CR0.TS = 1
- 置 eflags.NT = 1
設定 CR0.TS 標誌位表示當前發生過任務切換,processor 只會置位,而不會清位,事實上,你應該使用 clts 指令進行清位工作。設定 eflags.NT 標誌位表示當前任務是在巢狀層內,它指示當進行中斷返回時,需切換回原來的任務,因此,請注意:
當執行 iret 指令時,processor 會檢查 eflags.NT 標誌是否置位 |
當 eflags.NT 被置位時,processor 執行另一個任務切換工作,從 TSS 塊的 link 域中取出原來的 TSS selector 從而切換回原來的任務。這不像 ret 指令,它不會檢查 eflags.NT 標誌位。
processor 也會對 TSS descriptor 做一些設定標誌,當進入新任務時,processor 會設定 new task 的 TSS descriptor 為 busy,當切換回原任務時,會置回這個任務的 TSS descriptor 為 available,同時 processor 會檢查 TSS 中的 link 域的 TSS selector(原任務的 TSS)是否為 busy,如果不為 busy 則會丟擲 #TS 異常。
當然發生切換時 processor 會儲存當前的 context 到 current TSS 塊中,因此:
- 切換到目標任務時,processor 會儲存當前的任務 context 到 TSS,讀取目標任務的 TSS,載入相應的資訊,然後進入目標任務
- 目標任務執行完後,切換回原任務時,processor 會儲存目標任務的 context 到目標任務的 TSS 中,從目標任務的 TSS 塊讀取 link(原任務的 TSS selector),載入相應的資訊,返回到原任務
當從目標任務返回時,processor 會清目標任務的 eflags.NT = 0,如前所述目標任務的 TSS descriptor 也會被置為 available