最近,遇到了一个由于递归导致的卡死问题。这个问题非常有意思,值得总结。
你知道什么情况下无限递归会卡死,而不崩溃吗?你知道递归层数过多时,如何找到导致递归调用的函数吗?你知道如何快速找到关键线程吗?你知道如何附加到一个正在被调试的进程吗?你知道如何在 windbg
中显示指定数量的栈帧吗?
带着这些疑问,一起来看看这个非常有意思的问题吧。
说明: 文章末尾有这些问题的答案,可以直接跳到末尾查看。
程序在执行某个功能时,迟迟不能完成,通过任务管理器可以发现 CPU
使用率比较高(12.47%
),大概耗尽了一个核心(机器是八核的,每个核心占 12.5%
)。
心中暗喜,大概率是遇到了死循环,应该很好解决。赶紧用 vs
附加上去看看。
附加到被调试进程后,手动暂停,然后通过并行堆栈找到可疑线程。
温馨提示: 可以通过 调试 -> 窗口 -> 并行堆栈
打开并行堆栈视图,也可以使用快捷键 Ctrl+Shift+D, S
打开 。
一般情况下调用栈最长的线程就是可疑线程。即使不是,也可以在并行堆栈视图中快速切换线程。相比于手动一个个切换线程,并行堆栈简直是太方便了!
通过并行堆栈视图,可以观察到当前线程的调用栈非常深,已经超出了 vs
所支持的最大栈帧数。仔细观察调用栈,可以发现 00007ffc9dcb3cb5
这个地址会重复出现,说明这很可能是一个递归问题。
然而,只知道这是一个递归问题还不够,我们需要找到引发递归调用的函数。如果能看到完整的调用栈,那么就可以找到罪魁祸首了。由于 vs
不能显示更多的调用栈帧,我们可以请老朋友 windbg
出马。
启动 windbg
,以 Noninvasive
模式附加到被调试进程(由于该进程正在被 vs
调试,如果不以 Noninvasive
模式附加,windbg
无法成功附加)。
附加成功后,通过 ~~[12544]s
切换到目标线程,没想到报错了。
没关系,直接切不过去,还有其它方法可以找到目标线程。可以简单粗暴的使用 ~* k
命令显示所有线程的调用栈,然后根据调用栈判断哪个线程是目标线程,也可以通过 !runaway
查看所有线程的运行时间,根据运行时间长短快速找出目标线程。
在 windbg
中输入 !runaway
可以查看所有线程的运行时间。一般,CPU
占用率越高的线程,运行时间也越长。
可以发现 0
号线程运行时间最长,然后是 32
号线程。先切换到 0
号线程,执行 k
命令查看调用栈,发现是主线程(一般情况下 0
号线程都是主线程),不是我们关心的线程。再执行 ~32s
切换到运行时间排名第二的线程,然后执行 k
命令查看调用栈,发现与在 vs
中看到的调用栈吻合,32
号线程是目标线程了。
**说明: ** 当时比较着急,忘了 windbg
中默认使用十六进制。如果执行 ~~[0n12544]s
即可正常切换过去了。0n
表示使用十进制。
找到对应的线程后,接下来的任务是查看完整调用栈。
默认情况下,windbg
的 k
命令最多只显示 256
个调用栈帧,最大的栈帧号是 ff
,从 0
开始计数。
我们可以在 windbg
中执行 kN
来指定要显示的栈帧数,如果 N
足够大,那么应该可以显示出完整的调用栈。
先尝试输入 k200
,发现看不到头,再试试 k2000
,依然看不到头,k5000
依然看不到头(这调用栈不是一般的深啊~)。 直接输入 k50000
,这次应该够了吧?没想到报错了。
根据提示可知,可以输入的最大值是 0xffff
。在 windbg
中输入 k0xffff
,耐心等待一会儿就可以看到完整的调用栈了。如下图:
说明: 不要输入 kffff
,因为会被解释为 kf fff
,第一个 f
会被解释为选项,用来显示两个栈帧的间距。
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课