<2019年7月>
30123456
78910111213
14151617181920
21222324252627
28293031123
45678910

文章分类

导航

订阅

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

书接上回,今天继续分析双误异常导致的蓝屏。上次说到因为发生双误,CPU动用硬件的任务切换机制,切换到了专门用于处理双误异常的新线程。在分析了TSS结构和栈回溯中的TSS信息后,我们找到了切换回本来触发双误线程的方法,也就是执行.tss 28命令。

0: kd> .tss 28
eax=f655d5d4 ebx=af0d4180 ecx=00000000 edx=af0d2081 esi=80460dd0 edi=f655d7d4
eip=aebc6af2 esp=f655cfb8 ebp=f655d7dc iopl=0     vif nv up ei ng nz ac pe nc
cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00090296
HAIBAO+0x3af2:
aebc6af2 55              push    ebp

上面的寄存器信息是CPU在切换任务时帮我们保存在TSS段里的,因此它代表了这个导致问题的线程的最终状态。其中每个字节的信息都是非常宝贵的。

先看程序指针EIP,它的值指向HAIBAO模块(问题中的驱动程序)的一条push指令。难道是这条push指令导致的双误么?

一般来说,EIP指针原则上是指向将要执行的那条指令,但是当CPU执行一条指令时遇到错误类异常,比如页错误,那么它会把程序指针重现指向这条导致异常的指令的开始处。因此,这条push指令是否执行过,是需要其它信息来确定的。

反汇编它的前一条指令,结果是:

aebc6aec 81ec08040000    sub     esp,408h

这条指令导致双误的概率更低了,指令本身根本不会导致异常,除非从内存中读取这条指令时导致页错误,但是现在能顺利从转储文件中读到这条指令,基本可以否定这一点了。

既然前一条指令不会导致双误,那么这条push指令的可能性便提高了。压栈指令怎么会导致双误呢?

典型的原因便是栈溢出。

看一下,当前的栈指针ESP,其值为f655cfb8。怎么知道这个栈指针是否还指向有效的栈空间呢?

简单的方法就是观察线程信息中的栈边界,使用!thraed命令:

 0: kd> !thread
THREAD 885bbda0  Cid e0.dc  Teb: 7ffde000  Win32Thread: e23cd4e8 RUNNING
IRP List:
    89016008: (0006,00dc) Flags: 00000070  Mdl: 00000000
Not impersonating
Owning Process 885ba400
Wait Start TickCount    2147          Elapsed Ticks: 1
Context Switch Count    178                   LargeStack
UserTime                  0:00:00.0000
KernelTime                0:00:00.0109
Start Address csrss!NtProcessStartup (0x5fff1130)
Stack Init f6560000 Current f655de8c Base f6560000 Limit f655d000 Call 0
Priority 13 BasePriority 13 PriorityDecrement 0 DecrementCount 0

可见,这个线程的基地址是f6560000,边界是f655d000,栈空间是向低地址方向生长的,现在的栈指针显然已经出界了。已经超出了72个字节:

0: kd> ? f655cfb8-f655d000
Evaluate expression: -72 = ffffffb8

使用dd命令观察,也可以看的出:

0: kd> dd f655cfb8
f655cfb8  ???????? ???????? ???????? ????????
f655cfc8  ???????? ???????? ???????? ????????
f655cfd8  ???????? ???????? ???????? ????????
f655cfe8  ???????? ???????? ???????? ????????
f655cff8  ???????? ???????? 00000000 00000000
f655d008  00000000 00000000 00000000 00000000
f655d018  00000000 00000000 00000000 00000000
f655d028  00000000 00000000 00000000 00000000

显示为?号的部分就是超出边界的部分。分析到这里,大家可能恍然大悟了。回放一下出问题的经过。在执行当前函数的父函数时,一切似乎还都很正常,它为了执行某项功能而调用当前函数使用了FPO优化没有建立栈帧基地址(根据符号和反汇编可以知道),一上来就调整栈指针,为当前函数分配栈帧空间,即我们看到的那条减法指令:

aebc6aec 81ec08040000    sub     esp,408h

这一调整,其实已经把ESP指向悬崖外面了,不过问题并没有立刻暴漏出来,得等有人走过来才会发现。接下来的push指令就是这个“倒霉蛋”。它试图向栈里压入4个字节,CPU接到命令,执行这条压栈指令,将栈指针的值减4得到目标地址,然后试图将数据写到这个地址,但是要写的这个地方已经是上不着天下不着地了,于是因为要写的内存无效而发生页错误异常。

发生异常后,CPU试图转去执行异常处理例程,但是转移前需要将当前的部分寄存器信息压入到栈中,比如EIP,于是再次执行压栈动作,尽管这个压栈动作是CPU内部逻辑来做的,但是因为栈上已经没有有效空间了,所以也难逃异常命运,于是再次导致异常。连续两次页错误异常后,CPU“火大了”——不,其实是有准备的,意识到了在当前线程中折腾已经没有意义了,于是动用硬件的任务切换机制切换一个崭新的线程。

在每次页错误异常时,CPU会把访问的地址写到cr2寄存器中,读取这个寄存器:

0: kd> r cr2
Last set context:
cr2=f655cfb4

正是当前栈指针f655cfb8-4的位置。可怜的CPU就是向这个无效的地址猛撞了两次后而绝望的。

把当前的栈指针+408可以追溯父函数有关的信息:

0: kd> dd f655cfb8+408
f655d3c0  aebc6eac f655d5d4 aebc3e20 f655e0a0
f655d3d0  f655e00c 00000000 00000000 00000000
f655d3e0  00000000 00000000 00000000 00000000

其中的aebc6eac就是当前函数的返回地址,是CALL指令自动压到栈上的。此后的f655d5d4应该是参数。

0: kd> ln aebc6eac
d:\work\rtm\haibao\src\...\bug.c(160)+0xc
(aebc6da8)   HAIBAO!IsXXXX+0x104   |  (aebc70ee)   HAIBAO!IsXXX

使用ln命令借助调试符号可以得到当前函数和父函数的名称,知道了函数名后,便可以修改代码了。怎么修改代码呢?观察反汇编或者源代码都可以看到,问题中的函数分配很多512字节的局部变量,过度使用内核栈了,应该改为从内核池动态分配。

故事讲完了,感想如何?经常做用户态开发的朋友们是不是还难以理解啊,内核态的栈怎么那么小啊?是的啊。

posted on 2010年4月5日 23:36 由 Raymond

# re: 调试笔记:双误异常导致的蓝屏(二) @ 2010年4月6日 14:00

學習了,謝謝張老師的精彩分析,看來是要多動一下調試器,調試才能洞察真理。

chhzh

# re: 调试笔记:双误异常导致的蓝屏(二) @ 2011年3月23日 23:18

最近也经常被这种蓝屏所困,不知如何着手分析,今天看到张老师这篇详细的分析,受益匪浅。希望今后能多阅读到张老师这样的文章,对于提高Debug能力很有帮助,在这先谢谢啦^_^

mr6698

Powered by Community Server Powered by CnForums.Net