x86 processor 在加電後被初始化爲 real mode 也稱爲 real-address mode,關於實模式請詳見文章:http://www.mouseos.com/arch/001.htmlhtml
processor 執行的第一條指針在 0xFFFFFFF0 處,這個地址通過 North Bridge(北橋)和 South ridge(南橋)芯片配合解碼,最終會訪問到固化的 ROM 塊,同時,通過別名機制映射在地址空間低端,實際上等於 ROM 被映射到地址空間最高端和低端位置。web
此時在系統的內存裏其實並不存在 BIOS 代碼,ROM BIOS 的一部分職責是負責安裝 BIOS 代碼進入系統內存。windows
jmp far f000:e05b |
典型是這條指令就是 0xFFFFFFF0 處的 ROM BIOS 指令,執行後它將跳到 0x000FE05B 處,這條指令的做用很大:數據結構
前面說過,此時內存中也不存在 BIOS,也就是說 IVT(中斷向量表)也是不存在的,中斷系統此時是不可用的,那麼由 ROM BIOS 設置 IVT 。oop
IDTR.base 被初始化爲 0,ROM BIOS 將不會對 IDTR.base 進行更改,所以若是實模式 OS 不更改 IDTR.base 的值,這意味着 IVT 在 0 的位置上,典型的如: DOS 操做系統。測試
在保護模式下 IDTR.base 將向再也不是中斷向量表,而是中斷描述符表。再也不稱爲 IVT 而是 IDT。那是由於:spa
在 x86/x64 體系中容許有 256 箇中斷存在,中斷號從 0x00 - 0xff,共 256 箇中斷,如圖:操作系統
上面這個圖是實模式下的 IVT 表,每一個向量佔據 4 個字節,中斷服務例程入口是以 segment:offset 形式提供的,offset 在低端,segment 在高端,整個 IVT 表從地址 0x0 - 0x3FF,佔據了 1024 個字節,即 1K bytes設計
事實上,咱們徹底能夠在實模式下更改 IVT 的地址,下面的代碼做爲示例:指針
bits 16 org BOOT_SEG ; for int 19 sidt [old_IVT] ; save old IVT mov cx, [old_IVT] mov dword [new_IVT+2], 0x8000 ; base of new IVT mov si, [old_IVT+2] rep movsb lidt [new_IVT] ; set new IVT
;----------------------------------- print_loop: done:
old_IVT dw 0 ; limit of IVT new_IVT dw 0 ; limit of IVT
|
在 vmware 上這段代碼的執行結果如圖:
這段代碼在實模式下將 IVT 表複製到 0x8000 位置上,而後將 IVT 地址設爲 0x8000 上,這樣徹底能夠正常工做。正如代碼上看到的,我作:
在中斷向量表裏還有許多空 vector 是未使用的,咱們能夠在這些空白的向量裏設置本身的中斷服務例程,典型的如: DOS 操做系統中使用了 0x21 號向量做爲 DOS 提供給用戶的系統調用!
在這裏我將展現,使用 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
引入保護模式後,情形變得複雜多了,實施了權限控制機制,爲了支持權限的控制增添了幾個重要的數據結構,下面是與中斷相關的結構:
在 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),在中斷體系裏分爲兩種引起方式:
然而軟件上發起的中斷調用還可分爲:
硬件引起的中斷請求還可分爲:
不管何種方式,進入 interrupt handler 的途徑都是同樣的。
在發生中斷時,processor 在 IDTR.base 裏能夠獲取 IDT 的地址,根據中斷號在 IDT 表裏讀取 descriptor,descriptor 的職責是給出 interrupt handler 的入口地址,processor 會檢查中斷號(vector)是否超越 IDT 的 limit 值。
上圖是 interrupt handler 的定位查找圖。在 32 位模式下的過程是:
它的邏輯用 C 描述,相似下面:
long IDT_address; /* address of IDT */
selector = gate_descriptor.selector; ((*(void))interrupt_handler)(); /* do interrupt_handler() */ |
上面的 C 代碼顯示了 processor 定位 interrupt handler 的邏輯過程,爲了清楚展現這個過程,這裏省略了各類的檢查機制!
processor 會對 IDT 表中的 descriptor 類型進行檢查,這個檢查發生在:
當讀取 IDT 表中的 descriptor 時 |
在 IDT 中的 descriptor 類型要屬於:
非上述所說的類型,都將會產生 #GP 異常。當 descriptor 的 S 標誌爲 1 時,表示這是個 user 的 descriptor,它們是:code/data segment descriptor。能夠看到在 32 位保護模式下 IDT 容許存在 interrupt/trap gate 以及 task gate
在 32 位保護模式下 interrupt handler 也能使用 16-bit gate descriptor,包括:
這是一個比較特別的現象,假如使用 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 ;----------------------------------------------------- do_BP_handler: iret |
這個 interrupt handler 很簡單,只是打印一條信息而已,值得注意的是,這裏須要使用 bits 16 來指示編譯爲 16 位代碼。
那麼這樣咱們就可使用 int3 指令來調用這個 16-bit 的 interrupt handler,執行結果如圖:
完整的源代碼和軟盤映像下載:interrupt_demo1.rar
在 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)) |
咱們看看下面這個圖:
當咱們設:
那麼 IDT 表的有效地址範圍是:0x10000 - 0x1001f,也就是:IDTR.base + IDTR.limit 這表示:
上面是這 4 個 vector 的有效範圍,所以:當設 IDTR.limit = 0x1e 時,若是訪問 vector 3 時(調用中斷3)processor 檢測到訪問 IDT 越界而出錯!
所以:訪問的 vector 地址在 IDTR.base 到 IDTR.base + IDTR.limit(含)以外,將會產生 #GP 異常。
訪問權限的檢查是 x86/x64 體系中保護措施中很是重要的一環,它控制着訪問者是否有權限進行訪問,在訪問 interrupt handler 過程權限控制中涉及 3 個權限類別:
CPL 權限級別表明着訪問者的權限,也就是說當前正在執行代碼的權限,要理解權限控制的邏輯,你須要明白下面兩點:
在調用 interrupt handler 中並不使用 selector 來訪問 gate 而是使用使用 vector 來訪問 gate,所以中斷權限控制中並不使用 RPL 權限類別,咱們能夠得知中斷訪問權限控制的要素:
需同時知足上面的兩個式子,在比較表達式中數字高的權限低,數字低的權限高!用 C 描述爲:
DPLg = gate_descriptor.DPL; /* DPL of gate */ if ((CPL <= DPLg) && (CPL >= CPLs)) |
對於 gate 的權設置,咱們應考慮 interrupt handler 調用上的兩個原則:
由這兩個原則產生了 gate 權根設置的兩個設計方案:
這是現代操做系統典型的 gate 權限設置思路,絕大部分的 gate 都設置爲高權限,僅有小部分容許用戶訪問。很明顯:系統服務例程的調用入口應該設置爲 3 級,以供用戶調用。
下面是很典型的設計:
系統調用在每一個 OS 實現上多是不一樣的,#BP 異常一定是 vector 3,所以對於 vector 3 所使用的 gate 必須使用 3 級權限。
下面是在 windows 7 x64 操做系統上的 IDT 表的設置:
<bochs:2> info idt ... ... IDT[0x29]=64-Bit Interrupt Gate target=0x0010:fffff80003bf2290, DPL=0 |
上面的粗體顯示 interrupt gate 被設置爲 3 級,在 windows 7 x64 下 vector 0x2c 和 0x2d 被設置爲系統調用接口。實際上這兩個 vector 的入口雖然不一樣,可是代碼是同樣的。你能夠經過 int 0x2c 和 int 0x2d 請求系統調用。
那麼對於系統內部使用的 gate 咱們應該保持與用戶的隔離,絕大部分 interrupt handler 的 gate 權限都是設置爲 0 級的。
前面說過:interrupt handler 的執行權限應該至少不低於調用者的權限,意味着 interrupt handler 須要在高權限級別下運行。不管是系統提供給用戶的系統服務例程仍是系統內部使用的 interrupt handler 咱們都應該將 interrupt handler 設置爲 0 級別的運行權限(最高權限),這樣才能保證 interrupt handler 能訪問系統的所有資源。
在權限檢查方面,要求 DPLs 權限(interrupt handler 的執行權限)要高於或等於調用者的權限,也就是 CPL 權限,當數字上 DPLs 要小於等於 CPL(DPLs <= CPL)。
使用 interrupt gate 來構造中斷調用機制的,當 processor 進入 interrupt handler 執行前,processor 會將 eflags 值壓入棧中保存而且會清 eflags.IF 標誌位,這意味着進入中斷後不容許響應 makeable 中斷(可屏蔽中斷)。它的邏輯 C 描述爲:
*(--esp) = eflags; /* push eflags */ if (gate_descriptor.type == INTERRUPT_GATE) |
interrupt handler 使用 iret 指令返回時,會將棧中 eflags 值出棧以恢復原來的 eflags 值。
下面是 interrupt gate 的結構圖:
能夠看到 interrupt gate 和 trap gate 的結構是徹底同樣的,除了以 type 來區分 gate 外,interrupt gate 的類型是:
32 位的 offset 值提供了 interrupt handler 的入口偏移地址,這個偏移量是基於 code segment 的 base 值,selector 域提供了目標 code segment 的 selector,用來在 GDT 或 LDT進行查找 code segment descriptor。這些域的使用描述爲:
if (gate_descriptor.selector.TI == 0)
|
注得注意的是:在 interrupt gate 和 trap gate 中的 selector 它的 RPL 是不起做用的,這個 selector.RPL 將被忽略。
在 OS 的實現中大部分的 interrupt handler 都是使用 interrupt gate 進行構建的。在 windows 7 x64 系統上所有都使用 interrupt gate 並無使用 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) { |
在使用 task gate 的情形下變得異常複雜,你須要爲 new task 準備一個 task 信息的 TSS,然而你必須事先要設置好當前的 TSS 塊,也就是說,系統中應該有兩個 TSS 塊:
當前的 TSS 是系統初始化設置好的,這個 TSS 的做用是:當發生 task 切換時保存當前 processor 的狀態信(當前進程的 context 環境),新任務的 TSS 是經過 task gete 進行切換時使用的 TSS 塊,這個 TSS 是存放新任務的入口信息。
tss_desc dw 0x67 ; seletor.SI = 3 tss_gate_desc dw 0x67 ; selector.SI = 4 |
在上面的示例代碼中,設置了兩個 TSS descriptor,一個供系統初始化使用(tss_desc),另外一個是爲新任務而設置(tss_task_gate),代碼中必須設置兩個 TSS 塊:
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 |
示例代碼中,我爲 vector 3(#BP handler)設置爲 task-gate descirptor,當發生 #BP 異常時,就會經過 task-gate 進行任務切換到咱們的新任務(BP_handler32)。
; load IDT into IDTR
|
固然咱們應該先設置好 IDT 表和加載當前的 TSS 塊,這個 TSS 塊就是咱們所定義的第1個 TSS descirptor (tss_desc),這個 TSS 塊裏什麼內容都沒有,設置它的目的是爲切換到新任務時,保存當前任務的 context 環境,以便執行完新任務後切換回到原來的任務。
db 0xcc ; throw BreakPoint |
如今咱們就能夠測試咱們的 BP_handler32(),經過 INT3 指令引起 #BP 異常,這個異常經過 task-gate 進行切換。
咱們的 BP_handler32 代碼是這樣的:
;----------------------------------------------------- do_BP_handler32: mov edi, 10 clts ; clear CR0.TS flag iret |
它只是簡單的顯示一條信息,在這個 BP_handler32 中,咱們應該要清 CR0.TS 標誌位,這個標誌位是經過 TSS 進行任務切換時,processor 自動設置的,然而 processsor 不會清 CR0.TS 標誌位,須要代碼中清除。
在本例中,咱們來看看當進行任務切換時發生了什麼,processor 會設置一些標誌位:
設置 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 會清目標任務的 eflags.NT = 0,如前所述目標任務的 TSS descriptor 也會被置爲 available
版權 mik 全部,轉載請註明出處
from: http://www.mouseos.com/arch/interrupt.html