1. 程式人生 > >深入理解 x86/x64 的中斷體系--IVT VS IDT

深入理解 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

 在 0 的位置上,典型的如: DOS 作業系統。

保護模式下 IDTR.base 將向不再是中斷向量表,而是中斷描述符表。不再稱為 IVT 而是 IDT。那是因為:

  • 在真實模式下,DITR.base 指向的表格項直接給出中斷服務例程(Interrupt Service Routine)的入口地址。
  • 在保護模式下,並不直接給出入口地址,而是門描述符(Interrupt/Trap/Task gate),從這些門描述符間接取得中斷服務例程入口。

在 x86/x64 體系中允許有 256 箇中斷存在,中斷號從 0x00 - 0xff,共 256 箇中斷,如圖:

上面這個圖是真實模式下的 IVT 表,每個向量佔據 4 個位元組,中斷服務例程入口是以 segment:offset

 形式提供的,offset 在低端,segment 在高階,整個 IVT 表從地址 0x0 - 0x3FF,佔據了 1024 個位元組,即 1K bytes

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 上,這樣完全可以正常工作。正如程式碼上看到的,我做:

  1. 使用 sidt 指令取得 IDTR 暫存器的值,即 IVT 的 limit 和 base 值,儲存在 old_IVT 裡
  2. 設定 new_IVT 值,limit 等於 old_IVT 的 limit,base 設為 0x8000
  3. 將 IVT 表複製到 0x8000 處
  4. 使用 lidt 指令載入 IDTR 暫存器,即設 IVT 表在 0x8000 

1.3 設定自己的中斷服務例程

在中斷向量表裡還有許多空 vector 是未使用的,我們可以在這些空白的向量裡設定自己的中斷服務例程,典型的如: DOS 作業系統中使用了 0x21 號向量作為 DOS 提供給使用者的系統呼叫!

在這裡我將展示,使用 0x40 向量作為自己的中斷服務例程向量,我所需要做的是:

  1. 寫一個自己的中斷服務例程,在本例中的 my_isr
  2. 設定向量 0x40 的 segment 和 offset 值
  3. 呼叫 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 位模式下的過程是:

  1. 從 IDTR.base 得到 IDT 表地址
  2. 從 IDTR.base + vector * 8(每個 descriptor 為 8 bytes)處讀取 8 bytes 寬的 escriptor
  3. 對 descriptor 進行分析檢查,包括:
    • descriptor 型別的檢查
    • IDT limit 的檢查
    • 訪問許可權的檢查
  4. 從 gate descriptor 裡讀取 selector
  5. 判斷 code segment descriptor 是存放在 GDT 表還是 LDT 表
  6. 使用 selector 從 descriptor table 讀取 code segment descriptor,當然這裡也要經過對 code segment descriptor 的檢查
    • descriptor 型別檢查
    • GDT limit 檢查
    • 訪問許可權的檢查
  7. 從 code segment descriptor 中讀取 code segment base 值
  8. 從 gate descriptor 裡讀取 interrupt handler 的 offset 值
  9. 得取 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 許可權級別代表著訪問者的許可權,也就是說當前正在執行程式碼的許可權,要理解許可權控制的邏輯,你需要明白下面兩點:

  1. 要呼叫 interrupt handler 那麼首先你必須要有許可權去訪問 IDT 表中的 gate,這表示:CPL 的許可權必須不低於 DPLg (gate 所要求的許可權),這樣你才有許可權去訪問 gate
  2. 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