Windows 2000 SP4 内核调试初窥
今天又试了两个 Windows 版本。首先尝试的是虚拟机上安装的 Windows 2000 RTM,也是用 WinDbg 连接到它的 COM2 口的命名管道上进行调试(在 Virtual PC 上指定 COM2 为 \\.\pipe\nt5com2,并在 WinDbg 中连接它)。发现系统已经比昨天调试 NT 4 的情况要好了——系统已经能正常运行了(我猜测是 NT 4 也许要用它那个老版本的 i386kd.exe 才能正常调试,只是个猜测,但那个命令是否支持命名管道还是个疑问,所以放弃了)。但是,在 WinDbg 中 dt nt!_EPROCESS 仍然拿不到任何数据,依然是找不到符号。键入 lm 显示,WinDbg 已经从 Microsoft Symbol Server 上拿到对应的符号文件(PDB)。换用从 Microsoft 网站上下载到的 Windows 2000 RTM Symbol Package 并重新加载,依然没有 _EPROCESS。
然后,试着把 Windows 2000 RTM 升级到 SP4,再作试验。结果,试验成功。
此处先推荐一个网页:http://www.codeproject.com/Articles/7913/Debug-Tutorial-Part-6-Navigating-The-Kernel-Debugg
下面,我记录一下我初步认识 _EPROCESS(执行体进程,Executive Process)结构的一些经历。执行体进程结构是 Windows 执行体用来表示进程的结构。
1. !process 是个扩展命令。它有一个语法 !process 0 <Flags>。其中 <Flags> 可以是 0,表示只显示最基本的信息。!process 0 0 将列出所有进程的 _EPROCESS 结构的地址。
2. 随便抓一个进程,例如我们选择位于 0x81231580 地址(虚拟内存地址)的 sol.exe(Solitaire,纸牌,你懂的)。如果这个程序并没有运行的话,你可以先在 WinDbg 中键入 g,让系统运行,启动这个进程,然后再重新让 WinDbg break(打断执行)。
PROCESS 81231580 SessionId: 0 Cid: 0228 Peb: 7ffdf000 ParentCid: 0128 DirBase: 07c6c000 ObjectTable: 81253108 TableSize: 23. Image: sol.exe
3. 使用 dt _EPROCESS 81231580 来查看它的 _EPROCESS 结构。_EPROCESS 结构很大,所以这里只列几个有趣的项:
nt!_EPROCESS +0x000 Pcb : _KPROCESS ... +0x09c UniqueProcessId : 0x00000228 ... +0x1b0 Peb : 0x7ffdf000 _PEB ... +0x1fc ImageFileName : [16] "sol.exe"
4. 当我们知道结构中域的名字的时候,我们可以只列一个域。例如,dt _EPROCESS Pcb 81231580,即可列出 Pcb 域的内容。这里面每一行 + 右边的数字是结构中的偏移量(16 进制)。再右边是类型。可以看到,Pcb 是 _KPROCESS(内核进程)类型,所以如果对它的地址(即 81231580 + 000 = 81231580)显示 _KPROCESS,就能得到 _KPROCESS 结构的内容。键入:dt _KPROCESS 81231580。
5. ActiveProcessLinks 域是一个链接表。它是怎样的结构呢?我分析了一下,发现它有两个域。
+0x0a0 ActiveProcessLinks : _LIST_ENTRY [ 0x8046e460 - 0x81245a80 ]
5.1 先计算基址加偏移量。
kd> dt _LIST_ENTRY 81231620 nt!_LIST_ENTRY [ 0x8046e460 - 0x81245a80 ] +0x000 Flink : 0x8046e460 _LIST_ENTRY [ 0x814a71c0 - 0x81231620 ] +0x004 Blink : 0x81245a80 _LIST_ENTRY [ 0x81231620 - 0x812496a0 ]
5.2 再查看它的内容。
kd> dt _LIST_ENTRY 81231620 nt!_LIST_ENTRY [ 0x8046e460 - 0x81245a80 ] +0x000 Flink : 0x8046e460 _LIST_ENTRY [ 0x814a71c0 - 0x81231620 ] +0x004 Blink : 0x81245a80 _LIST_ENTRY [ 0x81231620 - 0x812496a0 ]
5.3 可见,它等效于 C/C++ 中的结构体:
struct _LIST_ENTRY { DWORD_PTR Flink; DWORD_PTR Blink; };
5.4 其中,Flink 的值是 8046e460。Blink 是 81245a80。那么,它们到底链接到哪里呢?我仔细想了想,它的前后链接所指的地址上也是个 _LIST_ENTRY 结构。猜一下,后来发现果然猜中了:它链接的是 _EPROCESS 结构体中的 ActiveProcessLinks 域。也就是说,如果 81245a80 – a0,就是另一个 _EPROCESS 的地址。我们可以验证一下:
kd> ?? 0x81245a80 - 0xa0 unsigned int 0x812459e0 kd> dt _EPROCESS 812459e0 ... +0x09c UniqueProcessId : 0x000002f8 ... +0x1fc ImageFileName : [16] "internat.exe" ...
有趣的是 FLink 8046e460 – a0 如果显示它为 _EPROCESS 的话,看出来像是个空进程。可能是因为 sol.exe 是我最后启动的一个进程的缘故吧。
好了,时间有限,浅尝辙止,以后有机会继续。
====
第二次尝试:断点 KiRetireDpcList。该函数的任务是把等待处理的 DPC 任务交付给目标函数,让它们处理(若有讹误敬请指正)。客户机仍然是单 CPU 的。步骤:
1. 先在不设断点的情况下让系统运行起来。即使用 g 命令。
2. 然后在客户机内运行一个死循环程序。这个的做法就仁者见仁智者见智了。
3. WinDbg 内菜单 Debug -> Break。
4. 然后设置断点:
kd> bp nt!KiRetireDpcList kd> g
5. 过一会儿就会运行到断点。再查看调用栈:
kd> kb ChildEBP RetAddr Args to Child f7423ff4 804042be be20fd44 00000000 00000000 nt!KiRetireDpcList f7423ff8 be20fd44 00000000 00000000 00000000 nt!KiDispatchInterrupt+0x2a WARNING: Frame IP not in any known module. Following frames may be wrong. 804042be 00000000 00000008 bb835275 00000128 0xbe20fd44
6. 多尝试几次。基本上都是这个位置。KiDispatchInterrupt 也许是在中断发生时把任务分发到实际的中断处理程序的这位。
7. 禁用断点。然后运行。
kd> bl 0 e 80464ba4 0001 (0001) nt!KiRetireDpcList kd> bd 0 kd> g
8. 在客户机内把死循环程序关闭。
9. 再使用 Debug -> Break 菜单项把机器中止。
10. 执行 be 0(启用断点)。再执行 g(运行)。
11. 然后会运行到断点。查看它的调用栈:
kd> kb ChildEBP RetAddr Args to Child 80473b64 80464b6f 0000000e 00000000 00000000 nt!KiRetireDpcList 80473b68 00000000 00000000 00000000 00000000 nt!KiIdleLoop+0x26
结论:可见在忙碌时,KiRetireDpcList 是通过中断来调用的,而空闲时,KiRetireDpcList 是通过空闲线程来调用的。
鸣谢:潘老师,高老板,Mark Russinovich 博士,John Robbins 老师,我现在的经理,和许多帮助过我的人。