初学者必学的 GDB 调试指令:如何在线查看变量值并追踪函数调用栈?

各位编程爱好者、未来的软件架构师们,晚上好!

欢迎来到今天的技术讲座。我是你们的讲师,一位在软件开发领域摸爬滚打了多年的老兵。今天,我们将共同探索一个对所有开发者来说都至关重要的技能——调试。具体来说,我们将聚焦于开源世界中最强大、最经典的调试器之一:GDB。

想象一下,你辛辛苦苦写了几百行甚至几千行代码,满怀信心地编译运行,结果程序崩溃了,或者输出了一个完全不符合预期的结果。这时候,你是不是感到头疼?是不是想知道程序在执行到哪一行时出了问题?某个变量在那一刻的值到底是什么?函数是如何被层层调用的?

这就是调试器大显身手的时候。它就像一个X光机,能够穿透你的代码,让你在程序运行时,暂停它,检查它,甚至修改它。而GDB,就是这台X光机中的“瑞士军刀”。对于初学者来说,掌握GDB的基本用法,尤其是如何在线查看变量值和追踪函数调用栈,是迈向独立解决问题的第一步,也是成为一名高效程序员的必经之路。

今天的讲座,我将以实战为导向,结合大量的代码示例,深入浅出地讲解GDB的核心调试指令。我们将从最基础的GDB启动与退出,到设置断点,控制程序执行流,再到今天的主题:如何精确地检查变量状态,以及如何清晰地理解函数间的调用关系。

无需担心,即使你是GDB的零基础新手,只要跟着我的讲解,一步步操作,你将很快掌握这些强大的调试技能。让我们开始吧!


第一章:GDB调试的基石——环境准备与基本操作

在深入变量查看和栈追踪之前,我们必须确保我们的程序能够被GDB调试,并且了解GDB的一些最基本的操作。

1.1 编译你的代码:加上调试信息

GDB之所以能让你看到源代码、变量名,并能根据行号设置断点,是因为编译器在生成可执行文件时,额外嵌入了调试信息。这些信息告诉GDB源代码与机器码之间的对应关系。

要包含这些调试信息,你需要在编译时加上 -g 选项。

示例代码:simple_program.c

#include <stdio.h>
#include <stdlib.h> // For malloc and free

int calculate_sum(int a, int b) {
    int sum = a + b;
    return sum;
}

void process_array(int* arr, int size) {
    printf("Processing array elements:n");
    for (int i = 0; i <= size; ++i) { // Potential off-by-one error
        if (i < size) {
            printf("arr[%d] = %dn", i, arr[i]);
        } else {
            printf("Attempting to access arr[%d] (out of bounds for size %d)n", i, size);
        }
    }
}

int main() {
    int x = 10;
    int y = 20;
    int result = calculate_sum(x, y);
    printf("Sum of %d and %d is: %dn", x, y, result);

    int data_size = 5;
    int* my_array = (int*)malloc(data_size * sizeof(int));
    if (my_array == NULL) {
        fprintf(stderr, "Memory allocation failed!n");
        return 1;
    }

    for (int i = 0; i < data_size; ++i) {
        my_array[i] = i * 10;
    }

    process_array(my_array, data_size);

    free(my_array); // Don't forget to free allocated memory
    my_array = NULL; // Best practice: set pointer to NULL after freeing

    printf("Program finished successfully.n");
    return 0;
}

编译指令:

gcc -g simple_program.c -o simple_program

现在,simple_program 可执行文件就包含了调试信息,可以被GDB完美地调试了。

1.2 启动与退出GDB

启动GDB非常简单,只需在终端中输入 gdb 后面跟着你的可执行文件名。

启动GDB:

gdb simple_program

你会看到GDB的提示符 (gdb),表示你已进入GDB环境。

退出GDB:

要退出GDB,你可以输入 quitq

(gdb) quit

1.3 GDB调试的基本流程控制

在能够查看变量和栈之前,我们首先需要学会如何控制程序的执行。

核心控制命令:

命令 缩写 描述
break b 设置断点。程序执行到断点处会暂停。
run r 启动程序执行。如果程序已经启动过,会重新启动。
continue c 让程序从当前暂停点继续执行,直到遇到下一个断点或程序结束。
next n 单步执行,不进入函数内部(跳过函数调用)。
step s 单步执行,会进入函数内部(如果当前行是函数调用,则进入该函数的第一行)。
finish 运行到当前函数结束并返回。
list l 查看源代码。list <line_number>list <function_name>

示例:设置断点并单步执行

(gdb) b main
Breakpoint 1 at 0x1183: file simple_program.c, line 23.
(gdb) run
Starting program: /path/to/simple_program 

Breakpoint 1, main () at simple_program.c:23
23      int x = 10;
(gdb) n
24      int y = 20;
(gdb) n
25      int result = calculate_sum(x, y);
(gdb) s
calculate_sum (a=10, b=20) at simple_program.c:6
6       int sum = a + b;
(gdb) n
7       return sum;
(gdb) finish
Run till exit from #0  calculate_sum (a=10, b=20) at simple_program.c:7
0x0000555555555193 in main () at simple_program.c:25
25      int result = calculate_sum(x, y);
Value returned is $1 = 30
(gdb) 

通过这些基本命令,我们现在已经掌握了在GDB中暂停程序执行流的能力。接下来,就是如何在程序暂停时,深入探查其内部状态的关键时刻。


第二章:在线查看变量值——洞察程序内部状态

当程序在断点处暂停时,GDB允许你“窥视”任何可访问的变量的值,无论是基本类型、指针、数组还是结构体。这是调试中最常用,也是最有力的功能之一。

2.1 print 命令:随时查看变量

print (缩写 p) 是GDB中查看变量值的核心命令。你可以在程序暂停的任何时候使用它来打印表达式的值。

语法: print <expression>

2.1.1 查看基本类型变量

当程序暂停在 main 函数的第25行(即 int result = calculate_sum(x, y); 之后),我们可以查看 x, y, result 的值。

(gdb) b 26
Breakpoint 2 at 0x555555555198: file simple_program.c, line 26.
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /path/to/simple_program 

Breakpoint 1, main () at simple_program.c:23
23      int x = 10;
(gdb) c
Continuing.

Breakpoint 2, main () at simple_program.c:26
26      printf("Sum of %d and %d is: %dn", x, y, result);
(gdb) print x
$1 = 10
(gdb) print y
$2 = 20
(gdb) print result
$3 = 30

GDB会为每次 print 命令的结果分配一个编号(如 $1, $2),你可以在后续的GDB会话中通过这些编号引用它们。

2.1.2 查看指针及其指向的值

main 函数中,我们有 int* my_array。当程序暂停在 process_array 调用之后,我们可以查看 my_array 指针本身的值(内存地址)和它所指向的内容。

(gdb) b 39
Breakpoint 3 at 0x55555555523a: file simple_program.c, line 39.
(gdb) c
Continuing.

Breakpoint 3, main () at simple_program.c:39
39      free(my_array); 
(gdb) print my_array
$4 = (int *) 0x5555555592a0 // my_array 的内存地址
(gdb) print *my_array
$5 = 0 // my_array[0] 的值
(gdb) print my_array[0]
$6 = 0
(gdb) print my_array[1]
$7 = 10
(gdb) print my_array[4]
$8 = 40

2.1.3 查看数组

要查看整个数组的内容,你可以使用 print <array_name>@<length>

(gdb) print my_array@5
$9 = {0, 10, 20, 30, 40} // 打印 my_array 从起始地址开始的5个int值

2.1.4 格式化打印

print 命令支持多种格式化输出,这在查看不同类型的数据时非常有用。

格式符 描述 示例
/x 十六进制 print/x x (显示 x 的十六进制值)
/d 十进制 print/d x (显示 x 的十进制值)
/u 无符号十进制 print/u x
/o 八进制 print/o x
/t 二进制 print/t x
/c 字符 print/c 65 (显示字符 ‘A’)
/s 字符串 print/s "hello" (显示字符串)

示例:

(gdb) print/x x
$10 = 0xa // 10的十六进制是A
(gdb) print/t x
$11 = 1010 // 10的二进制是1010
(gdb) print/c 65
$12 = 65 'A'

2.2 display 命令:自动追踪变量

如果你想在每次程序暂停时,自动打印某个变量的值,而不是每次都手动输入 print,那么 display 命令是你的好帮手。

语法: display <expression>

示例:追踪 i 变量在循环中的变化

我们来调试 process_array 函数,看看循环变量 i 的变化。

(gdb) b process_array
Breakpoint 4 at 0x5555555551c6: file simple_program.c, line 10.
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /path/to/simple_program 

Breakpoint 1, main () at simple_program.c:23
23      int x = 10;
(gdb) c
Continuing.

Breakpoint 4, process_array (arr=0x5555555592a0, size=5) at simple_program.c:10
10      printf("Processing array elements:n");
(gdb) n
Processing array elements:
11      for (int i = 0; i <= size; ++i) { 
(gdb) display i
1: i = 0
(gdb) display size
2: size = 5
(gdb) n
12          if (i < size) {
1: i = 0
2: size = 5
(gdb) n
13              printf("arr[%d] = %dn", i, arr[i]);
1: i = 0
2: size = 5
(gdb) c
Continuing.
arr[0] = 0

Breakpoint 4, process_array (arr=0x5555555592a0, size=5) at simple_program.c:11
11      for (int i = 0; i <= size; ++i) { 
1: i = 1
2: size = 5
(gdb) c
Continuing.
arr[1] = 10

Breakpoint 4, process_array (arr=0x5555555592a0, size=5) at simple_program.c:11
11      for (int i = 0; i <= size; ++i) { 
1: i = 2
2: size = 5
(gdb) c
Continuing.
arr[2] = 20

Breakpoint 4, process_array (arr=0x5555555592a0, size=5) at simple_program.c:11
11      for (int i = 0; i <= size; ++i) { 
1: i = 3
2: size = 5
(gdb) c
Continuing.
arr[3] = 30

Breakpoint 4, process_array (arr=0x5555555592a0, size=5) at simple_program.c:11
11      for (int i = 0; i <= size; ++i) { 
1: i = 4
2: size = 5
(gdb) c
Continuing.
arr[4] = 40

Breakpoint 4, process_array (arr=0x5555555592a0, size=5) at simple_program.c:11
11      for (int i = 0; i <= size; ++i) { 
1: i = 5 // i 已经等于 size,但循环条件是 i <= size,这是潜在的越界访问!
2: size = 5
(gdb) n
12          if (i < size) { // 条件 i < size 为假
1: i = 5
2: size = 5
(gdb) n
15              printf("Attempting to access arr[%d] (out of bounds for size %d)n", i, size);
1: i = 5
2: size = 5
(gdb) c
Continuing.
Attempting to access arr[5] (out of bounds for size 5)

通过 display 命令,我们清晰地看到了 i 如何从0变化到5,最终导致了数组越界访问的提示信息。这是一个典型的“差一错误”(off-by-one error)。

管理 display 列表:

  • info display: 查看所有正在 display 的表达式。
  • undisplay <display_number>: 停止自动显示某个表达式。
  • disable display <display_number>: 禁用某个表达式的自动显示(但不移除)。
  • enable display <display_number>: 重新启用。

2.3 x 命令:检查内存内容

有时候,你可能想直接查看某个内存地址的内容,而不是通过变量名。这在处理指针、内存损坏或汇编级别调试时尤其有用。x (examine) 命令允许你做到这一点。

语法: x/<nfu> <address>

  • n: (可选) 要显示的内存单元数量。默认为1。
  • f: (可选) 格式。与 print 类似,如 x (十六进制), d (十进制), u (无符号十进制), o (八进制), t (二进制), a (地址), c (字符), s (字符串), i (机器指令)。默认为上次使用的格式。
  • u: (可选) 内存单元大小。
    • b: byte (1字节)
    • h: halfword (2字节)
    • w: word (4字节)
    • g: giant word (8字节)
      默认为上次使用的大小。

示例:检查 my_array 的内存

当程序暂停在 free(my_array); 之前,my_array 指向的是我们分配的5个 int 元素的内存块。

(gdb) b 39
Breakpoint 3 at 0x55555555523a: file simple_program.c, line 39.
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /path/to/simple_program 

Breakpoint 1, main () at simple_program.c:23
23      int x = 10;
(gdb) c
Continuing.

Breakpoint 3, main () at simple_program.c:39
39      free(my_array);
(gdb) print my_array
$13 = (int *) 0x5555555592a0
(gdb) x/5xw my_array // 从 my_array 地址开始,以十六进制格式显示5个word (4字节)
0x5555555592a0: 0x00000000  0x0000000a  0x00000014  0x0000001e
0x5555555592b0: 0x00000028

这里 0x00000000 是0,0x0000000a 是10,0x00000014 是20,0x0000001e 是30,0x00000028 是40。这与我们用 print my_array@5 看到的结果完全一致。

2.3.1 检查栈上的局部变量内存

我们可以通过 print &x 获取变量 x 的地址,然后用 x 命令查看其内存内容。

(gdb) b main
Breakpoint 1 at 0x555555555183: file simple_program.c, line 23.
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /path/to/simple_program 

Breakpoint 1, main () at simple_program.c:23
23      int x = 10;
(gdb) n
24      int y = 20;
(gdb) print &x
$14 = (int *) 0x7fffffffdc2c // 变量 x 在栈上的地址
(gdb) x/d &x // 以十进制格式查看 x 的值
0x7fffffffdc2c: 10
(gdb) x/x &x // 以十六进制格式查看 x 的值
0x7fffffffdc2c: 0x0000000a

2.4 set variable 命令:运行时修改变量值

GDB不仅能让你查看变量,还能让你在程序暂停时修改它们的值。这对于测试不同输入、修复临时错误或跳过某些逻辑非常有用。

语法: set variable <variable_name> = <new_value>

示例:修改 x 的值

假设我们想看看如果 x 是100,calculate_sum 会返回什么。

(gdb) b 25
Breakpoint 5 at 0x55555555518e: file simple_program.c, line 25.
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /path/to/simple_program 

Breakpoint 1, main () at simple_program.c:23
23      int x = 10;
(gdb) c
Continuing.

Breakpoint 5, main () at simple_program.c:25
25      int result = calculate_sum(x, y);
(gdb) print x
$15 = 10
(gdb) set variable x = 100
(gdb) print x
$16 = 100
(gdb) s // 进入 calculate_sum
calculate_sum (a=100, b=20) at simple_program.c:6
6       int sum = a + b;
(gdb) n
7       return sum;
(gdb) print sum
$17 = 120
(gdb) finish
Run till exit from #0  calculate_sum (a=100, b=20) at simple_program.c:7
0x0000555555555193 in main () at simple_program.c:25
25      int result = calculate_sum(x, y);
Value returned is $18 = 120
(gdb) print result
$19 = 120

可以看到,x 的值成功被修改,并且 calculate_sum 函数也使用了新的 x 值进行计算。


第三章:追踪函数调用栈——理解程序执行路径

程序在执行过程中,函数会一层一层地调用。当一个函数调用另一个函数时,当前的执行上下文(包括参数、局部变量、返回地址等)会被“压入”一个称为“调用栈”(Call Stack)的内存区域。当函数返回时,其上下文会被“弹出”。

理解调用栈对于理解程序的执行路径、定位错误来源至关重要,尤其是当程序崩溃时,调用栈可以告诉你崩溃发生之前,程序都经过了哪些函数。

3.1 backtrace 命令:查看整个调用栈

backtrace (缩写 bt) 命令是GDB中查看调用栈最核心的命令。它会显示从当前执行点到 main 函数(或程序入口点)的所有函数调用。

语法: backtracebt
更详细的语法: backtrace fullbt full (会显示每个栈帧的局部变量和参数)

让我们故意让程序在 process_array 函数内部暂停,然后查看调用栈。

(gdb) b 13 // 在 process_array 内部设置断点
Breakpoint 6 at 0x5555555551e1: file simple_program.c, line 13.
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /path/to/simple_program 

Breakpoint 1, main () at simple_program.c:23
23      int x = 10;
(gdb) c
Continuing.

Breakpoint 6, process_array (arr=0x5555555592a0, size=5) at simple_program.c:13
13              printf("arr[%d] = %dn", i, arr[i]);
(gdb) bt
#0  process_array (arr=0x5555555592a0, size=5) at simple_program.c:13
#1  0x0000555555555235 in main () at simple_program.c:37
#2  0x00007ffff7a06d0a in __libc_start_call_main (main=0x555555555160 <main>, argc=1, argv=0x7fffffffde18) at ../sysdeps/nptl/libc_start_call_main.h:58
#3  0x00007ffff7a06dbc in __libc_start_main_impl (main=0x555555555160 <main>, argc=1, argv=0x7fffffffde18, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffde08) at ../csu/libc-start.c:360
#4  0x00005555555550a5 in _start ()

解析 backtrace 输出:

  • 每一行代表一个“栈帧”(Stack Frame),也就是一个函数调用。
  • #0 是当前正在执行的函数(最顶层)。
  • #1 是调用 #0 的函数。
  • 依此类推,直到程序入口点(通常是 main 函数,再往上是系统启动代码)。
  • 每帧会显示函数名、参数值(如果GDB能解析),以及源代码文件名和行号。

从上面的输出可以看出:

  • 当前程序执行在 process_array 函数的第13行。
  • process_array 是由 main 函数在 simple_program.c 的第37行调用的。
  • 再往上是C标准库的启动函数,最终由 _start 启动。

使用 bt full 查看详细信息:

(gdb) bt full
#0  process_array (arr=0x5555555592a0, size=5) at simple_program.c:13
    i = 0
#1  0x0000555555555235 in main () at simple_program.c:37
    data_size = 5
    my_array = 0x5555555592a0
    x = 10
    y = 20
    result = 30
#2  0x00007ffff7a06d0a in __libc_start_call_main (main=0x555555555160 <main>, argc=1, argv=0x7fffffffde18) at ../sysdeps/nptl/libc_start_call_main.h:58
    ret = <optimized out>
    main_map = <optimized out>
#3  0x00007ffff7a06dbc in __libc_start_main_impl (main=0x555555555160 <main>, argc=1, argv=0x7fffffffde18, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffde08) at ../csu/libc-start.c:360
    __invoke_main = <optimized out>
    __libc_csu_init = <optimized out>
    __libc_csu_fini = <optimized out>
    __rtld_fini = <optimized out>
    unwind_buf = {__cancel_jmp_buf = {{__jmpbuf = {140737488340992, 4016503730704029288, 140737488340960, 140737488340968, 140737488340976, 140737488340984, -481440628}, __mask_was_saved = 0}}, __setjmp_result = 0}
#4  0x00005555555550a5 in _start ()

bt full 显示了每个栈帧中的局部变量和参数值,这对于理解每个函数调用时的具体状态非常有用。例如,在 main 函数的栈帧中,我们可以看到 x=10, y=20, result=30, my_array=0x..., data_size=5

3.2 frame 命令:切换和查看栈帧

backtrace 让你看到整个调用栈,而 frame (缩写 f) 命令则允许你在这些栈帧之间切换,以便查看不同函数调用上下文中的变量和状态。

语法:

  • frame <frame_number>: 切换到指定的栈帧。
  • frame: 显示当前栈帧的信息。
  • info frame: 显示当前栈帧的详细信息。

示例:在 process_arraymain 之间切换

我们继续停留在 process_array 函数的第13行。

(gdb) b 13
Breakpoint 6 at 0x5555555551e1: file simple_program.c, line 13.
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /path/to/simple_program 

Breakpoint 1, main () at simple_program.c:23
23      int x = 10;
(gdb) c
Continuing.

Breakpoint 6, process_array (arr=0x5555555592a0, size=5) at simple_program.c:13
13              printf("arr[%d] = %dn", i, arr[i]);
(gdb) frame // 查看当前栈帧
#0  process_array (arr=0x5555555592a0, size=5) at simple_program.c:13
13              printf("arr[%d] = %dn", i, arr[i]);
(gdb) print i // 查看当前栈帧的局部变量
$20 = 0
(gdb) frame 1 // 切换到调用者 main 函数的栈帧
#1  0x0000555555555235 in main () at simple_program.c:37
37      process_array(my_array, data_size);
(gdb) print x // 查看 main 函数的局部变量
$21 = 10
(gdb) print my_array // 查看 main 函数的局部变量
$22 = (int *) 0x5555555592a0
(gdb) frame 0 // 切换回 process_array 栈帧
#0  process_array (arr=0x5555555592a0, size=5) at simple_program.c:13
13              printf("arr[%d] = %dn", i, arr[i]);
(gdb) print x // 此时在 process_array 栈帧中,x 是 main 的局部变量,不可访问
No symbol "x" in current context.

可以看到,当你切换栈帧后,print 命令会查询当前栈帧上下文中的变量。这正是 frame 命令的强大之处:让你能够完整地检查程序在不同调用层级的状态。

3.3 updown 命令:相对移动栈帧

updown 命令提供了一种更方便的方式来在栈帧之间移动,特别是当你不想记住具体的帧编号时。

语法:

  • up <n>: 向上移动 n 个栈帧(向调用者方向)。n 默认为1。
  • down <n>: 向下移动 n 个栈帧(向被调用者方向)。n 默认为1。

继续以上面的例子为例:

(gdb) frame // 当前在 process_array (#0)
#0  process_array (arr=0x5555555592a0, size=5) at simple_program.c:13
13              printf("arr[%d] = %dn", i, arr[i]);
(gdb) up // 向上移动一个栈帧,到达 main (#1)
#1  0x0000555555555235 in main () at simple_program.c:37
37      process_array(my_array, data_size);
(gdb) print x
$23 = 10
(gdb) down // 向下移动一个栈帧,回到 process_array (#0)
#0  process_array (arr=0x5555555592a0, size=5) at simple_program.c:13
13              printf("arr[%d] = %dn", i, arr[i]);
(gdb) print i
$24 = 0

3.4 info argsinfo locals:快速查看参数和局部变量

当你在某个栈帧中时,GDB提供了专门的命令来快速查看该函数的所有参数和局部变量。

  • info args: 显示当前栈帧函数的所有参数。
  • info locals: 显示当前栈帧函数的所有局部变量。

继续在 process_array 栈帧 (#0):

(gdb) frame 0
#0  process_array (arr=0x5555555592a0, size=5) at simple_program.c:13
13              printf("arr[%d] = %dn", i, arr[i]);
(gdb) info args
arr = (int *) 0x5555555592a0
size = 5
(gdb) info locals
i = 0

切换到 main 栈帧 (#1):

(gdb) frame 1
#1  0x0000555555555235 in main () at simple_program.c:37
37      process_array(my_array, data_size);
(gdb) info args // main 函数没有参数
No arguments.
(gdb) info locals
x = 10
y = 20
result = 30
data_size = 5
my_array = (int *) 0x5555555592a0

这些命令大大简化了在复杂函数中查找参数和局部变量的过程。


第四章:GDB进阶技巧与最佳实践

掌握了变量查看和栈追踪后,我们可以再了解一些GDB的进阶功能,它们能让你更高效地定位问题。

4.1 观察点(Watchpoints):当变量改变时暂停

断点在代码行上暂停,而观察点则在变量值改变时暂停。这对于调试那些值被意外修改的变量非常有用。

语法:

  • watch <expression>: 当 expression 的值改变时暂停。
  • rwatch <expression>: 当 expression 被读取时暂停。
  • awatch <expression>: 当 expression 被读取或写入时暂停。

示例:观察 i 的变化

我们来观察 process_array 函数中 i 变量的变化。

(gdb) b process_array
Breakpoint 4 at 0x5555555551c6: file simple_program.c, line 10.
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /path/to/simple_program 

Breakpoint 1, main () at simple_program.c:23
23      int x = 10;
(gdb) c
Continuing.

Breakpoint 4, process_array (arr=0x5555555592a0, size=5) at simple_program.c:10
10      printf("Processing array elements:n");
(gdb) n
Processing array elements:
11      for (int i = 0; i <= size; ++i) { 
(gdb) watch i
Hardware watchpoint 7: i // GDB会尝试使用硬件观察点,效率更高
(gdb) c
Continuing.
Hardware watchpoint 7: i

Old value = 0
New value = 1
0x00005555555551d0 in process_array (arr=0x5555555592a0, size=5) at simple_program.c:11
11      for (int i = 0; i <= size; ++i) { 
(gdb) c
Continuing.
arr[0] = 0
Hardware watchpoint 7: i

Old value = 1
New value = 2
0x00005555555551d0 in process_array (arr=0x5555555592a0, size=5) at simple_program.c:11
11      for (int i = 0; i <= size; ++i) { 

每次 i 的值改变时,GDB都会暂停并报告旧值和新值,这非常方便!

管理观察点:

  • info watchpoints: 查看所有观察点。
  • delete <watchpoint_number>: 删除观察点。

4.2 条件断点:只在特定条件下暂停

有时你只想在某个变量达到特定值,或者某个表达式为真时才暂停程序。条件断点允许你设置一个条件,只有当条件满足时,断点才会触发。

语法: break <location> if <condition>

示例:只在 i == 5 时暂停

我们让 process_array 函数只在 i 达到5时暂停。

(gdb) b 11 if i == 5
Breakpoint 8 at 0x5555555551d0: file simple_program.c, line 11.
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /path/to/simple_program 

Breakpoint 1, main () at simple_program.c:23
23      int x = 10;
(gdb) c
Continuing.
Processing array elements:
arr[0] = 0
arr[1] = 10
arr[2] = 20
arr[3] = 30
arr[4] = 40

Breakpoint 8, process_array (arr=0x5555555592a0, size=5) at simple_program.c:11
11      for (int i = 0; i <= size; ++i) { 
(gdb) print i
$25 = 5

程序直接跳过了 i=0i=4 的循环,直接在 i=5 时暂停。这对于调试循环中的特定迭代或处理大数据集时非常有用。

4.3 断点命令列表:自动化调试动作

你可以为断点设置一系列的GDB命令,当断点触发时,这些命令会自动执行。这可以用来打印变量、继续执行,或者执行其他操作,而无需手动干预。

语法:

commands <breakpoint_number>
<command1>
<command2>
...
end

示例:当 i 变化时自动打印并继续

(gdb) b 11 // 在循环开始处设置断点
Breakpoint 9 at 0x5555555551d0: file simple_program.c, line 11.
(gdb) commands 9
Type commands for breakpoint 9, one per line.
End with a line containing just "end".
>print i
>c
>end
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /path/to/simple_program 

Breakpoint 1, main () at simple_program.c:23
23      int x = 10;
(gdb) c
Continuing.
Processing array elements:
Breakpoint 9, process_array (arr=0x5555555592a0, size=5) at simple_program.c:11
11      for (int i = 0; i <= size; ++i) { 
$26 = 0
Continuing.
arr[0] = 0
Breakpoint 9, process_array (arr=0x5555555592a0, size=5) at simple_program.c:11
11      for (int i = 0; i <= size; ++i) { 
$27 = 1
Continuing.
arr[1] = 10
... (会一直打印 i 的值并继续,直到循环结束或遇到其他断点)

4.4 TUI模式:文本用户界面

GDB提供了一个文本用户界面(TUI),可以将源代码、汇编代码和GDB命令窗口同时显示在终端中,提供更直观的调试体验。

进入TUI模式: layout srcCtrl+x a
退出TUI模式: layout offCtrl+x a

(gdb) layout src

这将把屏幕分成两部分,上面显示源代码,下面是GDB的命令行。这对于同时跟踪代码执行和GDB输出非常有用。

4.5 查看源代码:list 命令

虽然TUI模式很方便,但你也可以随时使用 list (缩写 l) 命令来查看源代码。

  • list: 显示当前行附近的源代码。
  • list <line_number>: 显示指定行号附近的源代码。
  • list <function_name>: 显示指定函数的源代码。
  • list -: 显示前一个 list 范围之前的代码。
(gdb) list
8   }
9   
10  void process_array(int* arr, int size) {
11      printf("Processing array elements:n");
12      for (int i = 0; i <= size; ++i) { // Potential off-by-one error
13          if (i < size) {
14              printf("arr[%d] = %dn", i, arr[i]);
15          } else {
16              printf("Attempting to access arr[%d] (out of bounds for size %d)n", i, size);

第五章:GDB常用命令速查表

为了方便大家快速查阅,我为大家整理了本次讲座中涉及的主要GDB命令:

表 5.1:GDB程序执行控制命令

命令 缩写 描述
run r 启动或重新启动程序。
continue c 从当前暂停点继续执行。
next n 单步执行,不进入函数。
step s 单步执行,进入函数。
finish 运行到当前函数结束。
until u 运行到指定行或跳出循环。
quit q 退出GDB。

表 5.2:GDB断点与观察点命令

命令 缩写 描述
break <location> b 在指定位置设置断点(文件:行号, 函数名)。
break <location> if <condition> 设置条件断点。
info breakpoints info b 查看所有断点。
delete <breakpoint_number> d 删除断点。
enable <breakpoint_number> 启用断点。
disable <breakpoint_number> 禁用断点。
commands <breakpoint_number> 为断点设置命令列表。
watch <expression> 当表达式值改变时暂停。
rwatch <expression> 当表达式被读取时暂停。
awatch <expression> 当表达式被读取或写入时暂停。
info watchpoints 查看所有观察点。

表 5.3:GDB变量查看命令

命令 缩写 描述
print <expression> p 打印表达式的值。
print/<format> <expression> 格式化打印表达式的值(如 /x, /d, /c, /s)。
print <array>@<length> 打印数组的指定长度元素。
display <expression> 每次程序暂停时自动打印表达式的值。
info display 查看所有自动显示的表达式。
undisplay <display_number> 停止自动显示表达式。
x/<nfu> <address> 检查指定内存地址的内容。
set variable <var> = <value> 修改变量的值。

表 5.4:GDB调用栈追踪命令

命令 缩写 描述
backtrace bt 显示整个函数调用栈。
backtrace full bt full 显示带有局部变量和参数的完整调用栈。
frame <number> f 切换到指定的栈帧。
frame 显示当前栈帧的信息。
up <n> 向上移动 n 个栈帧(向调用者方向)。
down <n> 向下移动 n 个栈帧(向被调用者方向)。
info args 显示当前函数的所有参数。
info locals 显示当前函数的所有局部变量。

表 5.5:GDB源代码查看命令

命令 缩写 描述
list l 显示当前行附近的源代码。
list <line_number> 显示指定行号附近的源代码。
list <function_name> 显示指定函数的源代码。
layout src 切换到TUI模式,显示源代码和GDB命令行。

结束语

恭喜各位,通过今天的讲座,你们已经掌握了GDB最核心、最实用的调试技能。从现在开始,当你的程序再次“罢工”时,你不再是束手无策的旁观者,而是能够拿起GDB这把利器,深入其内部,探查变量的奥秘,追踪函数的足迹,最终找出问题的根源并加以解决。

调试是一门艺术,也是一门科学。它需要耐心、细致和逻辑思维。GDB作为你的忠实伙伴,将极大提升你的问题解决能力。记住,实践是最好的老师,多动手,多尝试,你将越来越熟练。希望今天的分享能为你们的编程之路点亮一盏明灯,祝愿大家在编程世界中探索愉快,成就非凡!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注