6、调用栈回溯:bt、frame与up/down命令详解
调试崩溃问题,说白了就是回答三个问题:程序在哪崩的?怎么走到这里的?调用链是什么样的?
调用栈回溯,就是解决后两个问题的利器。我个人习惯,一看到段错误,第一件事就是敲 bt。别急着看变量值,先搞清楚程序是怎么走到这一步的。
6.1 bt 命令:看一眼调用链全貌
bt 是 backtrace 的缩写。它的作用,就是把当前线程的函数调用栈整个打印出来。
举个例子,假设程序在 func_c 里崩了:
(gdb) bt
#0 0x00005555555546a7 in func_c (p=0x0) at test.c:15
#1 0x00005555555546c8 in func_b (x=42) at test.c:21
#2 0x00005555555546e4 in func_a () at test.c:27
#3 0x00005555555546f9 in main () at test.c:32
每一帧(frame)都告诉你:
- 帧编号:#0 是当前正在执行的函数,#1 是它的调用者,依此类推
- 函数名与参数:比如
func_c (p=0x0),一眼就能看出传了个空指针 - 源码位置:test.c 第 15 行
我在项目中遇到过一回,bt 出来有 40 多帧,全是递归调用。当时我就知道,栈溢出了。嗯,这种场景下 bt 就是救命稻草。
核心要点:bt 只显示当前线程的调用栈。多线程程序里,每个线程都有自己的栈,需要用 thread apply all bt 来看全部。
6.2 frame 命令:切换到指定帧
光看 bt 还不够。你想想看,有时候问题出在调用者传的参数不对,但崩溃点却在深层函数里。这时候就需要切换到调用者的帧,看看它的局部变量。
frame N 就是干这个的:
(gdb) frame 1
#1 0x00005555555546c8 in func_b (x=42) at test.c:21
21 func_c(&local_var);
(gdb) print local_var
$1 = 0
你看,切到 #1 帧后,就能看到 func_b 里的局部变量 local_var 是 0。而 func_c 却把它当指针解引用了——这不崩才怪。
小技巧:frame 可以简写为 f。比如 f 2 就是切到 #2 帧。我习惯用简写,省时间。
6.3 up/down 命令:沿着调用链上下移动
如果你不想记帧编号,或者想一帧一帧地看,那就用 up 和 down。
- up:往调用者方向移动(帧编号变小,往 #0 方向)
- down:往被调用者方向移动(帧编号变大,往深处走)
举个例子,当前在 #2 帧:
(gdb) up
#1 0x00005555555546c8 in func_b (x=42) at test.c:21
21 func_c(&local_var);
(gdb) up
#0 0x00005555555546a7 in func_c (p=0x0) at test.c:15
15 *p = 100;
连续两次 up,就从 #2 走到了 #0。反过来,down 就是往回走。
注意:up 和 down 的方向容易搞混。我个人记法:up 是往上走,离 main 更近;down 是往下走,离崩溃点更近。你可以在脑子里画个栈的图,栈顶是 #0,栈底是 main。up 就是往栈顶走,down 就是往栈底走。
6.4 实战:一个典型的崩溃排查流程
我曾经排查过一个诡异的崩溃。程序跑着跑着就崩,但每次崩的位置还不一样。嗯,典型的栈破坏症状。
我的排查步骤:
- 先 bt:看看调用链是否合理。那次 bt 出来,发现 #0 帧的地址很奇怪,0x41414141 这种——明显是野指针写坏了栈。
- 切到 #1 帧:用
frame 1看调用者的局部变量,发现一个局部数组被越界写入了。 - 用 up/down 来回切换:对比不同帧的变量值,最终定位到是
memcpy时长度参数传错了。
你看,整个过程就是 bt 看全貌,frame 切到关键帧,up/down 辅助对比。三板斧下来,问题基本就清楚了。
6.5 调用栈回溯的 SVG 流程图
下面这张图,展示了调用栈回溯的核心逻辑:
6.6 避坑指南与实用技巧
最后,分享几个我踩过的坑:
- bt 显示问号:比如
#0 0x00007ffff7... in ?? ()。这通常是因为栈被破坏了,或者符号表没加载。我曾经遇到过,原因是动态库没带 debug 符号。解决方案:重新编译时加-g,或者用info sharedlibrary检查库的加载情况。 - frame 切换后变量不可见:有时候切到某帧,
print变量却提示No symbol。嗯,这多半是因为编译器优化掉了局部变量。我建议调试时用-O0编译,别开优化。 - up/down 用反了:这个我刚开始也搞混过。后来我记住一句话:up 是往上追溯调用者,down 是往下深入细节。你可以在脑子里把栈画成倒着长的树,根是 main,叶子是当前函数。up 就是往根走,down 就是往叶子走。
一句话总结:bt 看全貌,frame 切关键,up/down 做对比。这三板斧用熟了,90% 的崩溃问题都能快速定位。
公众号:蓝海资料掘金营,微信deep3321