<2024年4月>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

文章分类

导航

订阅

调试笔记:双误异常导致的蓝屏(一)

网球里有个术语叫双误,英文叫double fault。简单说,就是发球失误了一次之后,第二次发还失误。一次失误可以理解,第二次还失误,那么只好不客气了。类似的,在IA-32 CPU中,也有个双误,double fault,是一种异常,向量号是8,属于终止类(Abort)异常,一旦发生,问题就很严重。

老雷不会网球,因此下面讨论的都是计算机中的双误。

简单来说,双误就是发生了一个比较严重的异常后,还没处理完,就又发生了第二个比较严重的异常。用句成语,就是一波未平一波又起。为什么要在异常前面加上一个“比较严重”的限定呢?因为对于不严重的异常,那么CPU是可以分而治之,逐一把它们搞定的,只有当CPU无法逐一处理时,它才会抛出双误异常,意思是,“这他妈的也太糟糕了,按下葫芦起来瓢,老子不干了”。

那么到底哪些异常比较严重呢?这在IA-32手册中是有明确定义的。在老雷的《<软件调试>补编》中也有中文的解释,即表C-1和表C-2,摘录如下:

看了这两张表,清楚了吧?举例来说,如果两次都是良性异常,那么没有问题,不会产生双误。但是如果两次都是页错误,那么就双误了。

如果这样解释,还不够深入,那么给大家分析一个真实的例子——双误导致的系统崩溃,蓝屏。

先说明一下,这是一位朋友发过来的真实转储,为了不泄露朋友的“秘密”,以下分析中将故意隐藏掉或者替换一些具体的信息。

打开转储文件,除了司空见惯的目标信息外,WinDBG还给出了一个警告:

WARNING: Process directory table base 0FF2F000 doesn't match CR3 00030000

意思是当前进程的页目录基地址和CR3寄存器的内容不一致,前者是0FF2F000,后者是00030000。

执行一下!process命令,可以看到当前进程是CSRSS,它的页目录基地址是0FF2F000,即:

0: kd> !process
PROCESS 885ba400  SessionId: 0  Cid: 00e0    Peb: 7ffdf000  ParentCid: 00c4
    DirBase: 0ff2f000  ObjectTable: 885ba988  TableSize:  25.
    Image: csrss.exe
    VadRoot 885ba948 Clone 0 Private 55. Modified 573. Locked 0.

...

执行一下r  cr3,观察CR3寄存器的值,真的是00030000

0: kd> r cr3
cr3=00030000

二者真的不一样。这正常么?不正常,CR3寄存器就是用来存放当前进程的页目录基地址的,因此,正常情况下,二者应该是一致的。现在不一致了,说明系统已经切换了CR3寄存器的内容,但是还没有切换当前进程。

事实上,这个不正常情况恰恰是双误异常的典型症状。直接了当的说,在发生双误异常后,CPU通过硬件实现的任务切换机制,切换到了一个新的专门用来处理双误异常的线程,这个线程开始执行后,便发起蓝屏了,操作系统没有机会来切换当前进程,所以当前进程还是旧的。

新的线程是在系统进程中执行的,因此,CR3中包含的应该是系统进程的页目录基地址,不信你看:

0: kd> !process 8 1
Searching for Process with Cid == 8
PROCESS 890598a0  SessionId: 0  Cid: 0008    Peb: 00000000  ParentCid: 0000
    DirBase: 00030000  ObjectTable: 89095308  TableSize:  55.
    Image: System

重复一遍,当双重发生后,CPU切换把CR3寄存器切换到00030000,也就是系统进程所使用的页目录基地址,然后在新的线程中开始执行处理双误的代码,让系统崩溃和保存转储信息。因为是硬件做的线程切换,所以操作系统没有来得及更新当前进程。

顺便说下,当前进程信息是如何保存的呢?首先,在CPU的控制区(即FS寄存器所指向的段)中保存着每个CPU的当前线程结构(KTHREAD);在线程结构中,有这个线程所属进程的进程结构,即EPROCESS。

举例来说,PsGetCurrentProcessId函数就是用类似的方法找到进程ID的:

0: kd> u nt!PsGetCurrentProcessId
nt!PsGetCurrentProcessId:
80455ab4 64a124010000    mov     eax,dword ptr fs:[00000124h]
80455aba 8b80e0010000    mov     eax,dword ptr [eax+1E0h]
80455ac0 c3              ret

0: kd> dd fs:[00000124h] l1
0030:00000124  885bbda0

0: kd> dd 885bbda0+44  l1
885bbde4  885ba400

其中885ba400,正是CSRSS的进程结构。

现在,大家应该明白了那个警告的含义。

接下来该如何办呢?看一下栈回溯吧。

0: kd> kvn
 # ChildEBP RetAddr  Args to Child             
00 00000000 aebc6af2 f000e2c3 f000eef3 f000eef3 nt!KiTrap08+0x41 (FPO: TSS 28:0)
WARNING: Stack unwind information not available. Following frames may be wrong.
01 f655d7dc 00000000 f655d7ec f655e09c 4d4c4b48 HAIBAO+0x3af2

#号栈帧显示KiTrap08在执行,它就是负责处理双误异常的内核函数,也是CPU通过硬件机制切换到新线程时所用的线程入口函数。从它的当前执行点反向汇编:

0: kd> ub nt!KiTrap08+0x41
nt!KiTrap08+0x2a:
80468382 812424ffbfffff  and     dword ptr [esp],0FFFFBFFFh
80468389 9d              popfd
8046838a 6a00            push    0
8046838c 6a00            push    0
8046838e 6a00            push    0
80468390 6a08            push    8
80468392 6a7f            push    7Fh
80468394 e8d928fcff      call    nt!KeBugCheckEx (8042ac72)

可以看到,这一点其实就是调用蓝屏函数的call指令的下一条指令。

接下来需要注意#0号栈帧后面括号里的信息:

(FPO: TSS 28:0)

这就是所谓的任务门(Task Gate)信息。啥叫任务门,简单说是登记在IDT表中的一种特殊入口,CPU可以根据这个门所指向的任务状态段(TSS)来切换到指定的任务(线程)。比如,CPU在发生双误时,就是通过IDT表中8号表项处的任务门,来切换到处理双误异常的新线程的。

观察IDT表的8号表项,或者通过!pcr命令得到当前线程(即双误线程)的TSS,然后观察其内容:

0: kd> dt _KTSS 80474850
nt!_KTSS
   +0x000 Backlink         : 0x28
   +0x002 Reserved0        : 0
   +0x004 Esp0             : 0x80471850
   +0x008 Ss0              : 0x10
   +0x00a Reserved1        : 0
   +0x00c NotUsed1         : [4] 0
   +0x01c CR3              : 0x30000
   +0x020 Eip              : 0x80468358
   +0x024 NotUsed2         : [9] 0
   +0x048 Es               : 0x23
   +0x04a Reserved2        : 0
   +0x04c Cs               : 8
   +0x04e Reserved3        : 0
   +0x050 Ss               : 0x10
   +0x052 Reserved4        : 0
   +0x054 Ds               : 0x23
   +0x056 Reserved5        : 0
   +0x058 Fs               : 0x30
   +0x05a Reserved6        : 0
   +0x05c Gs               : 0
   +0x05e Reserved7        : 0
   +0x060 LDT              : 0
   +0x062 Reserved8        : 0
   +0x064 Flags            : 0
   +0x066 IoMapBase        : 0x20ac
   +0x068 IoMaps           : [1] _KiIoAccessMap
   +0x208c IntDirectionMap  : [32]  ""

可以看到其CR3寄存器的内容就是0x30000。另外注意它的Backlink字段,这个字段代表着CPU所执行的前一个任务,或者说是从哪个线程切换到当前线程的。在IA-32手册里,这个地段被称为Previous task link field。其内容是前一个任务的TSS段的段选择子。

说到这,大家该知道,上面的(FPO: TSS 28:0)信息的含义和来历了。其含义是,前一个线程的TSS段选择子是0x28,数据的来源是当前线程的TSS段中的Previous task link field字段。

有了这个信息后,我们就可以使用.tss命令轻松的切换到前一个线程了。也就是出问题的那个线程,这对于找到这次崩溃的根源有着决定性的意义,因为那里有导致崩溃的第一现场。切换过去的情况怎么样呢?老雷要吃晚饭了,今天就写到这里,下一次接着写!

posted on 2010年3月28日 18:41 由 Raymond

# re: 调试笔记:双误异常导致的蓝屏(一) @ 2010年3月31日 15:06

翹首期盼!
不知道張老師的下一篇文,是什麼時候?

chhzh

# re: 调试笔记:双误异常导致的蓝屏(一) @ 2010年4月2日 12:42

to chhzh, 多谢留言,这一周在外travel了,这两天就写

Raymond

# re: 调试笔记:双误异常导致的蓝屏(一) @ 2011年1月7日 14:43

内核态下fs段这个地方是个什么样的结构,我尝试使用
dg @fs
然后
dt _kthread ffdff000 (dg显示出的段值)
发现是这个定义,
+0x124 Affinity : 0x8054a6a0
好奇怪,

记得在用户态下的定义很清楚,fs就是teb的地址。
dt _nt_tib 7ffdf000
+0x018 Self : 0x7ffdf000 _NT_TIB
18这个地方的指针就是self自己。

rong_bo

# re: 调试笔记:双误异常导致的蓝屏(一) @ 2011年1月9日 8:46

明白了,内核下FS是KPCR的结构,末尾处是KPRCB结构,其中包含了KTHREAD的指针。

rong_bo

Powered by Community Server Powered by CnForums.Net