Skip to content

LV071-gdb-backtrace

一、 概述

对于 C、C++程序而言,异常往往出现在某个函数体内,例如 main() 主函数、调用的系统库函数或者自定义的函数等。要知道,程序中每个被调用的函数在执行时,都会生成一些必要的信息,包括:

  • 函数调用发生在程序中的具体位置;
  • 调用函数时的参数;
  • 函数体内部各局部变量的值等等。

这些信息会集中存储在一块称为“栈帧”的内存空间中。也就是说,程序执行时调用了多少个函数,就会相应产生多少个栈帧,其中每个栈帧自函数调用时生成,函数调用结束后自动销毁。

这些信息会集中存储在一块称为“栈帧”的内存空间中。也就是说,程序执行时调用了多少个函数,就会相应产生多少个栈帧,其中每个栈帧自函数调用时生成,函数调用结束后自动销毁。

Tips:这些栈帧所在的位置也不是随意的,它们集中位于一个大的内存区域里,我们通常将其称为栈区或者栈。

也就意味着,当程序因某种异常暂停执行时,如果其发生在某个函数内部,我们可以尝试借助该函数对应栈帧中记录的信息,找到程序发生异常的原因。

GDB 调试器为了方便用户在调试程序时查看某个栈帧中记录的信息,提供了 frame 和 backtrace 命令。接下来我们就来看一看这些命令。

1. 函数调用栈

函数调用栈是一种数据结构,用于记录程序运行过程中的函数调用关系。每当一个函数被调用时,系统会创建一个新的栈帧(Stack Frame),其中包含该函数的局部变量、参数、返回地址等信息。栈帧按照"后进先出"(LIFO)的原则组织:

  • 最新的函数调用位于栈顶
  • 最早的函数调用(通常是 main 函数)位于栈底
  • 当前正在执行的函数位于栈顶

2. 栈帧

栈帧(Stack Frame)是函数调用栈中的一个独立单元,每个栈帧包含:

  • 函数的参数
  • 函数的局部变量
  • 返回地址(调用该函数的下一条指令地址)
  • 上一栈帧的基址指针

理解栈帧对于使用 btupdown 命令至关重要,因为这些命令本质上就是在不同的栈帧之间进行查看和切换。

Tips: 栈帧这里可以参考01-编程语言/01-C语言/18-函数/LV070-可变参数.md

二、frame命令

对于 C、C++ 程序而言,其至少也要包含一个函数,即 main() 主函数,这意味着程序执行时至少会生成一个栈帧

main() 主函数对应的栈帧,又称为初始帧或者最外层的帧。

除此之外,每当程序中多调用一个函数,执行过程中就会生成一个新的栈帧。更甚者,如果该函数是一个递归函数,则会生成多个栈帧。

在程序内部,各个栈帧用地址作为它们的标识符,注意这里的地址并不一定为栈帧的起始地址。我们知道,每个栈帧往往是由连续的多个字节构成,每个字节都有自己的地址,不同操作系统为栈帧选定地址标识符的规则不同,它们会选择其中一个字节的地址作为栈帧的标识符。

然而,GDB 调试器并没有套用地址标识符的方式来管理栈帧。对于当前调试环境中存在的栈帧,GDB 调试器会按照既定规则对它们进行编号:当前正被调用函数对应的栈帧的编号为 0,调用它的函数对应栈帧的编号为 1,以此类推。

1. 语法格式

shell
# 根据栈帧编号或者栈帧地址,选定要查看的栈帧
(gdb) frame spec

该命令可以将 spec 参数指定的栈帧选定为当前栈帧。spec 参数的值,常用的指定方法有 3 种:

(1)通过栈帧的编号指定。0 为当前被调用函数对应的栈帧号,最大编号的栈帧对应的函数通常就是 main() 主函数;

(2)借助栈帧的地址指定。栈帧地址可以通过 info frame 命令(后续会讲)打印出的信息中看到;

(3)通过函数的函数名指定。注意,如果是类似递归函数,其对应多个栈帧的话,通过此方法指定的是编号最小的那个栈帧。

2. 查看当前栈帧

使用如下命令,我们可以查看当前栈帧中存储的信息:

shell
(gdb) info frame

该命令会依次打印出当前栈帧的如下信息:

  • 当前栈帧的编号,以及栈帧的地址;
  • 当前栈帧对应函数的存储地址,以及该函数被调用时的代码存储的地址
  • 当前函数的调用者,对应的栈帧的地址;
  • 编写此栈帧所用的编程语言;
  • 函数参数的存储地址以及值;
  • 函数中局部变量的存储地址;
  • 栈帧中存储的寄存器变量,例如指令寄存器(64位环境中用 rip 表示,32为环境中用 eip 表示)、堆栈基指针寄存器(64位环境用 rbp 表示,32位环境用 ebp 表示)等。

除此之外,还可以使用info args命令查看当前函数各个参数的值;使用info locals命令查看当前函数中各局部变量的值。

二、 bt 命令

btbacktrace 的缩写,用于打印当前函数调用栈的回溯信息。这是调试时最常用的命令之一,可以帮助开发者快速了解程序执行到当前位置的完整调用路径。

1. 语法格式

text
(gdb) bt [选项]
(gdb) backtrace [选项]

常用选项如下:

选项说明
无选项打印所有栈帧信息
n只打印栈顶的 n 个栈帧(n 为正整数,打印最里层)
-n只打印栈底的 n 个栈帧(n 为正整数,打印最外层)
full同时打印每个栈帧中的局部变量值
no-filters禁用所有栈帧过滤器

当调试多线程程序时,该命令仅用于打印当前线程中所有栈帧的信息。如果想要打印所有线程的栈帧信息,应执行thread apply all backtrace命令。

2. 命令详解

2.1 基本用法

在 GDB 中使用 bt 命令可以查看完整的函数调用栈:

text
(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 限制输出数量

当调用栈很深时,可以使用数字参数限制输出:

text
(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...)

查看栈底的两个栈帧:

text
(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:52

2.3 显示局部变量

使用 bt full 可以同时显示每个栈帧中的局部变量:

text
(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 = 5

3. 相关命令

命令说明
wherebt 的别名,功能完全相同
info stack显示栈帧摘要信息
info frame显示当前栈帧详细信息

三、 up 命令

up 命令用于在函数调用栈中向上移动(向调用者方向),即从当前栈帧移动到编号更大的栈帧。

1. 基本语法

text
up [n]
参数说明
无参数向上移动一个栈帧
n向上移动 n 个栈帧(n 为正整数,默认值为1)

该命令表示在当前栈帧编号(假设为 m)的基础上,选定 m+n 为编号的栈帧作为新的当前栈帧。

2. 命令详解

2.1 基本用法

假设当前位于 multiply 函数(栈帧 #0),执行 up 命令后:

text
(gdb) up
#1  0x000055555555519c in square (n=5) at 022-gdb-bt-up-down:24
24          int result = multiply(n, n);

此时,GDB 的当前上下文切换到了 square 函数的栈帧,可以查看该函数作用域内的变量:

text
(gdb) info locals
result = 25
(gdb) print n
$1 = 5

2.2 指定移动步数

可以一次向上移动多个栈帧:

text
(gdb) up 2
#3  0x0000555555555272 in main (argc=1, argv=0x7fffffffdd98) at 022-gdb-bt-up-down:52
52          process(num);

2.3 移动到栈顶

当已经位于栈底时,继续执行 up 命令会提示:

text
(gdb) up
Initial frame selected; you cannot go up.

3. 使用场景

  • 查看调用者函数的上下文信息
  • 检查传递给当前函数的参数在调用处的值
  • 追踪函数调用链,定位问题根源

四、 down 命令

down 命令用于在函数调用栈中向下移动(向被调用者方向),即从当前栈帧移动到编号更小的栈帧。这与 up 命令的方向相反。

1. 基本语法

text
down [n]
参数说明
无参数向下移动一个栈帧
n向下移动 n 个栈帧(n 为正整数,默认值为 1)

该命令表示在当前栈帧编号(假设为 m)的基础上,选定 m-n 为编号的栈帧作为新的当前栈帧。

2. 命令详解

2.1 基本用法

假设当前位于 main 函数(栈帧 #3),执行 down 命令后:

text
(gdb) down
#2  0x0000555555555218 in process (x=5) at 022-gdb-bt-up-down:43
43          int sq = square(x);

2.2 指定移动步数

可以一次向下移动多个栈帧:

text
(gdb) down 2
#0  multiply (a=5, b=5) at 022-gdb-bt-up-down:16
16          int result = a * b;

2.3 移动到栈底

当已经位于栈顶(当前正在执行的函数)时,继续执行 down 命令会提示:

text
(gdb) down
Bottom (innermost) frame selected; you cannot go down.

3. 使用场景

  • 从调用者返回到被调用函数查看细节
  • 配合 up 命令在调用栈中自由导航
  • 深入分析特定函数的执行状态

五、 调试示例

1. 示例1

本节通过一个完整的调试会话演示 btupdown 命令的实际使用。

1.1 测试程序

示例程序 022-gdb-bt-up-down.c包含多层函数调用和递归函数,用于演示栈帧导航:

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 选项编译程序以包含调试信息:

bash
cd ~/workspace/c-learning/02-c-basic/21-debug
gcc 023-gdb-bt-up-down.c -g

1.2 启动调试

text
$ gdb ./a.out
GNU gdb (Ubuntu 12.1-0ubuntu1~22.04) 12.1
...
Reading symbols from ./030-gdb-bt-up-down...

1.3 设置断点

multiply 函数处设置断点:

text
(gdb) break multiply
Breakpoint 1 at 0x1189: file 022-gdb-bt-up-down, line 16.

1.4 运行程序

text
(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 命令查看当前调用栈:

text
(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 函数:

text
(gdb) up
#1  0x000055555555519c in square (n=5) at 022-gdb-bt-up-down:24
24          int result = multiply(n, n);

查看 square 函数中的局部变量:

text
(gdb) info locals
result = <optimized out>
(gdb) print n
$1 = 5

继续向上移动到 process 函数:

text
(gdb) up
#2  0x0000555555555218 in process (x=5) at 022-gdb-bt-up-down:43
43          int sq = square(x);

查看 process 函数中的变量:

text
(gdb) info locals
sq = <optimized out>
fact = <optimized out>
(gdb) print x
$2 = 5

1.7 使用 down 命令向下导航

从当前位置向下移动回 square 函数:

text
(gdb) down
#1  0x000055555555519c in square (n=5) at 022-gdb-bt-up-down:24
24          int result = multiply(n, n);

继续向下返回到 multiply 函数:

text
(gdb) down
#0  multiply (a=5, b=5) at 022-gdb-bt-up-down:16
16          int result = a * b;

1.8 使用 bt full 查看详细信息

text
(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 函数设置断点,观察递归调用栈:

text
(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) {

查看递归调用栈:

text
(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 参数值。使用 updown 可以在不同递归层级间导航:

text
(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 = 3

2. 示例2

2.1 测试程序

c
#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 选项编译程序以包含调试信息:

bash
cd ~/workspace/c-learning/02-c-basic/21-debug
gcc 023-gdb-frame-bt.c -g

2.2 调试示例

shell
 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. 命令对照表

命令全称功能移动方向
btbacktrace显示函数调用栈不移动
upup向调用者方向移动栈帧编号增大
downdown向被调用者方向移动栈帧编号减小

2. 栈帧导航示意图

text
栈底 (最早调用)                              栈顶 (最新调用)
    ↓                                            ↓
+--------+    +--------+    +--------+    +--------+
| 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(默认)选项:

bash
gcc -g -O0 -o program program.c

3. 核心转储文件

btupdown 命令同样适用于分析核心转储文件(core dump):

bash
gdb ./program core
(gdb) bt

4. 多线程程序

在多线程程序中,每个线程都有独立的调用栈。使用 info threads 查看所有线程,使用 thread n 切换线程后再使用 bt 命令:

text
(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