本章的內(nèi)容適用于Windows XP 32位版本,但大多數(shù)內(nèi)容也適用于其他Windows 32位版本(Windows NT、Windows 2000和Windows Vista),并且可以輕松擴(kuò)展到64位版本的Windows系統(tǒng)。
11.1 中斷描述符表
在保護(hù)模式下,當(dāng)有中斷或異常發(fā)生時(shí),CPU是通過中斷描述符表(Interrupt Descriptor Table,IDT)來尋找處理函數(shù)的。因此,可以說IDT是CPU(硬件)與操作系統(tǒng)(軟件)交接中斷和異常的關(guān)口(gate)。操作系統(tǒng)在啟動(dòng)早期的一個(gè)重要任務(wù)就是設(shè)置IDT,準(zhǔn)備好處理異常和中斷的各個(gè)函數(shù)。
11.1.1 概況
簡單來說,IDT是一張位于物理內(nèi)存中的線性表,共有256個(gè)表項(xiàng)。在IA-32e(64位)模式下,每個(gè)IDT項(xiàng)的長度是16字節(jié),IDT的總長度是4096字節(jié)(4KB)。在32位模式下,每個(gè)IDT項(xiàng)的長度是8字節(jié),IDT的總長度是2048字節(jié)(2KB)。32位與64位的主要差異在于地址長度的變化,因此,下文只討論32位的情況。
IDT的位置和長度是由CPU的IDTR來描述的。IDTR共有48位,高32位是IDT的基地址,低16位是IDT的長度(limit)。LIDT(Load IDT)指令用于將操作數(shù)指定的基地址和長度加載到IDTR中,也就是改寫IDTR的內(nèi)容。SIDT(Store IDT)指令用于將IDTR的內(nèi)容寫到內(nèi)存變量中,也就是讀取IDTR的內(nèi)容。LIDT和SIDT指令只能在實(shí)模式或保護(hù)模式的高特權(quán)級(Ring 0)下執(zhí)行。在內(nèi)核調(diào)試時(shí),可以使用rigtr和rigtl命令觀察IDTR的內(nèi)容(卷1中的2.6.2節(jié))。
在Windows操作系統(tǒng)中,IDT的初始化過程大致是這樣的。IDT的最初建立和初始化工作是由Windows系統(tǒng)的加載程序(NTLDR或WinLoad)在實(shí)模式下完成的。在準(zhǔn)備好一個(gè)內(nèi)存塊后,加載程序先執(zhí)行CLI指令關(guān)閉中斷處理,然后執(zhí)行LIDT指令將IDT的位置和長度信息加載到CPU中,而后,加載程序?qū)PU從實(shí)模式切換到保護(hù)模式,并將執(zhí)行權(quán)移交給NT內(nèi)核的入口函數(shù)KiSystemStartup。接下來,內(nèi)核中的處理器初始化函數(shù)會(huì)通過SIDT指令取得IDT的信息,對其進(jìn)行必要的調(diào)整,然后以參數(shù)形式傳遞給KiInitializePcr函數(shù),后者將其記錄到描述處理器的基本數(shù)據(jù)區(qū)PCR(Processor Control Region)和Prcb(Processor control block)中。
以上介紹的過程都是發(fā)生在0號處理器中的,也就是所謂的Bootstrap Processor,簡稱BSP。因?yàn)榧词故嵌郈PU的系統(tǒng),在把NTLDR或WinLoad及執(zhí)行權(quán)移交給內(nèi)核的階段都只有BSP在運(yùn)行。在BSP完成了內(nèi)核初始化和執(zhí)行體的階段0初始化后,在階段1初始化時(shí),BSP才會(huì)執(zhí)行KeStartAllProcessors函數(shù)來初始化其他CPU。BSP之外的其他CPU一般稱為AP(Application Processor)。對于每個(gè)AP,KeStartAllProcessors函數(shù)會(huì)為其建立一個(gè)單獨(dú)的處理器狀態(tài)區(qū),包括它的IDT,然后調(diào)用KiInitProcessor函數(shù),后者會(huì)根據(jù)啟動(dòng)CPU的IDT為要初始化的AP復(fù)制一份,并做必要的修改。
在內(nèi)核調(diào)試會(huì)話中,可以使用!pcr命令觀察CPU的PCR內(nèi)容,清單11-1顯示了Windows Vista系統(tǒng)中0號CPU的PCR內(nèi)容。
清單11-1 Windows Vista系統(tǒng)中0號CPU的PCR內(nèi)容
kd> !pcr KPCR for Processor 0 at 81969a00: // KPCR結(jié)構(gòu)的線性內(nèi)存地址 Major 1 Minor 1 // KPCR結(jié)構(gòu)的主版本號和子版本號 N: 9f1d9644 // 異常處理注冊鏈表 […] // 省略數(shù)行關(guān)于NTTIB的信息 SelfPcr: 81969a00 // 本結(jié)構(gòu)的起始地址 Prcb: 81969b20 // KPRCB結(jié)構(gòu)的地址 Irql: 0000001f // CPU的中斷請求級別(IRQL) IRR: 00000000 // IDR: ffff20f0 // InterruptMode: 00000000 // IDT: 834da400 // IDT的基地址 GDT: 834da000 // GDT的基地址 TSS: 8013e000 // 任務(wù)狀態(tài)段(TSS)的地址 CurrentThread: 84af6270 // 當(dāng)前在執(zhí)行的線程,ETHREAD地址 NextThread: 00000000 // 下一個(gè)準(zhǔn)備執(zhí)行的線程 IdleThread: 8196cdc0 // IDLE線程的ETHREAD地址
內(nèi)核數(shù)據(jù)結(jié)構(gòu)KPCR描述了PCR內(nèi)存區(qū)的布局,因此也可以使用dt命令來觀察PCR,例如,kd> dt nt!_KPCR 81969a00。
11.1.2 門描述符
IDT的每個(gè)表項(xiàng)是一個(gè)所謂的門描述符(gate descriptor)結(jié)構(gòu)。之所以這樣稱呼,是因?yàn)镮DT項(xiàng)的基本用途就是引領(lǐng)CPU從一個(gè)空間到另一個(gè)空間去執(zhí)行,每個(gè)表項(xiàng)好像是一個(gè)從一個(gè)空間進(jìn)入另一個(gè)空間的大門(gate)。在穿越這扇門時(shí)CPU會(huì)做必要的安全檢查和準(zhǔn)備工作。
IDT中可以包含以下3種門描述符。
(1)任務(wù)門(task-gate)描述符:用于任務(wù)切換,里面包含用于選擇任務(wù)狀態(tài)段(TSS)的段選擇子。可以使用JMP或CALL指令通過任務(wù)門來切換到任務(wù)門所指向的任務(wù),當(dāng)CPU因?yàn)橹袛嗷虍惓^D(zhuǎn)移到任務(wù)門時(shí),也會(huì)切換到指定的任務(wù)。
(2)中斷門(interrupt-gate)描述符:用于描述中斷處理例程的入口。
(3)陷阱門(trap-gate)描述符:用于描述異常處理例程的入口。
圖11-1描述了以上3種門描述符的內(nèi)容布局。
圖11-1 IDT中的3種門描述符的內(nèi)容布局
從圖11-1可以看出,3種門描述符的格式非常相似,有很多共同的字段。其中DPL代表描述符優(yōu)先級(descriptor previlege level),用于優(yōu)先級控制,P是段存在標(biāo)志。段選擇子用于選擇一個(gè)段描述符(位于LDT或GDT中,選擇子的格式參見本書卷1的2.6.3節(jié)),偏移部分用來指定段中的偏移,二者共同定義一個(gè)準(zhǔn)確的內(nèi)存位置。對于中斷門和陷阱門,二者指定的就是中斷或異常處理例程的地址;對于任務(wù)門,它們指定的就是任務(wù)狀態(tài)段的內(nèi)存地址。
系統(tǒng)通過門描述符的類型字段,即高4字節(jié)的6~12位,來區(qū)分一個(gè)描述符的種類。例如任務(wù)門的類型是0b00101(b代表二進(jìn)制數(shù)),中斷門的類型是0b0D110,其中D位用來表示描述的是16位門(0)還是32位門(1),陷阱門的類型是0b0D111。
11.1.3 執(zhí)行中斷和異常處理函數(shù)
下面我們看看當(dāng)有中斷或異常發(fā)生時(shí),CPU是如何通過IDT尋找和執(zhí)行處理函數(shù)的。首先,CPU會(huì)根據(jù)其向量號碼和IDTR中的IDT基地址信息找到對應(yīng)的門描述符。然后判斷門描述符的類型,如果是任務(wù)描述符,那么CPU會(huì)執(zhí)行硬件方式的任務(wù)切換,切換到這個(gè)描述符所定義的線程;如果是陷阱描述符或中斷描述符,那么CPU會(huì)在當(dāng)前任務(wù)上下文中調(diào)用描述符所描述的處理例程。下面分別加以討論。
我們先來看任務(wù)門的情況。簡單來說,任務(wù)門描述的是一個(gè)TSS,CPU要做的是切換到這個(gè)TSS所代表的線程,然后開始執(zhí)行這個(gè)線程。TSS是用來保存任務(wù)信息的一段內(nèi)存區(qū),其格式是CPU所定義的。圖11-2給出了IA-32 CPU的TSS格式。從中我們看到TSS中包含了一個(gè)任務(wù)的關(guān)鍵上下文信息,如段寄存器、通用寄存器和控制寄存器,其中特別值得注意的是靠下方的SS0~SS2和ESP0~ESP2字段,它們記錄著一項(xiàng)任務(wù)在不同優(yōu)先級執(zhí)行時(shí)所應(yīng)使用的棧,SSx用來選擇棧所在的段,ESPx是棧指針值。
CPU在通過任務(wù)門的段選擇子找到TSS描述符后,會(huì)執(zhí)行一系列的檢查動(dòng)作,比如確保TSS描述符中的存在標(biāo)志是1,邊界值應(yīng)該大于0x67,B(Busy)標(biāo)志不為1等。所有檢查都通過后,CPU會(huì)將當(dāng)前任務(wù)的狀態(tài)保存到當(dāng)前任務(wù)的TSS中。然后把TSS描述符中的B標(biāo)志設(shè)置為1。接下來,CPU要把新任務(wù)的段選擇子(與門描述符中的段選擇子等值)加載到TR寄存器,然后把新任務(wù)的寄存器信息加載到物理寄存器中。最后,CPU開始執(zhí)行新的任務(wù)。
圖11-2 32位的任務(wù)狀態(tài)段(TSS)
下面通過一個(gè)小實(shí)驗(yàn)來加深大家的理解。首先,在一個(gè)調(diào)試Windows Vista的內(nèi)核調(diào)試會(huì)話中,通過ridtr命令得到系統(tǒng)IDT的基地址。
kd> r idtr idtr=834da400
因?yàn)殡p重錯(cuò)誤異常(Double Fault,#DF)通常是使用任務(wù)門來處理的,所以我們觀察這個(gè)異常對應(yīng)的IDT項(xiàng)。因?yàn)?DF異常的向量號是8,每個(gè)IDT項(xiàng)的長度是8字節(jié),所以我們可以使用如下命令顯示出8號IDT項(xiàng)的內(nèi)容。
kd> db 834da400+8*8 l8 834da440 00 00 50 00 00 85 00 00 ..P.....
其中第2、3字節(jié)(從0起,下同)組成的WORD是段選擇子,即0x0050。第5字節(jié)(0x85)是P標(biāo)志(為1)、DPL(0b00)和類型(0b00101)。
接下來使用dg命令顯示段選擇子所指向的段描述符。
kd> dg 50 P Si Gr Pr Lo Sel Base Limit Type l ze an es ng Flags ---- -------- -------- ---------- - -- -- -- -- ----------- 0050 81967000 00000068 TSS32 Avl 0 Nb By P Nl 00000089
也就是說,TSS的基地址是0x81967000,長度是0x68字節(jié)(Gran位指示By即Byte)。Type字段顯示這個(gè)段的類型是32位的TSS(TSS32),它的狀態(tài)為Available,并非Busy。
至此,我們知道了#DF異常對應(yīng)的門描述符所指向的TSS,是位于內(nèi)存地址0x81967000開始的0x68字節(jié)。使用內(nèi)存觀察命令便可以顯示這個(gè)TSS的內(nèi)容了(清單11-2)。
清單11-2 TSS的內(nèi)容
kd> dd 81967000 81967000 00000000 81964000 00000010 00000000 81967010 00000000 00000000 00000000 00122000 81967020 8193f0a0 00000000 00000000 00000000 81967030 00000000 00000000 81964000 00000000 81967040 00000000 00000000 00000023 00000008 81967050 00000010 00000023 00000030 00000000 81967060 00000000 20ac0000 00000000 81964000 81967070 00000010 00000000 00000000 00000000
參考清單11-2,從上至下,81964000是在優(yōu)先級0執(zhí)行時(shí)的棧指針,00000010是在優(yōu)先級0執(zhí)行時(shí)的棧選擇子,00122000是這個(gè)任務(wù)的頁目錄基地址寄存器(PDBR,即CR3)的值,8193f0a0是程序指針寄存器(EIP)的值,當(dāng)CPU切換到這個(gè)任務(wù)時(shí)便是從這里開始執(zhí)行的。接下來,依次是標(biāo)志寄存器(EFLAGS)和通用寄存器的值。偏移0x48字節(jié)處的0x23是ES寄存器的值,相鄰的00000008是CS寄存器的值,即這個(gè)任務(wù)的代碼段的選擇子。而后是SS寄存器的值,即棧段的選擇子,再往后是DS、FS和GS寄存器的值(0x23、0x30和0)。偏移0x64字節(jié)處的20ac0000是TSS的最后4字節(jié),它的最低位是T標(biāo)志(0),即我們在卷1的4.3.3節(jié)介紹過的TSS中的陷阱標(biāo)志。高16字節(jié)是用來定位IO映射區(qū)基地址的偏移地址,它是相對于TSS的基地址的。
使用ln命令可以觀察EIP的值對應(yīng)的就是內(nèi)核函數(shù)KiTrap08。
kd> ln 8193f0a0 (8193f0a0) nt!KiTrap08 | (8193f118) nt!Dr_kit9_a Exact matches: nt!KiTrap08 = <no type information>
也就是說,當(dāng)有#DF異常發(fā)生時(shí),CPU會(huì)切換到以上TSS所描述的線程,然后在這個(gè)線程環(huán)境中執(zhí)行KiTrap08函數(shù)。之所以要切換到一個(gè)新的線程,而不是像其他異常那樣在原來的線程中處理,是因?yàn)?DF異常指的是在處理一個(gè)異常時(shí)又發(fā)生了異常,這可能意味著本來的線程環(huán)境已經(jīng)不可靠了,所以有必要切換到一個(gè)新的線程來執(zhí)行。
類似地,代表緊急任務(wù)的不可屏蔽中斷(NMI)也是使用任務(wù)門機(jī)制來處理的。最后要說明的是,因?yàn)閤64架構(gòu)不支持硬件方式的任務(wù)切換,所以IDT中也不再有任務(wù)門了。
大多數(shù)中斷和異常是利用中斷門或陷阱門來處理的,下面我們看看這兩種情況。
首先,CPU會(huì)根據(jù)門描述符中的段選擇子定位到段描述符,然后再進(jìn)行一系列檢查,如果檢查通過,CPU就判斷是否需要切換棧。如果目標(biāo)代碼段的特權(quán)級別比當(dāng)前特權(quán)級別高(級別的數(shù)值?。?,那么CPU需要切換棧,其方法是從當(dāng)前任務(wù)的TSS中讀取新棧的段選擇子(SS)和棧指針(ESP),并將其加載到SS和ESP寄存器。然后,CPU會(huì)把被中斷過程(舊的)的棧段選擇子(SS)和棧指針(ESP)壓入新的棧。接下來,CPU會(huì)執(zhí)行如下兩項(xiàng)操作。
(1)把EFLAGS、CS和EIP的指針壓入棧。CS和EIP的指針代表了轉(zhuǎn)到處理例程前CPU正在執(zhí)行代碼的位置。
(2)如果發(fā)生的是異常,而且該異常具有錯(cuò)誤代碼(參見本書卷1的3.3.2節(jié)),那么把該錯(cuò)誤代碼也壓入棧。
如果處理例程所在代碼段的特權(quán)級別與當(dāng)前特權(quán)級別相同,那么CPU便不需要進(jìn)行棧切換,但仍要執(zhí)行上面的兩步操作。
TR寄存器中存放著指向當(dāng)前任務(wù)TSS的選擇子,使用WinDBG可以觀察TSS的內(nèi)容。
kd> r tr tr=00000028 kd> dg 28 P Si Gr Pr Lo Sel Base Limit Type l ze an es ng Flags ---- -------- -------- ---------- - -- -- -- -- --------- 0028 8013e000 000020ab TSS32 Busy 0 Nb By P Nl 0000008b
經(jīng)常做內(nèi)核調(diào)試的讀者可能會(huì)發(fā)現(xiàn),TR寄存器的值大多時(shí)候是固定的。也就是說,值并不隨著應(yīng)用程序的線程切換而變化。事實(shí)上,Windows系統(tǒng)中的TSS個(gè)數(shù)并不是與系統(tǒng)中的線程個(gè)數(shù)相關(guān)的,而是與CPU個(gè)數(shù)相關(guān)的。在啟動(dòng)期間,Windows系統(tǒng)會(huì)為每個(gè)CPU創(chuàng)建3~4個(gè)TSS,一個(gè)用于處理NMI,一個(gè)用于處理#DF異常,一個(gè)用于處理機(jī)器檢查異常(與版本有關(guān),在XP SP1中存在),另一個(gè)供所有Windows線程共享。當(dāng)Windows系統(tǒng)切換線程時(shí),它把當(dāng)前線程的狀態(tài)復(fù)制到共享的TSS中。也就是說,普通的線程切換并不會(huì)切換TSS,只有當(dāng)NMI或 #DF異常發(fā)生時(shí),才會(huì)切換TSS,這就是所謂的以軟件方式切換線程(任務(wù))。
11.1.4 IDT一覽
使用WinDBG的!idt擴(kuò)展命令可以列出IDT中的各個(gè)項(xiàng),不過該命令做了很多翻譯,顯示出的不是門描述符的原始格式。
lkd> !idt -a Dumping IDT: 00: 804dbe13 nt!KiTrap00 // 0號異常,即除以0 01: 804dbf6b nt!KiTrap01 02: Task Selector = 0x0058 // NMI的門描述符,顯示的是TSS的選擇子 03: 804dc2bd nt!KiTrap03
表11-1列出了典型Windows系統(tǒng)的IDT設(shè)置,對于不同的Windows版本或硬件配置不同的系統(tǒng),某些表項(xiàng)可能有所不同,但是大多數(shù)表項(xiàng)是一致的。
表11-1 IDT設(shè)置一覽(略)
在Windows XP系統(tǒng)中,處理機(jī)器檢查異常(#MC)的18號表項(xiàng)處是一個(gè)任務(wù)門描述符,指向一個(gè)單獨(dú)的TSS,對應(yīng)的處理函數(shù)是hal模塊中的HalpMcaExceptionHandlerWrapper。
11.2 異常的描述和登記
為了更好地管理異常,Windows系統(tǒng)定義了專門的數(shù)據(jù)結(jié)構(gòu)來描述異常,并定義了一系列代碼來標(biāo)識典型的異常。
在操作系統(tǒng)層次,除了CPU產(chǎn)生的異常,還有通過軟件方式模擬出的異常,比如調(diào)用RaiseException API而產(chǎn)生的異常和使用編程語言的throw關(guān)鍵字拋出的異常。為了行文方便,我們把前一類稱為CPU異常(或硬件異常),把后一類稱為軟件異常。Windows是使用統(tǒng)一的方式來描述和分發(fā)這兩類異常的。本節(jié)介紹異常的描述方式,11.3節(jié)將介紹異常的分發(fā)過程。
11.2.1 EXCEPTION_RECORD結(jié)構(gòu)
Windows系統(tǒng)使用EXCEPTION_RECORD結(jié)構(gòu)來描述異常,清單11-3給出了這個(gè)結(jié)構(gòu)的定義。
清單11-3 EXCEPTION_RECORD結(jié)構(gòu)
typedef struct _EXCEPTION_RECORD { DWORD ExceptionCode; // 異常代碼 DWORD ExceptionFlags; // 異常標(biāo)志 struct _EXCEPTION_RECORD* ExceptionRecord; // 相關(guān)的另一個(gè)異常 PVOID ExceptionAddress; // 異常發(fā)生地址 DWORD NumberParameters; // 參數(shù)數(shù)組中的元素個(gè)數(shù) ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; // 參數(shù)數(shù)組 } EXCEPTION_RECORD, *PEXCEPTION_RECORD;
其中ExceptionCode為異常代碼,是一個(gè)32位的整數(shù),其格式是Windows系統(tǒng)的狀態(tài)代碼格式,N中包含了已經(jīng)定義的所有狀態(tài)代碼,在WinBa中可以看到異常代碼只是狀態(tài)代碼的別名,例如:
#define EXCEPTION_BREAKPOINT STATUS_BREAKPOINT #define EXCEPTION_SINGLE_STEP STATUS_SINGLE_STEP
表11-2列出了常見的用于異常代碼的狀態(tài)代碼。
字段用來記錄異常標(biāo)志,它的每一位代表一種標(biāo)志,目前已經(jīng)定義的標(biāo)志位如下。
(1)EH_NONCONTINUABLE(1),該異常不可恢復(fù)繼續(xù)執(zhí)行。
(2)EH_UNWINDING(2),當(dāng)因?yàn)閳?zhí)行棧展開而調(diào)用異常處理函數(shù)時(shí),會(huì)設(shè)置此標(biāo)志。
(3)EH_EXIT_UNWIND(4),也是用于棧展開,較少使用。
(4)EH_STACK_INVALID(8),當(dāng)檢測到棧錯(cuò)誤時(shí),設(shè)置此標(biāo)志。
(5)EH_NESTED_CALL(0x10),用于標(biāo)識內(nèi)嵌的異常(第24章)。
EH_NONCONTINUABLE位用來表示該異常是否可以恢復(fù)繼續(xù)執(zhí)行,如果試圖恢復(fù)運(yùn)行一個(gè)不可繼續(xù)的異常,便會(huì)導(dǎo)致EXCEPTION_NONCONTINUABLE_EXCEPTION異常。
ExceptionRecord指針指向與該異常有關(guān)的另一個(gè)異常記錄,如果沒有相關(guān)的異常,那么這個(gè)指針便為空。
表11-2 用于的異常代碼的狀態(tài)代碼(略)
ExceptionAddress字段用來記錄異常地址,對于硬件異常,它的值因?yàn)楫惓n愋筒煌赡苁菍?dǎo)致異常的那條指令的地址,或者是導(dǎo)致異常指令的下一條指令的地址。例如,非法訪問異常(EXCEPTION_ACCESS_VIOLATION)屬于錯(cuò)誤(Fault)類異常,ExceptionAddress的值是導(dǎo)致異常的那條指令的地址。數(shù)據(jù)斷點(diǎn)觸發(fā)的調(diào)試異常屬于陷阱(Trap)類異常,ExceptionAddress的值是導(dǎo)致異常指令的下一條指令的地址。
NumberParameters是附加參數(shù)的個(gè)數(shù),即ExceptionInformation數(shù)組中包含的有效參數(shù)個(gè)數(shù),該結(jié)構(gòu)最多允許存儲(chǔ)15個(gè)附加參數(shù)。
導(dǎo)致非法訪問異常的原因主要來源于CPU的頁錯(cuò)誤異常#PF(14),但也可能是由于系統(tǒng)檢測到的其他違反系統(tǒng)規(guī)則的情況。
11.2.2 登記CPU異常
對于CPU異常,KiTrapXX例程在完成針對本異常的特別動(dòng)作后,通常會(huì)調(diào)用CommonDispatchException函數(shù),并通過寄存器將如下信息傳遞給這個(gè)函數(shù)。
(1)將唯一標(biāo)識該異常的一個(gè)異常代碼(表11-2)放入EAX寄存器。
(2)將導(dǎo)致異常的指令地址放入EBX寄存器。
(3)將其他信息作為附帶參數(shù)(最多3個(gè))分別放入EDX(參數(shù)1)、ESI(參數(shù)2)和EDI(參數(shù)3)寄存器,并將參數(shù)個(gè)數(shù)放入ECX寄存器。
CommonDispatchException被調(diào)用后,它會(huì)在棧中分配一個(gè)EXCEPTION_ RECORD結(jié)構(gòu),并把以上異常信息存儲(chǔ)到該結(jié)構(gòu)中。在準(zhǔn)備好這個(gè)結(jié)構(gòu)后,它會(huì)調(diào)用內(nèi)核中的KiDispatchException函數(shù)來分發(fā)異常。
11.2.3 登記軟件異常
下面看看軟件異常的產(chǎn)生和登記過程。簡單來說,軟件異常是通過直接或間接調(diào)用內(nèi)核服務(wù)NtRaiseException而產(chǎn)生的。
NTSTATUS NtRaiseException (IN PEXCEPTION_RECORD ExceptionRecord, IN PCONTEXT ContextRecord, IN BOOLEAN FirstChance )
用戶模式中的程序可以通過RaiseException ()API來調(diào)用這個(gè)內(nèi)核服務(wù)。RaiseException API是由KERNEL32.DLL導(dǎo)出的API,供應(yīng)用程序產(chǎn)生“自定義”的異常,其原型如下。
void RaiseException( DWORD , DWORD , DWORD , const DWORD* );
其中是異常代碼,可以是表11-2中的代碼,也可以是應(yīng)用程序自己定義的代碼。和用來定義異常的常數(shù),相當(dāng)于EXCEPTION_RECORD結(jié)構(gòu)中的和。事實(shí)上,RaiseException的實(shí)現(xiàn)也很簡單,它只是將參數(shù)放入一個(gè) EXCEPTION_RECORD 后便調(diào)用NTDLL.DLL中的RtlRaiseException()。RtlRaiseException會(huì)將當(dāng)前的執(zhí)行上下文(通用寄存器等)放入CONTEXT結(jié)構(gòu),然后通過NTDLL.DLL中的系統(tǒng)服務(wù)調(diào)用機(jī)制調(diào)用內(nèi)核中的NtRaiseException。
NtRaiseException內(nèi)部會(huì)調(diào)用另一個(gè)內(nèi)核函數(shù)KiRaiseException。
NTSTATUS KiRaiseException (IN PEXCEPTION_RECORD ExceptionRecord, IN PCONTEXT ContextRecord, IN PKEXCEPTION_FRAME ExceptionFrame, IN PKTRAP_FRAME TrapFrame, IN BOOLEAN FirstChance )
ExceptionRecord是指向異常記錄的指針,ContextRecord是指向線程上下文(CONTEXT)結(jié)構(gòu)的指針,ExceptionFrame對于x86平臺(tái)總是為NULL,TrapFrame就是棧幀的基地址,F(xiàn)irstChance表示這是該異常的第一輪(TRUE)還是第二輪(FALSE)處理機(jī)會(huì)。
內(nèi)核中的代碼可以通過RtlRaiseException(相當(dāng)于NTDLL.DLL中的版本)來調(diào)用NtRaiseException和KiRaiseException。也就是說,不論是從用戶模式調(diào)用RaiseException API,還是從內(nèi)核模式調(diào)用相應(yīng)的函數(shù),最后都會(huì)轉(zhuǎn)到KiRaiseException。
KiRaiseException內(nèi)部會(huì)通過KeContextToKframes例程把ContextRecord結(jié)構(gòu)中的信息復(fù)制到當(dāng)前線程的內(nèi)核棧,然后把ExceptionRecord 中的異常代碼的最高位清0,以便把軟件產(chǎn)生的異常與CPU異常區(qū)分開來。接下來KiRaiseException會(huì)調(diào)用KiDispatchException開始分發(fā)該異常。
對于Visual C++程序拋出的異常,比如MFC中從CException派生來的各個(gè)異常類對應(yīng)的異常,throw關(guān)鍵字直接對應(yīng)的是CxxThrowException函數(shù),CxxThrowException會(huì)調(diào)用RaiseException,并將ExceptionCode參數(shù)固定為0xe06d7363(對應(yīng)的ASCII碼為.msc)。接下來的過程與上面直接調(diào)用RaiseException的情況相同。因?yàn)镃++異常的實(shí)現(xiàn)與編譯器有關(guān),所以本書只討論使用Visual C++編譯器的情況。
.NET程序拋出的異常(CLR異常)也是通過RaiseException API產(chǎn)生的,其異常代碼固定為0xe0434f4d(對應(yīng)的ASCII碼為.COM)。
綜上所述,不論是CPU異常還是軟件異常,盡管產(chǎn)生的原因不同,但最終都會(huì)調(diào)用內(nèi)核中的KiDispatchException來分發(fā)異常,也就是說,Windows系統(tǒng)是使用統(tǒng)一的方法來分發(fā)CPU異常和軟件異常的。
11.3 異常分發(fā)過程
根據(jù)前面兩節(jié)的介紹,當(dāng)有異常發(fā)生時(shí),CPU會(huì)通過IDT找到異常處理函數(shù),即內(nèi)核中的KiTrapXX系列函數(shù),然后轉(zhuǎn)去執(zhí)行。但是,KiTrapXX函數(shù)通常只是對異常作簡單的表征和描述,為了支持調(diào)試和軟件自己定義的異常處理函數(shù),系統(tǒng)需要將異常分發(fā)給調(diào)試器或應(yīng)用程序的處理函數(shù)。對于軟件異常,Windows系統(tǒng)是以和CPU異常統(tǒng)一的方式來分發(fā)和處理的,本節(jié)將介紹分發(fā)異常的核心函數(shù)KiDispatchException和它的工作過程。
11.3.1 KiDispatchException函數(shù)
Windows內(nèi)核中的KiDispatchException函數(shù)是分發(fā)各種Windows異常的樞紐。其函數(shù)原型如下。
VOID KiDispatchException ( IN PEXCEPTION_RECORD ExceptionRecord, IN PKEXCEPTION_FRAME ExceptionFrame, IN PKTRAP_FRAME TrapFrame, IN KPROCESSOR_MODE PreviousMode, IN BOOLEAN FirstChance )
其中,參數(shù)ExceptionRecord指向的是上一節(jié)介紹的EXCEPTION_RECORD結(jié)構(gòu),用來描述要分發(fā)的異常。參數(shù)ExceptionFrame對于x86系統(tǒng)總是為NULL。參數(shù)TrapFrame指向的是 KTRAP_FRAME 結(jié)構(gòu),用來描述異常發(fā)生時(shí)的處理器狀態(tài),包括各種通用寄存器、調(diào)試寄存器、段寄存器等。參數(shù)PreviousMode 是一個(gè)枚舉類型的常量,DDK的頭文件中有這個(gè)枚舉類型的定義。
typedef enum _MODE { KernelMode, UserMode, MaximumMode} MODE;
也就是說,PreviousMode等于0表示前一個(gè)模式(通常是觸發(fā)異常代碼的執(zhí)行模式)是內(nèi)核模式,1表示用戶模式。FirstChance參數(shù)表示是否是第一輪分發(fā)這個(gè)異常。對于一個(gè)異常,Windows系統(tǒng)最多分發(fā)兩輪。
圖11-3畫出了KiDispatchException分發(fā)異常的基本過程(示意圖)。
從圖11-3中可以看到,KiDispatchException會(huì)先調(diào)用KeContextFromKframes函數(shù),目的是根據(jù)TrapFrame參數(shù)指向的KTRAP_FRAME結(jié)構(gòu)產(chǎn)生一個(gè)CONTEXT結(jié)構(gòu),以供向調(diào)試器和異常處理器函數(shù)報(bào)告異常時(shí)使用。
接下來,根據(jù)前一個(gè)模式(異常發(fā)生的模式)是內(nèi)核模式還是用戶模式,KiDispatchException會(huì)選取左右兩個(gè)流程之一來分發(fā)異常,下面我們分別作進(jìn)一步說明。
本文截選自《軟件調(diào)試 第2版 卷2 Windows平臺(tái)調(diào)試 上、下冊》
本書是國內(nèi)當(dāng)前集中介紹軟件調(diào)試主題的權(quán)威著作。本書第2 卷分為5 篇,共30 章,主要圍繞Windows系統(tǒng)展開介紹。第一篇(第1~4 章)介紹Windows 系統(tǒng)簡史、進(jìn)程和線程、架構(gòu)和系統(tǒng)部件,以及Windows系統(tǒng)的啟動(dòng)過程,既從空間角度講述Windows 的軟件世界,也從時(shí)間角度描述Windows 世界的搭建過程。第二篇(第5~8 章)描述特殊的過程調(diào)用、墊片、托管世界和Linux 子系統(tǒng)。第三篇(第9~19 章)深入探討用戶態(tài)調(diào)試模型、用戶態(tài)調(diào)試過程、中斷和異常管理、未處理異常和JIT 調(diào)試、硬錯(cuò)誤和藍(lán)屏、錯(cuò)誤報(bào)告、日志、事件追蹤、WHEA、內(nèi)核調(diào)試引擎和驗(yàn)證機(jī)制。第四篇(第20~25 章)從編譯和編譯期檢查、運(yùn)行時(shí)庫和運(yùn)行期檢查、棧和函數(shù)調(diào)用、堆和堆檢查、異常處理代碼的編譯、調(diào)試符號等方面概括編譯器的調(diào)試支持。第五篇(第26~30 章)首先縱覽調(diào)試器的發(fā)展歷史、工作模型和經(jīng)典架構(gòu),然后分別討論集成在Visual Studio 和Visual Studio(VS)Code 中的調(diào)試器,最后深度解析WinDBG 調(diào)試器的歷史、結(jié)構(gòu)和用法。
本書理論與實(shí)踐結(jié)合,不僅涵蓋了相關(guān)的技術(shù)背景知識,還深入研討了大量具有代表性的技術(shù)細(xì)節(jié),是學(xué)習(xí)軟件調(diào)試技術(shù)的珍貴資料。
本書適合所有從事軟件開發(fā)工作的讀者閱讀,特別適合從事軟件開發(fā)、測試和支持的技術(shù)人員閱讀。
1.《【0xe06d7363怎么解決】專題從操作系統(tǒng)(Windows)的角度討論中斷和異常機(jī)制》援引自互聯(lián)網(wǎng),旨在傳遞更多網(wǎng)絡(luò)信息知識,僅代表作者本人觀點(diǎn),與本網(wǎng)站無關(guān),侵刪請聯(lián)系頁腳下方聯(lián)系方式。
2.《【0xe06d7363怎么解決】專題從操作系統(tǒng)(Windows)的角度討論中斷和異常機(jī)制》僅供讀者參考,本網(wǎng)站未對該內(nèi)容進(jìn)行證實(shí),對其原創(chuàng)性、真實(shí)性、完整性、及時(shí)性不作任何保證。
3.文章轉(zhuǎn)載時(shí)請保留本站內(nèi)容來源地址,http://f99ss.com/gl/2033206.html