LV071-gdb-backtrace
一、 概述
对于 C、C++程序而言,异常往往出现在某个函数体内,例如 main() 主函数、调用的系统库函数或者自定义的函数等。要知道,程序中每个被调用的函数在执行时,都会生成一些必要的信息,包括:
- 函数调用发生在程序中的具体位置;
- 调用函数时的参数;
- 函数体内部各局部变量的值等等。
这些信息会集中存储在一块称为“栈帧”的内存空间中。也就是说,程序执行时调用了多少个函数,就会相应产生多少个栈帧,其中每个栈帧自函数调用时生成,函数调用结束后自动销毁。
这些信息会集中存储在一块称为“栈帧”的内存空间中。也就是说,程序执行时调用了多少个函数,就会相应产生多少个栈帧,其中每个栈帧自函数调用时生成,函数调用结束后自动销毁。
Tips:这些栈帧所在的位置也不是随意的,它们集中位于一个大的内存区域里,我们通常将其称为栈区或者栈。
也就意味着,当程序因某种异常暂停执行时,如果其发生在某个函数内部,我们可以尝试借助该函数对应栈帧中记录的信息,找到程序发生异常的原因。
GDB 调试器为了方便用户在调试程序时查看某个栈帧中记录的信息,提供了 frame 和 backtrace 命令。接下来我们就来看一看这些命令。
1. 函数调用栈
函数调用栈是一种数据结构,用于记录程序运行过程中的函数调用关系。每当一个函数被调用时,系统会创建一个新的栈帧(Stack Frame),其中包含该函数的局部变量、参数、返回地址等信息。栈帧按照"后进先出"(LIFO)的原则组织:
- 最新的函数调用位于栈顶
- 最早的函数调用(通常是
main函数)位于栈底 - 当前正在执行的函数位于栈顶
2. 栈帧
栈帧(Stack Frame)是函数调用栈中的一个独立单元,每个栈帧包含:
- 函数的参数
- 函数的局部变量
- 返回地址(调用该函数的下一条指令地址)
- 上一栈帧的基址指针
理解栈帧对于使用 bt、up、down 命令至关重要,因为这些命令本质上就是在不同的栈帧之间进行查看和切换。
Tips: 栈帧这里可以参考01-编程语言/01-C语言/18-函数/LV070-可变参数.md
二、frame命令
对于 C、C++ 程序而言,其至少也要包含一个函数,即 main() 主函数,这意味着程序执行时至少会生成一个栈帧。
main() 主函数对应的栈帧,又称为初始帧或者最外层的帧。
除此之外,每当程序中多调用一个函数,执行过程中就会生成一个新的栈帧。更甚者,如果该函数是一个递归函数,则会生成多个栈帧。
在程序内部,各个栈帧用地址作为它们的标识符,注意这里的地址并不一定为栈帧的起始地址。我们知道,每个栈帧往往是由连续的多个字节构成,每个字节都有自己的地址,不同操作系统为栈帧选定地址标识符的规则不同,它们会选择其中一个字节的地址作为栈帧的标识符。
然而,GDB 调试器并没有套用地址标识符的方式来管理栈帧。对于当前调试环境中存在的栈帧,GDB 调试器会按照既定规则对它们进行编号:当前正被调用函数对应的栈帧的编号为 0,调用它的函数对应栈帧的编号为 1,以此类推。
1. 语法格式
# 根据栈帧编号或者栈帧地址,选定要查看的栈帧
(gdb) frame spec该命令可以将 spec 参数指定的栈帧选定为当前栈帧。spec 参数的值,常用的指定方法有 3 种:
(1)通过栈帧的编号指定。0 为当前被调用函数对应的栈帧号,最大编号的栈帧对应的函数通常就是 main() 主函数;
(2)借助栈帧的地址指定。栈帧地址可以通过 info frame 命令(后续会讲)打印出的信息中看到;
(3)通过函数的函数名指定。注意,如果是类似递归函数,其对应多个栈帧的话,通过此方法指定的是编号最小的那个栈帧。
2. 查看当前栈帧
使用如下命令,我们可以查看当前栈帧中存储的信息:
(gdb) info frame该命令会依次打印出当前栈帧的如下信息:
- 当前栈帧的编号,以及栈帧的地址;
- 当前栈帧对应函数的存储地址,以及该函数被调用时的代码存储的地址
- 当前函数的调用者,对应的栈帧的地址;
- 编写此栈帧所用的编程语言;
- 函数参数的存储地址以及值;
- 函数中局部变量的存储地址;
- 栈帧中存储的寄存器变量,例如指令寄存器(64位环境中用 rip 表示,32为环境中用 eip 表示)、堆栈基指针寄存器(64位环境用 rbp 表示,32位环境用 ebp 表示)等。
除此之外,还可以使用info args命令查看当前函数各个参数的值;使用info locals命令查看当前函数中各局部变量的值。
二、 bt 命令
bt 是 backtrace 的缩写,用于打印当前函数调用栈的回溯信息。这是调试时最常用的命令之一,可以帮助开发者快速了解程序执行到当前位置的完整调用路径。
1. 语法格式
(gdb) bt [选项]
(gdb) backtrace [选项]常用选项如下:
| 选项 | 说明 |
|---|---|
无选项 | 打印所有栈帧信息 |
n | 只打印栈顶的 n 个栈帧(n 为正整数,打印最里层) |
-n | 只打印栈底的 n 个栈帧(n 为正整数,打印最外层) |
full | 同时打印每个栈帧中的局部变量值 |
no-filters | 禁用所有栈帧过滤器 |
当调试多线程程序时,该命令仅用于打印当前线程中所有栈帧的信息。如果想要打印所有线程的栈帧信息,应执行thread apply all backtrace命令。
2. 命令详解
2.1 基本用法
在 GDB 中使用 bt 命令可以查看完整的函数调用栈:
(gdb) bt
#0 multiply (a=5, b=5) at 022-gdb-bt-up-down:16
#1 0x000055555555519c in square (n=5) at 022-gdb-bt-up-down:24
#2 0x0000555555555218 in process (x=5) at 022-gdb-bt-up-down:43
#3 0x0000555555555272 in main (argc=1, argv=0x7fffffffdd98) at 022-gdb-bt-up-down:52输出说明:
#0、#1等:栈帧编号,从 0 开始,0 表示当前栈帧(栈顶)multiply:函数名a=5, b=5:函数参数及其当前值at 022-gdb-bt-up-down:16:函数所在的源文件和行号
2.2 限制输出数量
当调用栈很深时,可以使用数字参数限制输出:
(gdb) bt 2
#0 multiply (a=5, b=5) at 022-gdb-bt-up-down:16
#1 0x000055555555519c in square (n=5) at 022-gdb-bt-up-down:24
(More stack frames follow...)查看栈底的两个栈帧:
(gdb) bt -2
#2 0x0000555555555218 in process (x=5) at 022-gdb-bt-up-down:43
#3 0x0000555555555272 in main (argc=1, argv=0x7fffffffdd98) at 022-gdb-bt-up-down:522.3 显示局部变量
使用 bt full 可以同时显示每个栈帧中的局部变量:
(gdb) bt full
#0 multiply (a=5, b=5) at 022-gdb-bt-up-down:16
result = 25
#1 0x000055555555519c in square (n=5) at 022-gdb-bt-up-down:24
result = 25
#2 0x0000555555555218 in process (x=5) at 022-gdb-bt-up-down:43
sq = 25
fact = <optimized out>
#3 0x0000555555555272 in main (argc=1, argv=0x7fffffffdd98) at 022-gdb-bt-up-down:52
num = 53. 相关命令
| 命令 | 说明 |
|---|---|
where | bt 的别名,功能完全相同 |
info stack | 显示栈帧摘要信息 |
info frame | 显示当前栈帧详细信息 |
三、 up 命令
up 命令用于在函数调用栈中向上移动(向调用者方向),即从当前栈帧移动到编号更大的栈帧。
1. 基本语法
up [n]| 参数 | 说明 |
|---|---|
无参数 | 向上移动一个栈帧 |
n | 向上移动 n 个栈帧(n 为正整数,默认值为1) |
该命令表示在当前栈帧编号(假设为 m)的基础上,选定 m+n 为编号的栈帧作为新的当前栈帧。
2. 命令详解
2.1 基本用法
假设当前位于 multiply 函数(栈帧 #0),执行 up 命令后:
(gdb) up
#1 0x000055555555519c in square (n=5) at 022-gdb-bt-up-down:24
24 int result = multiply(n, n);此时,GDB 的当前上下文切换到了 square 函数的栈帧,可以查看该函数作用域内的变量:
(gdb) info locals
result = 25
(gdb) print n
$1 = 52.2 指定移动步数
可以一次向上移动多个栈帧:
(gdb) up 2
#3 0x0000555555555272 in main (argc=1, argv=0x7fffffffdd98) at 022-gdb-bt-up-down:52
52 process(num);2.3 移动到栈顶
当已经位于栈底时,继续执行 up 命令会提示:
(gdb) up
Initial frame selected; you cannot go up.3. 使用场景
- 查看调用者函数的上下文信息
- 检查传递给当前函数的参数在调用处的值
- 追踪函数调用链,定位问题根源
四、 down 命令
down 命令用于在函数调用栈中向下移动(向被调用者方向),即从当前栈帧移动到编号更小的栈帧。这与 up 命令的方向相反。
1. 基本语法
down [n]| 参数 | 说明 |
|---|---|
无参数 | 向下移动一个栈帧 |
n | 向下移动 n 个栈帧(n 为正整数,默认值为 1) |
该命令表示在当前栈帧编号(假设为 m)的基础上,选定 m-n 为编号的栈帧作为新的当前栈帧。
2. 命令详解
2.1 基本用法
假设当前位于 main 函数(栈帧 #3),执行 down 命令后:
(gdb) down
#2 0x0000555555555218 in process (x=5) at 022-gdb-bt-up-down:43
43 int sq = square(x);2.2 指定移动步数
可以一次向下移动多个栈帧:
(gdb) down 2
#0 multiply (a=5, b=5) at 022-gdb-bt-up-down:16
16 int result = a * b;2.3 移动到栈底
当已经位于栈顶(当前正在执行的函数)时,继续执行 down 命令会提示:
(gdb) down
Bottom (innermost) frame selected; you cannot go down.3. 使用场景
- 从调用者返回到被调用函数查看细节
- 配合
up命令在调用栈中自由导航 - 深入分析特定函数的执行状态
五、 调试示例
1. 示例1
本节通过一个完整的调试会话演示 bt、up、down 命令的实际使用。
1.1 测试程序
示例程序 022-gdb-bt-up-down.c包含多层函数调用和递归函数,用于演示栈帧导航:
#include <stdio.h>
int multiply(int a, int b) {
int result = a * b;
printf("multiply: %d * %d = %d\n", a, b, result);
return result;
}
int square(int n) {
int result = multiply(n, n);
printf("square: %d^2 = %d\n", n, result);
return result;
}
int factorial(int n) {
if (n <= 1) {
printf("factorial: base case, returning 1\n");
return 1;
}
int result = n * factorial(n - 1);
printf("factorial: %d! = %d\n", n, result);
return result;
}
void process(int x) {
printf("process: 开始处理 %d\n", x);
int sq = square(x);
int fact = factorial(x);
printf("process: %d 的平方是 %d\n", x, sq);
printf("process: %d 的阶乘是 %d\n", x, fact);
}
int main(int argc, char *argv[]) {
int num = 5;
printf("=== GDB bt/up/down 命令演示程序 ===\n");
process(num);
printf("=== 程序结束 ===\n");
return 0;
}使用 -g 选项编译程序以包含调试信息:
cd ~/workspace/c-learning/02-c-basic/21-debug
gcc 023-gdb-bt-up-down.c -g1.2 启动调试
$ gdb ./a.out
GNU gdb (Ubuntu 12.1-0ubuntu1~22.04) 12.1
...
Reading symbols from ./030-gdb-bt-up-down...1.3 设置断点
在 multiply 函数处设置断点:
(gdb) break multiply
Breakpoint 1 at 0x1189: file 022-gdb-bt-up-down, line 16.1.4 运行程序
(gdb) run
Starting program: ./030-gdb-bt-up-down
=== GDB bt/up/down 命令演示程序 ===
process: 开始处理 5
Breakpoint 1, multiply (a=5, b=5) at 022-gdb-bt-up-down:16
16 int result = a * b;1.5 查看调用栈
使用 bt 命令查看当前调用栈:
(gdb) bt
#0 multiply (a=5, b=5) at 022-gdb-bt-up-down:16
#1 0x000055555555519c in square (n=5) at 022-gdb-bt-up-down:24
#2 0x0000555555555218 in process (x=5) at 022-gdb-bt-up-down:43
#3 0x0000555555555272 in main (argc=1, argv=0x7fffffffdd98) at 022-gdb-bt-up-down:52
#4 0x00007ffff7df60b3 in __libc_start_main (main=0x555555555249 <main>,
argc=1, argv=0x7fffffffdd98, init=<optimized out>, fini=<optimized out>,
rtld_fini=<optimized out>, stack_end=0x7fffffffdd88) at ../csu/libc-start.c:308
#5 0x000055555555507e in _start ()1.6 使用 up 命令向上导航
从当前栈帧(#0)向上移动到 square 函数:
(gdb) up
#1 0x000055555555519c in square (n=5) at 022-gdb-bt-up-down:24
24 int result = multiply(n, n);查看 square 函数中的局部变量:
(gdb) info locals
result = <optimized out>
(gdb) print n
$1 = 5继续向上移动到 process 函数:
(gdb) up
#2 0x0000555555555218 in process (x=5) at 022-gdb-bt-up-down:43
43 int sq = square(x);查看 process 函数中的变量:
(gdb) info locals
sq = <optimized out>
fact = <optimized out>
(gdb) print x
$2 = 51.7 使用 down 命令向下导航
从当前位置向下移动回 square 函数:
(gdb) down
#1 0x000055555555519c in square (n=5) at 022-gdb-bt-up-down:24
24 int result = multiply(n, n);继续向下返回到 multiply 函数:
(gdb) down
#0 multiply (a=5, b=5) at 022-gdb-bt-up-down:16
16 int result = a * b;1.8 使用 bt full 查看详细信息
(gdb) bt full
#0 multiply (a=5, b=5) at 022-gdb-bt-up-down:16
result = <optimized out>
#1 0x000055555555519c in square (n=5) at 022-gdb-bt-up-down:24
result = <optimized out>
#2 0x0000555555555218 in process (x=5) at 022-gdb-bt-up-down:43
sq = <optimized out>
fact = <optimized out>
#3 0x0000555555555272 in main (argc=1, argv=0x7fffffffdd98) at 022-gdb-bt-up-down:52
num = 5
...1.9 递归函数调试示例
在 factorial 函数设置断点,观察递归调用栈:
(gdb) break factorial
Breakpoint 2 at 0x11c9: file 022-gdb-bt-up-down, line 29.
(gdb) continue
Continuing.
multiply: 5 * 5 = 25
square: 5^2 = 25
Breakpoint 2, factorial (n=5) at 022-gdb-bt-up-down:29
29 if (n <= 1) {
(gdb) continue
Continuing.
Breakpoint 2, factorial (n=4) at 022-gdb-bt-up-down:29
29 if (n <= 1) {
(gdb) continue
Continuing.
Breakpoint 2, factorial (n=3) at 022-gdb-bt-up-down:29
29 if (n <= 1) {
(gdb) continue
Continuing.
Breakpoint 2, factorial (n=2) at 022-gdb-bt-up-down:29
29 if (n <= 1) {
(gdb) continue
Continuing.
Breakpoint 2, factorial (n=1) at 022-gdb-bt-up-down:29
29 if (n <= 1) {查看递归调用栈:
(gdb) bt
#0 factorial (n=1) at 022-gdb-bt-up-down:29
#1 0x00005555555551e6 in factorial (n=2) at 022-gdb-bt-up-down:32
#2 0x00005555555551e6 in factorial (n=3) at 022-gdb-bt-up-down:32
#3 0x00005555555551e6 in factorial (n=4) at 022-gdb-bt-up-down:32
#4 0x00005555555551e6 in factorial (n=5) at 022-gdb-bt-up-down:32
#5 0x0000555555555242 in process (x=5) at 022-gdb-bt-up-down:44
#6 0x0000555555555272 in main (argc=1, argv=0x7fffffffdd98) at 022-gdb-bt-up-down:52
...可以看到递归调用形成了多层栈帧,每一层 factorial 调用都有自己的 n 参数值。使用 up 和 down 可以在不同递归层级间导航:
(gdb) up
#1 0x00005555555551e6 in factorial (n=2) at 022-gdb-bt-up-down:32
32 int result = n * factorial(n - 1);
(gdb) print n
$3 = 2
(gdb) up
#2 0x00005555555551e6 in factorial (n=3) at 022-gdb-bt-up-down:32
32 int result = n * factorial(n - 1);
(gdb) print n
$4 = 32. 示例2
2.1 测试程序
#include <stdio.h>
int func(int num) {
if (num == 1) {
return 1;
}
else {
return num * func(num - 1);
}
}
int main(int argc, char *argv[]) {
int n = 5;
int result = func(n);
printf("%d! = %d", n, result);
return 0;
}使用 -g 选项编译程序以包含调试信息:
cd ~/workspace/c-learning/02-c-basic/21-debug
gcc 023-gdb-frame-bt.c -g2.2 调试示例
✓ sumu@virtual-machine:~/workspace/c-learning/02-c-basic/21-debug [main ≡ +1 ~0 -0 !]
$ gdb ./a.out -q
Reading symbols from ./a.out...
(gdb) l # <=== 1. 查看源码
1 #include <stdio.h>
2 int func(int num) {
3 if (num == 1) {
4 return 1;
5 }
6 else {
7 return num * func(num - 1);
8 }
9 }
10 int main(int argc, char *argv[]) {
(gdb) b 3 # <=== 2. 第3行打断点
Breakpoint 1 at 0x1158: file 023-gdb-frame-bt.c, line 3.
(gdb) r # <=== 3. 运行程序
Starting program: /home/sumu/workspace/c-learning/02-c-basic/21-debug/a.out
# <=== 程序在第3行停下
Breakpoint 1, func (num=5) at 023-gdb-frame-bt.c:3
3 if (num == 1) {
(gdb) c # <=== 4. 继续执行
Continuing.
# <=== 再次在第3行停下
Breakpoint 1, func (num=4) at 023-gdb-frame-bt.c:3
3 if (num == 1) {
(gdb) p num # <=== 5. 打印num的值
$1 = 4 # <=== 此时num为4
(gdb) bt # <=== 6. 打印所有栈帧信息
#0 func (num=4) at 023-gdb-frame-bt.c:3
#1 0x0000555555555172 in func (num=5) at 023-gdb-frame-bt.c:7
#2 0x000055555555519c in main (argc=1, argv=0x7fffffffce28) at 023-gdb-frame-bt.c:12
(gdb) info frame # <=== 7. 打印当前栈帧信息
Stack level 0, frame at 0x7fffffffccf0: # <=== 帧编号为0,地址是0x7fffffffccf0
rip = 0x555555555158 in func (023-gdb-frame-bt.c:3); saved rip = 0x555555555172 # <=== 函数的存储地址是0x555555555158,调用它的函数的地址为0x555555555172
called by frame at 0x7fffffffcd10 # <=== 当前栈帧的上一级栈帧(编号 1 的栈帧)的地址为 0x7fffffffcd10
source language c.
Arglist at 0x7fffffffccc8, args: num=4 # <=== 函数参数的地址和值
Locals at 0x7fffffffccc8, Previous frame's sp is 0x7fffffffccf0 # <=== 函数内部局部变量的存储地址
Saved registers: # <=== 栈帧内部存储的寄存器
rbp at 0x7fffffffcce0, rip at 0x7fffffffcce8
(gdb) info args # <=== 8. 打印当前函数参数的值
num = 4
(gdb) info locals # <=== 9. 打印当前函数内部局部变量的信息(这里没有)
No locals.
(gdb) up # <=== 10. 向上移动1个栈帧
#1 0x0000555555555172 in func (num=5) at 023-gdb-frame-bt.c:7
7 return num * func(num - 1);
(gdb) frame 1 # <=== 11. 将编号为1的栈帧当做当前栈帧
#1 0x0000555555555172 in func (num=5) at 023-gdb-frame-bt.c:7
7 return num * func(num - 1);
(gdb) info frame # <=== 12. 打印编号为1的栈帧的详细信息
Stack level 1, frame at 0x7fffffffcd10:
rip = 0x555555555172 in func (023-gdb-frame-bt.c:7); saved rip = 0x55555555519c
called by frame at 0x7fffffffcd40, caller of frame at 0x7fffffffccf0
source language c.
Arglist at 0x7fffffffcce8, args: num=5
Locals at 0x7fffffffcce8, Previous frame's sp is 0x7fffffffcd10
Saved registers:
rbp at 0x7fffffffcd00, rip at 0x7fffffffcd08
(gdb)六、 命令对比总结
1. 命令对照表
| 命令 | 全称 | 功能 | 移动方向 |
|---|---|---|---|
bt | backtrace | 显示函数调用栈 | 不移动 |
up | up | 向调用者方向移动 | 栈帧编号增大 |
down | down | 向被调用者方向移动 | 栈帧编号减小 |
2. 栈帧导航示意图
栈底 (最早调用) 栈顶 (最新调用)
↓ ↓
+--------+ +--------+ +--------+ +--------+
| main() | -> |process()| -> |square()| -> |multiply()|
+--------+ +--------+ +--------+ +--------+
#3 #2 #1 #0
up 命令方向:← (向栈底方向,编号增大)
down 命令方向:→ (向栈顶方向,编号减小)3. 常用组合命令
| 组合 | 说明 |
|---|---|
bt + up | 查看调用栈后向上追溯调用者 |
bt + frame n | 直接跳转到指定编号的栈帧 |
up + info locals | 查看上层函数的局部变量 |
bt full | 一次性查看所有栈帧的详细信息 |
七、 注意事项
1. 栈帧编号
- 栈帧编号从 0 开始
- 编号 0 始终表示当前正在执行的函数(栈顶)
- 编号越大,表示调用层次越早(越靠近
main函数)
2. 优化影响
编译时如果使用了优化选项(如 -O2),某些函数可能被内联优化,导致调用栈信息不完整。调试时建议使用 -O0(默认)选项:
gcc -g -O0 -o program program.c3. 核心转储文件
bt、up、down 命令同样适用于分析核心转储文件(core dump):
gdb ./program core
(gdb) bt4. 多线程程序
在多线程程序中,每个线程都有独立的调用栈。使用 info threads 查看所有线程,使用 thread n 切换线程后再使用 bt 命令:
(gdb) info threads
Id Target Id Frame
* 1 Thread 0x7ffff7dc4740 (LWP 12345) "program" 0x00007ffff7fcd61a in read () from /lib/x86_64-linux-gnu/libc.so.6
2 Thread 0x7ffff7bc3700 (LWP 12346) "program" 0x00007ffff7fcd61a in read () from /lib/x86_64-linux-gnu/libc.so.6
(gdb) thread 2
[Switching to thread 2 (Thread 0x7ffff7bc3700 (LWP 12346))]
(gdb) bt