Windows 2000 SP4 内核调试初窥

2012年7月22日

今天又试了两个 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 老师,我现在的经理,和许多帮助过我的人。

留下您的评论