?? windows內(nèi)核調(diào)試器原理淺析.txt
字號(hào):
KdpGetVersion(&ManipulateState);
break;
case DbgKdCauseBugCheckApi:
KdpCauseBugCheck(&ManipulateState);
break;
case DbgKdPageInApi:
KdpNotSupported(&ManipulateState);
break;
case DbgKdWriteBreakPointExApi:
Status = KdpWriteBreakPointEx(&ManipulateState,
&MessageData,
ContextRecord);
if (Status) {
ManipulateState.ApiNumber = DbgKdContinueApi;
ManipulateState.u.Continue.ContinueStatus = Status;
return ContinueError;
}
break;
case DbgKdRestoreBreakPointExApi:
KdpRestoreBreakPointEx(&ManipulateState,&MessageData,ContextRecord);
break;
case DbgKdSwitchProcessor:
KdPortRestore ();
ContinueStatus = KeSwitchFrozenProcessor(ManipulateState.Processor);
KdPortSave ();
return ContinueStatus;
case DbgKdSearchMemoryApi:
KdpSearchMemory(&ManipulateState, &MessageData, ContextRecord);
break;
讀寫內(nèi)存、搜索內(nèi)存、設(shè)置/恢復(fù)斷點(diǎn)、繼續(xù)執(zhí)行、重啟等等,WinDBG里的功能是不是都能實(shí)現(xiàn)了?呵呵。
每次內(nèi)核調(diào)試器接管系統(tǒng)是通過調(diào)用在KiDispatchException里調(diào)用KiDebugRoutine(KdpTrace),但我們知道要讓系統(tǒng)執(zhí)行到KiDispatchException必須是系統(tǒng)發(fā)生了異常。而內(nèi)核調(diào)試器與被調(diào)試系統(tǒng)之間只是通過串口聯(lián)系,串口只會(huì)發(fā)生中斷,并不會(huì)讓系統(tǒng)引發(fā)異常。那么是怎么讓系統(tǒng)產(chǎn)生一個(gè)異常呢?答案就在KeUpdateSystemTime里,每當(dāng)發(fā)生時(shí)鐘中斷后在HalpClockInterrupt做了一些底層處理后就會(huì)跳轉(zhuǎn)到這個(gè)函數(shù)來更新系統(tǒng)時(shí)間(因?yàn)槭翘D(zhuǎn)而不是調(diào)用,所以在WinDBG斷下來后回溯堆棧是不會(huì)發(fā)現(xiàn)HalpClockInterrupt的地址的),是系統(tǒng)中調(diào)用最頻繁的幾個(gè)函數(shù)之一。在KeUpdateSystemTime里會(huì)判斷KdDebuggerEnable是否為TRUE,若為TRUE則調(diào)用KdPollBreakIn判斷是否有來自內(nèi)核調(diào)試器的包含中斷信息的包,若有則調(diào)用DbgBreakPointWithStatus,執(zhí)行一個(gè)int 0x3指令,在異常處理流程進(jìn)入了KdpTrace后將根據(jù)處理不同向內(nèi)核調(diào)試器發(fā)包并無限循環(huán)等待內(nèi)核調(diào)試的回應(yīng)。現(xiàn)在能理解為什么在WinDBG里中斷系統(tǒng)后堆棧回溯可以依次發(fā)現(xiàn)KeUpdateSystemTime->RtlpBreakWithStatusInstruction,系統(tǒng)停在了int 0x3指令上(其實(shí)int 0x3已經(jīng)執(zhí)行過了,只不過Eip被減了1而已),實(shí)際已經(jīng)進(jìn)入KiDispatchException->KdpTrap,將控制權(quán)交給了內(nèi)核調(diào)試器。
系統(tǒng)與調(diào)試器交互的方法除了int 0x3外,還有DbgPrint、DbgPrompt、加載和卸載symbols,它們共同通過調(diào)用DebugService獲得服務(wù)。
NTSTATUS DebugService(
ULONG ServiceClass,
PVOID Arg1,
PVOID Arg2
)
{
NTSTATUS Status;
__asm {
mov eax, ServiceClass
mov ecx, Arg1
mov edx, Arg2
int 0x2d
int 0x3
mov Status, eax
}
return Status;
}
ServiceClass可以是BEAKPOINT_PRINT(0x1)、BREAKPOINT_PROMPT(0x2)、BREAKPOINT_LOAD_SYMBOLS(0x3)、BREAKPOINT_UNLOAD_SYMBOLS(0x4)。為什么后面要跟個(gè)int 0x3,M$的說法是為了和int 0x3共享代碼(我沒弄明白啥意思-_-),因?yàn)閕nt 0x2d的陷阱處理程序是做些處理后跳到int 0x3的陷阱處理程序中繼續(xù)處理。但事實(shí)上對這個(gè)int 0x3指令并沒有任何處理,僅僅是把Eip加1跳過它。所以這個(gè)int 0x3可以換成任何字節(jié)。
int 0x2d和int 0x3生成的異常記錄結(jié)(EXCEPTION_RECORD)ExceptionRecord.ExceptionCode都是STATUS_BREAKPOINT(0x80000003),不同是int 0x2d產(chǎn)生的異常的ExceptionRecord.NumberParameters>0且ExceptionRecord.ExceptionInformation對應(yīng)相應(yīng)的ServiceClass比如BREAKPOINT_PRINT等。事實(shí)上,在內(nèi)核調(diào)試器被掛接后,處理DbgPrint等發(fā)送字符給內(nèi)核調(diào)試器不再是通過int 0x2d陷阱服務(wù),而是直接發(fā)包。用M$的話說,這樣更安全,因?yàn)椴挥谜{(diào)用KdEnterDebugger和KdExitDebugger。
最后說一下被調(diào)試系統(tǒng)和內(nèi)核調(diào)試器之間的通信。被調(diào)試系統(tǒng)和內(nèi)核調(diào)試器之間通過串口發(fā)數(shù)據(jù)包進(jìn)行通信,Com1的IO端口地址為0x3f8,Com2的IO端口地址為0x2f8。在被調(diào)試系統(tǒng)準(zhǔn)備要向內(nèi)核調(diào)試器發(fā)包之前先會(huì)調(diào)用KdEnterDebugger暫停其它處理器的運(yùn)行并獲取Com端口自旋鎖(當(dāng)然,這都是對多處理器而言的),并設(shè)置端口標(biāo)志為保存狀態(tài)。發(fā)包結(jié)束后調(diào)用KdExitDebugger恢復(fù)。每個(gè)包就象網(wǎng)絡(luò)上的數(shù)據(jù)包一樣,包含包頭和具體內(nèi)容。包頭的格式如下:
typedef struct _KD_PACKET {
ULONG PacketLeader;
USHORT PacketType;
USHORT ByteCount;
ULONG PacketId;
ULONG Checksum;
} KD_PACKET, *PKD_PACKET;
PacketLeader是四個(gè)相同字節(jié)的標(biāo)識(shí)符標(biāo)識(shí)發(fā)來的包,一般的包是0x30303030,控制包是0x69696969,中斷被調(diào)試系統(tǒng)的包是0x62626262。每次讀一個(gè)字節(jié),連續(xù)讀4次來識(shí)別出包。中斷系統(tǒng)的包很特殊,包里數(shù)據(jù)只有0x62626262。包標(biāo)識(shí)符后是包的大小、類型、包ID、檢測碼等,包頭后面就是跟具體的數(shù)據(jù)。這點(diǎn)和網(wǎng)絡(luò)上傳輸?shù)陌芟嗨啤_€有一些相似的地方比如每發(fā)一個(gè)包給調(diào)試器都會(huì)收到一個(gè)ACK答復(fù)包,以確定調(diào)試器是否收到。若收到的是一個(gè)RESEND包或者很長時(shí)間沒收到回應(yīng),則會(huì)再發(fā)一次。對于向調(diào)試器發(fā)送輸出字符串、報(bào)告SYMBOL情況等的包都是一接收到ACK包就立刻返回,系統(tǒng)恢復(fù)執(zhí)行,系統(tǒng)的表現(xiàn)就是會(huì)卡那么短短一下。只有報(bào)告狀態(tài)的包才會(huì)等待內(nèi)核調(diào)試器的每個(gè)控制包并完成對應(yīng)功能,直到發(fā)來的包包含繼續(xù)執(zhí)行的命令為止。無論發(fā)包還是收包,都會(huì)在包的末尾加一個(gè)0xaa,表示結(jié)束。
現(xiàn)在我們用幾個(gè)例子來看看調(diào)試流程。
記得我以前問過jiurl為什么WinDBG的單步那么慢(相對softICE),他居然說沒覺得慢?*$&$^$^(&(&(我ft。。。現(xiàn)在可以理解為什么WinDBG的單步和從操作系統(tǒng)正常執(zhí)行中斷下來為什么那么慢了。單步慢是因?yàn)槊繂尾揭淮纬吮匾奶幚硗猓€得從串行收發(fā)包,怎么能不慢。中斷系統(tǒng)慢是因?yàn)橹挥械鹊綍r(shí)鐘中斷發(fā)生執(zhí)行到KeUpdateSystemTime后被調(diào)試系統(tǒng)才會(huì)接受來自WinDBG的中斷包。現(xiàn)在我們研究一下為什么在KiDispatchException里不能下斷點(diǎn)卻可以用單步跟蹤KiDispatchException的原因。如果在KiDispatchException中某處下了斷點(diǎn),執(zhí)行到斷點(diǎn)時(shí)系統(tǒng)發(fā)生異常又重新回到KiDispatchException處,再執(zhí)行到int 0x3,如此往復(fù)造成了死循環(huán),無法不能恢復(fù)原來被斷點(diǎn)int 0x3所修改的代碼。但對于int 0x1,因?yàn)樗囊鹗且驗(yàn)镋FLAG寄存中TF位被置位,并且每次都自動(dòng)被復(fù)位,所以系統(tǒng)可以被繼續(xù)執(zhí)行而不會(huì)死循環(huán)。現(xiàn)在我們知道了內(nèi)部機(jī)制,我們就可以調(diào)用KdXXX函數(shù)實(shí)現(xiàn)一個(gè)類似WinDBG之類的內(nèi)核調(diào)試器,甚至可以替換KiDebugRoutine(KdpTrap)為自己的函數(shù)來自己實(shí)現(xiàn)一個(gè)功能更強(qiáng)大的調(diào)試器,呵呵。
SoftICE
SoftICE的原理和WinDBG完全不一樣。它通過替換正常系統(tǒng)中的中斷處理程序來獲得系統(tǒng)的控制權(quán),也正因?yàn)檫@樣它才能夠?qū)崿F(xiàn)單機(jī)調(diào)試。它的功能實(shí)現(xiàn)方法很底層,很少依賴與windows給的接口函數(shù),大部分功能的實(shí)現(xiàn)都是靠IO端口讀寫等來完成的。
SoftICE替換了IDT表中以下的中斷(陷阱)處理程序:
0x1: 單步陷阱處理程序
0x2: NMI不可屏蔽中斷
0x3: 調(diào)試陷阱處理程序
0x6: 無效操作碼陷阱處理程序
0xb: 段不存在陷阱處理程序
0xc: 堆棧錯(cuò)誤陷阱處理程序
0xd: 一般保護(hù)性錯(cuò)誤陷阱處理程序
0xe: 頁面錯(cuò)誤陷阱處理程序
0x2d: 調(diào)試服務(wù)陷阱處理程序
0x2e: 系統(tǒng)服務(wù)陷阱處理程序
0x31: 8042鍵盤控制器中斷處理程序
0x33: 串口2(Com2)中斷處理程序
0x34: 串口1(Com1)中斷處理程序
0x37: 并口中斷處理程序
0x3c: PS/2鼠標(biāo)中斷處理程序
0x41: 未使用
(這是在PIC系統(tǒng)上更換的中斷。如果是APIC系統(tǒng)的話更換的中斷號(hào)有不同,但同樣是更換這些中斷處理程序)
其中關(guān)鍵是替換了0x3 調(diào)試陷阱處理程序和0x31 i8042鍵盤中斷處理驅(qū)動(dòng)程序(鍵盤是由i8042芯片控制的),SoftICE從這兩個(gè)地方獲取系統(tǒng)的控制權(quán)。
啟動(dòng)softICE服務(wù)后SoftICE除了更換了IDT里的處理程序,還有幾點(diǎn)重要的,一是HOOK了i8042prt.sys里的READ_PORT_UCHAR函數(shù),因?yàn)樵趯?x60端口讀后,會(huì)改變0x64端口對應(yīng)控制寄存器的狀態(tài)。所以在SoftICE的鍵盤中斷控制程序讀了0x60端口后并返回控制權(quán)給正常的鍵盤中斷控制程序后,不要讓它再讀一次。還有就是把物理內(nèi)存前1MB的地址空間通過調(diào)用MmMapIoSpace映射到虛擬的地址空間里,里面包括顯存物理地址,以后重畫屏幕就通過修改映射到虛擬地址空間的這段顯存內(nèi)容就行了。
如果顯示模式是彩色模式,那么顯存起始地址是0xb8000,CRT索引寄存器端口0x3d4,CRT數(shù)據(jù)寄存器端口0x3d5。如果顯示模式是單色模式,那么顯存起始地址是0xb0000,CRT索引寄存器端口0x3b4,CRT數(shù)據(jù)寄存器端口0x3b5。首先寫索引寄存器選擇要進(jìn)行設(shè)置的顯示控制內(nèi)部寄存器之一(r0-r17),然后將參數(shù)寫到其數(shù)據(jù)寄存器端口。
i8042鍵盤控制器中斷控制驅(qū)動(dòng)程序在每按下一個(gè)鍵和彈起一個(gè)鍵都會(huì)被觸發(fā)。SoftICE在HOOK了正常的鍵盤中斷控制程序獲得系統(tǒng)控制權(quán)后,首先從0x60端口讀出按下鍵的掃描碼然后向0x20端口發(fā)送通用EOI(0x20)表示中斷已結(jié)束,如果沒有按下激活熱鍵(ctrl+d),則返回正常鍵盤中斷處理程序。如果是按下熱鍵則會(huì)判斷控制臺(tái)(就是那個(gè)等待輸入命令的顯示代碼的黑色屏幕)是否被激活,未被激活的話則先激活。然后設(shè)置IRQ1鍵盤中斷的優(yōu)先級為最高,同時(shí)設(shè)置兩個(gè)8259A中斷控制器里的中斷屏蔽寄存器(向0x21和0xa1發(fā)中斷掩碼,要屏蔽哪個(gè)中斷就把哪一位設(shè)為1),只允許IRQ1(鍵盤中斷)、IRQ2(中斷控制器2級聯(lián)中斷,因?yàn)镻S/2鼠標(biāo)中斷是歸8259A-2中斷控制器管的,只有開放IRQ2才能響應(yīng)來自8259A-2管理的中斷)、IRQ12(PS/2鼠標(biāo)中斷,如果有的話),使系統(tǒng)這時(shí)只響應(yīng)這3個(gè)中斷。新的鍵盤和鼠標(biāo)中斷處理程序會(huì)建立一個(gè)緩沖區(qū),保存一定數(shù)量的輸入掃描信息。當(dāng)前面的工作都完成后會(huì)進(jìn)入一段循環(huán)代碼,負(fù)責(zé)處理鍵盤和鼠標(biāo)輸入的掃描碼緩沖區(qū),同時(shí)不斷地更新顯存的映射地址緩沖區(qū)重畫屏幕(這段循環(huán)代碼和WinDBG里循環(huán)等待從串口發(fā)來的包的原理是一樣的,都是在后臺(tái)循環(huán)等待用戶的命令)。這段循環(huán)代碼是在激活控制臺(tái)的例程里調(diào)用的,也就是說當(dāng)控制臺(tái)已被激活的話正常流程不會(huì)再次進(jìn)入這段循環(huán)代碼的(廢話,再進(jìn)入系統(tǒng)不就死循環(huán)了)。當(dāng)有一個(gè)新的鍵按下時(shí),都會(huì)重新調(diào)用一遍鍵盤中斷處理程序,因?yàn)榭刂婆_(tái)已激活,所以它只是簡單地更新鍵盤輸入緩沖區(qū)內(nèi)容然后iret返回。它并不會(huì)返回正常的鍵盤中斷處理程序,因?yàn)槟菢訒?huì)交出控制權(quán)(想證明這點(diǎn)也很簡單,在SoftICE里斷正常的鍵盤中斷處理程序,然后g,1秒后在這里斷下,這是我們可以F10,如果SoftICE會(huì)把控制權(quán)交給正常的鍵盤中斷處理程序的話,在這里早就發(fā)生死循環(huán)了)。鼠標(biāo)中斷驅(qū)動(dòng)也是一樣。這個(gè)時(shí)候?qū)嶋Hiret返回到的還是那段循環(huán)代碼里面,所以被調(diào)試的代碼并不會(huì)被執(zhí)行,除非按下了F10之類的鍵,它會(huì)指示退出循環(huán)返回最開始時(shí)的中斷處理程序,然后再iret返回最開始中斷的地方。當(dāng)然,因?yàn)樵O(shè)置了EFLAG里的TF位,執(zhí)行了一個(gè)指令又會(huì)通過單步的處理程序進(jìn)入那段循環(huán)的代碼。
而處理int 0x3也差不多,若沒有激活控制臺(tái)則先激活并屏蔽除了鍵盤、鼠標(biāo)及8259A-2中斷控制器外的所有中斷,然后進(jìn)入那段循環(huán)代碼。
作為對比同樣來看一下在SoftICE里處理int 0x3和單步的過程。當(dāng)執(zhí)行到int 0x3時(shí),激活控制臺(tái)并屏蔽中斷,然后將int 0x3指令前后范圍的指令反匯編并寫入顯存映射地址空間,并把最新的寄存器值也寫進(jìn)去,最后在后臺(tái)循環(huán)等待鍵盤輸入命令。當(dāng)命令是F10時(shí),設(shè)置好EFLAG的TF位,清除8259A中斷控制器里的中斷屏蔽寄存器,開放所有中斷,將控制臺(tái)清除,從循環(huán)代碼中返回新鍵盤(或int 0x3)中斷處理程序,然后再返回到正常鍵盤(或int 0x3)中斷處理程序,由這里iret到被中斷代碼處執(zhí)行。執(zhí)行了一個(gè)指令后因?yàn)榘l(fā)生單步異常又進(jìn)入后臺(tái)循環(huán)代碼。
SoftICE里的單步比WinDBG要快得多的原因很簡單,SoftICE只需要把反匯編出來的代碼和數(shù)據(jù)經(jīng)過簡單處理再寫入顯存映射地址緩沖區(qū)里刷新屏幕就可以繼續(xù)執(zhí)行了,省略了串行的發(fā)包收包,怎么會(huì)不快。而中斷系統(tǒng)更快,按下鍵中斷就會(huì)發(fā)生,根本不用象WinDBG等時(shí)鐘中斷才能把系統(tǒng)斷下來
?? 快捷鍵說明
復(fù)制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號(hào)
Ctrl + =
減小字號(hào)
Ctrl + -