gdb 调试

一、GDB 是什么?

GDB 是一个由 GNU 开源组织发布的、功能强大的程序调试工具。它允许你查看程序在运行时的内部情况,或者在程序崩溃时正在做什么。它可以用于调试 C, C++, Go, Rust, Fortran 等多种语言编译的程序。

二、为什么要用 GDB?

  • 定位崩溃 (Segmentation Fault, Bus Error等):程序崩溃时,GDB 可以告诉你崩溃发生在哪一行代码。

  • 分析逻辑错误:可以一步步运行程序,观察变量值的变化,找出逻辑上的漏洞。

  • 检查核心转储 (Core Dump):即使程序已经终止,也可以通过其产生的 core 文件来事后分析崩溃原因。

  • 理解复杂程序:可以跟踪函数的调用流程,观察内存和寄存器的状态,帮助你理解他人的代码或复杂的系统交互。

  • 多线程/多进程调试:支持调试复杂的并发程序。

三、准备工作:编译带调试信息的程序

在使用 GDB 之前,必须在编译程序时加上 -g 选项。这个选项会让编译器在可执行文件中嵌入源代码和符号表等调试信息。

错误的方式(无法调试):

bash
gcc -o my_program my_program.c

正确的方式(可调试):

bash
gcc -g -o my_program my_program.c

对于 C++,使用 g++,同样需要 -g 选项。为了提高优化后代码的可调试性,有时会使用 -Og (优化调试体验) 或 -g3 (包含更多宏信息)。


四、启动 GDB 和基本命令

1. 启动 GDB

  • 调试一个程序:

    bash
    gdb ./my_program
  • 调试一个正在运行的进程:

    bash
    gdb -p <pid_of_my_program>
  • 调试一个核心转储文件:

    bash
    gdb ./my_program core
    # 或者
    gdb -c core ./my_program

2. 运行程序

  • run 或 r: 开始运行程序。可以在后面附带命令行参数。

    gdb
    (gdb) run arg1 arg2

3. 设置断点 (Breakpoint)

断点是调试的核心,它让程序在指定位置暂停。

  • break <location> 或 b <location>: 在某个位置设置断点。

    • b main: 在 main 函数入口设置断点。

    • b 10: 在当前文件的第 10 行设置断点。

    • b file.c:15: 在 file.c 文件的第 15 行设置断点。

    • b function_name: 在名为 function_name 的函数入口设置断点。

  • info breakpoints 或 i b: 列出所有已设置的断点。

  • delete <breakpoint_num> 或 d <breakpoint_num>: 删除指定编号的断点。delete 不加参数则删除所有断点。

  • disable/enable <breakpoint_num>: 暂时禁用/启用一个断点。

4. 控制程序执行

当程序在断点处暂停后,你可以控制它一步步执行。

  • next 或 n: 单步跳过。执行下一行代码,如果遇到函数调用,不会进入函数内部。

  • step 或 s: 单步进入。执行下一行代码,如果遇到函数调用,会进入该函数内部。

  • continue 或 c: 继续运行。从当前停止点继续运行,直到遇到下一个断点或程序结束。

  • finish 或 fin: 执行完当前函数。继续运行,直到当前函数返回,并打印返回值。

  • until <location>: 运行到指定行。常用于跳出循环。

5. 查看数据和状态

  • print <expression> 或 p <expression>: 打印变量或表达式的值。

    • p variable: 打印变量 variable 的值。

    • p *ptr@10: 打印指针 ptr 指向的 10 个元素(用于查看数组)。

    • p/x variable: 以十六进制格式打印变量。格式还有 d(十进制)、t(二进制)、c(字符)等。

  • display <expression>: 每次程序暂停时,自动打印指定表达式的值。

  • info locals: 打印当前栈帧的所有局部变量。

  • backtrace 或 bt: 显示调用栈。显示程序是如何执行到当前位置的(函数调用链)。这是分析崩溃的极其重要的命令。

    • bt full: 不仅显示调用栈,还显示每个栈帧的局部变量。

  • frame <frame_num> 或 f <frame_num>: 切换到调用栈的指定栈帧,然后可以查看该层的变量。

  • info registers: 显示所有寄存器的当前值(主要用于底层开发)。

6. 监视点 (Watchpoint)

监视点是一种特殊的断点,它会在某个表达式的值发生变化时暂停程序,非常适合追踪难以发现的变量修改。

  • watch <expression>: 设置一个监视点,当表达式的值被改变时暂停。

    • watch variable

  • rwatch <expression>: 当表达式被读取时暂停。

  • awatch <expression>: 当表达式被读取或改变时暂停。

7. 其他有用命令

  • list 或 l: 显示源代码。l 10 显示第 10 行周围的代码。

  • quit 或 q: 退出 GDB。

  • help <command>: 查看某个命令的帮助文档。


五、实战演示

假设我们有一个简单的、有错误的程序 buggy.c

c
#include <stdio.h>

int faulty_function(int *arr, int size) {
    int sum = 0;
    for (int i = 0; i <= size; i++) { // 错误:应该是 i < size
        sum += arr[i];                // 这里会发生数组越界
    }
    return sum;
}

int main() {
    int data[3] = {1, 2, 3};
    int result = faulty_function(data, 3);
    printf("Sum is: %d\n", result);
    return 0;
}

调试过程:

  1. 编译并启动 GDB

    bash
    gcc -g -o buggy buggy.c
    gdb ./buggy
  2. 在可能出错的地方设置断点

    gdb
    (gdb) b faulty_function
    Breakpoint 1 at 0x115d: file buggy.c, line 4.
  3. 运行程序

    gdb
    (gdb) run
    Starting program: /home/user/buggy
    
    Breakpoint 1, faulty_function (arr=0x7fffffffdf34, size=3) at buggy.c:4
    4           int sum = 0;
  4. 单步执行并观察变量

    gdb
    (gdb) n
    5           for (int i = 0; i <= size; i++) {
    (gdb) n
    6               sum += arr[i];
    (gdb) p i
    $1 = 0
    (gdb) p arr[i]
    $2 = 1
    (gdb) display i   # 让 GDB 每次暂停都自动显示 i 的值
    (gdb) display sum
  5. 继续执行,直到发现问题
    重复使用 n 或直接 c 继续,观察 i 和 sum 的变化。你会看到循环执行了 4 次 (i 从 0 到 3),而 data 数组只有 3 个元素。当 i=3 时,arr[3] 访问了非法的内存空间,这很可能导致程序崩溃或输出错误结果。

  6. 分析问题
    通过观察,你发现循环条件 i <= size 是错误的,应该是 i < size。问题定位成功!


六、图形化界面 (TUI) 和前端工具

虽然命令行 GDB 很强大,但图形界面有时更直观。

  • GDB 自带的 TUI 模式:
    启动时使用 gdb -tui ./my_program,或在 GDB 内按 Ctrl+X+A 切换。它会分屏显示源代码和调试命令。

  • 更现代的图形化前端:

    • CGDB: 类似于 GDB TUI,但更友好。

    • DDD: 数据显示调试器,功能非常强大。

    • IDE 集成: VS Code, CLion, Eclipse 等现代 IDE 都集成了图形化的 GDB 前端,提供了设置断点、查看变量等按钮操作,极大提升了易用性。它们底层调用的仍然是 GDB。

总结

GDB 的学习曲线稍陡,但一旦掌握,它就是你解决复杂问题的终极武器。建议从简单的程序开始练习,熟练掌握 runbreaknextstepprint, 和 backtrace 这几个核心命令,你就能解决大部分调试问题。随着经验的积累,再逐步学习更高级的功能如监视点、多线程调试等。

posted @ 2025-09-21 21:52  am2013  阅读(16)  评论(4)    收藏  举报