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 命令:沿着调用链上下移动

如果你不想记帧编号,或者想一帧一帧地看,那就用 updown

  • 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 就是往回走。

注意updown 的方向容易搞混。我个人记法:up 是往上走,离 main 更近;down 是往下走,离崩溃点更近。你可以在脑子里画个栈的图,栈顶是 #0,栈底是 main。up 就是往栈顶走,down 就是往栈底走。

6.4 实战:一个典型的崩溃排查流程

我曾经排查过一个诡异的崩溃。程序跑着跑着就崩,但每次崩的位置还不一样。嗯,典型的栈破坏症状。

我的排查步骤:

  1. 先 bt:看看调用链是否合理。那次 bt 出来,发现 #0 帧的地址很奇怪,0x41414141 这种——明显是野指针写坏了栈。
  2. 切到 #1 帧:用 frame 1 看调用者的局部变量,发现一个局部数组被越界写入了。
  3. 用 up/down 来回切换:对比不同帧的变量值,最终定位到是 memcpy 时长度参数传错了。

你看,整个过程就是 bt 看全貌,frame 切到关键帧,up/down 辅助对比。三板斧下来,问题基本就清楚了。

6.5 调用栈回溯的 SVG 流程图

下面这张图,展示了调用栈回溯的核心逻辑:

调用栈回溯核心流程 调用栈(从栈顶到栈底) #0 func_c(p=0x0) test.c:15 ← 当前崩溃点 #1 func_b(x=42) test.c:21 ← frame 1 可查看局部变量 #2 func_a() test.c:27 #3 main() test.c:32 up 方向(往调用者) down 方向(往被调用者) bt:查看全貌 | frame N:切换到指定帧 | up/down:沿调用链移动 ⚠ 注意:up 是往栈顶走(离崩溃点近),down 是往栈底走(离 main 近)

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