C语言进阶教程:进阶教程:调试技巧:GDB/LLDB 的使用

调试是软件开发过程中不可或缺的一环,它能帮助我们找出程序中的错误(Bugs)并理解程序的行为。GDB (GNU Debugger) 和 LLDB (LLVM Debugger) 是两款功能强大的命令行调试器,广泛用于C、C++等语言的程序调试。

一、GDB (GNU Debugger)

GDB 是 GNU 项目的一部分,是类 Unix 系统下最常用的调试器。

1. 编译时加入调试信息

要使用 GDB 调试程序,编译时必须加入调试信息。使用 GCC 编译器时,添加 -g 选项:

 gcc -g my_program.c -o my_program

不加 -g 选项编译出来的程序也能被 GDB 加载,但很多调试功能(如查看源码、变量名等)将无法使用。

2. 启动 GDB

 gdb ./my_program

这将启动 GDB 并加载 my_program 可执行文件。你会看到 GDB 的提示符 (gdb)

3. GDB 常用命令

  • 运行程序 (run, r)
    • run [args]: 运行已加载的程序,可以带参数 args
    • start [args]: 开始执行程序,并在 main 函数的第一行停下来。
  • 断点 (breakpoint, b)
    • break <location>b <location>: 设置断点。
      • b main: 在 main 函数入口处设置断点。
      • b my_program.c:10: 在 my_program.c 文件的第 10 行设置断点。
      • b my_function: 在 my_function 函数入口处设置断点。
      • b *0x12345678: 在内存地址 0x12345678 处设置断点。
    • info breakpointsi b: 查看已设置的断点信息。
    • delete <breakpoint_id>d <breakpoint_id>: 删除指定 ID 的断点。
    • clear <location>: 清除指定位置的断点。
    • disable <breakpoint_id>: 禁用断点。
    • enable <breakpoint_id>: 启用断点。
    • tbreak <location>: 设置临时断点,命中一次后自动删除。
    • rbreak <regex>: 在所有匹配正则表达式的函数名上设置断点。
    • 条件断点: break <location> if <condition>
      • b my_program.c:10 if i == 5: 当变量 i 等于 5 时,在第 10 行断点处停下。
  • 单步执行 (stepping)
    • nextn: 执行下一行代码(如果当前行是函数调用,则执行整个函数,不进入函数内部)。
    • steps: 执行下一行代码(如果当前行是函数调用,则进入函数内部)。
    • finish: 继续执行直到当前函数返回。
    • until <location>: 继续执行直到程序到达指定位置(或跳出当前循环)。
      • until my_program.c:20
  • 继续执行 (continue, c)
    • continuec: 继续执行程序,直到遇到下一个断点或程序结束。
  • 查看数据 (print, p, display, x)
    • print <expression>p <expression>: 打印表达式的值。
      • p my_var
      • p my_array[0]
      • p *my_ptr
      • p my_struct.member
      • p my_function(arg1, arg2) (会实际调用函数)
      • p/x my_var: 以十六进制格式打印。
      • p/t my_var: 以二进制格式打印。
      • p/c my_var: 以字符格式打印。
    • display <expression>: 每次程序暂停时自动打印表达式的值。
      • info display: 查看 display 列表。
      • undisplay <display_id>: 取消自动显示。
    • x/<nfu> <address>: 检查内存内容 (examine)。
      • n: 显示的单元数量。
      • f: 格式 (如 x 十六进制, d 十进制, s 字符串, i 指令)。
      • u: 单元大小 (如 b byte, h halfword, w word, g giant word)。
      • x/10wx &my_array: 从 my_array 地址开始,显示 10 个 word (4字节) 的十六进制值。
  • 查看栈信息 (backtrace, bt, frame, up, down)
    • backtracebt: 显示当前的函数调用栈。
    • bt full: 显示更详细的调用栈信息,包括局部变量。
    • frame <frame_num>f <frame_num>: 选择指定的栈帧。栈顶为 0。
    • up <n>: 向上移动 n 个栈帧。
    • down <n>: 向下移动 n 个栈帧。
    • info frame: 显示当前栈帧的详细信息。
    • info locals: 显示当前栈帧的局部变量。
    • info args: 显示当前栈帧的函数参数。
  • 查看源代码 (list, l)
    • listl: 显示当前停止点附近的源代码。
    • list <line_num>: 显示指定行号附近的源代码。
    • list <function_name>: 显示指定函数开始处的源代码。
    • list <filename>:<line_num>
    • set listsize <count>: 设置 list 命令显示的行数。
  • 修改变量值 (set variable)
    • set variable <variable_name> = <value>
      • set var my_var = 10
  • 监视点 (watchpoint)
    • watch <expression>: 当表达式的值改变时暂停程序。
      • watch my_var
    • rwatch <expression>: 当表达式被读取时暂停程序。
    • awatch <expression>: 当表达式被读取或写入时暂停程序。
    • info watchpoints: 查看监视点。
  • GDB TUI (Text User Interface)
    • 在 GDB 启动后或运行时,按 Ctrl + x 然后按 a (或者 Ctrl + x Ctrl + a) 可以切换到 TUI 模式。
    • TUI 模式会在终端中分割出源码窗口、汇编窗口、命令窗口等。
    • Ctrl + x 1: 单窗口模式(源码或汇编)。
    • Ctrl + x 2: 双窗口模式(源码/汇编 + 命令)。
    • layout src: 显示源码窗口。
    • layout asm: 显示汇编窗口。
    • layout regs: 显示寄存器窗口。
    • focus cmd/src/asm/regs: 切换焦点到不同窗口。
  • 其他
    • help [command]: 获取帮助信息。
    • quitq: 退出 GDB。
    • shell <command>: 执行 shell 命令。
    • attach <pid>: 附加到已在运行的进程。
    • detach: 脱离当前附加的进程。
    • set disassembly-flavor intel/att: 设置汇编语法风格 (Intel 或 AT&T)。

4. .gdbinit文件

可以在用户主目录 (~/.gdbinit) 或项目目录 (./.gdbinit) 创建此文件,用于存放 GDB 启动时自动执行的命令,如设置默认的汇编风格、自定义命令等。

 # ~/.gdbinit 示例
 set disassembly-flavor intel
 set history save on
 set history filename ~/.gdb_history
 set print pretty on
 
 # 自动显示源码窗口
 # layout src

二、LLDB (LLVM Debugger)

LLDB 是 LLVM 项目的一部分,是 macOS 上 Clang 编译器的默认调试器,也可用于 Linux 和 Windows。 LLDB 的命令设计上与 GDB 有很多相似之处,但也有些不同。

1. 编译时加入调试信息

使用 Clang 编译器时,同样添加 -g 选项:

 clang -g my_program.c -o my_program

2. 启动 LLDB

 lldb ./my_program

LLDB 启动后会创建一个目标 (target),并进入 LLDB 提示符 (lldb)

3. LLDB 常用命令

LLDB 的命令结构通常是 <noun> <verb> [-options] [args]

  • 运行程序
    • run [args]process launch [args]
    • process launch --stop-at-entryr --stop-at-entry: 类似 GDB 的 start
  • 断点 (breakpoint)
    • breakpoint set <options>b <options>
      • b main: 在 main 函数设置断点。
      • b my_program.c:10: 在文件第 10 行设置断点。
      • b -n my_function: 按函数名设置断点。
      • b -a 0x12345678: 按地址设置断点。
    • breakpoint listbr l: 查看断点。
    • breakpoint delete <id>br del <id>: 删除断点。
    • breakpoint disable <id> / breakpoint enable <id>: 禁用/启用断点。
    • 条件断点: breakpoint set --condition <expression> <location>
      • b my_program.c:10 -c "i == 5"
  • 单步执行 (thread step-...)
    • nextnthread step-over: 执行下一行(不进入函数)。
    • stepsthread step-in: 执行下一行(进入函数)。
    • finishthread step-out: 执行直到当前函数返回。
    • thread until <line_num>: 执行到指定行。
  • 继续执行 (continue, c)
    • continuecprocess continue
  • 查看数据 (print, p, frame variable, memory read)
    • print <expression>p <expression>expr <expression>: 打印表达式的值。
      • p my_var
      • p/x my_var (十六进制)
      • p/t my_var (二进制)
      • p (char)my_char_var (字符)
    • frame variable [variable_name]fr v [variable_name]: 查看当前栈帧的变量。
      • fr v: 查看所有局部变量和参数。
      • fr v my_var: 查看特定变量。
    • watchpoint set expression <expression>: 设置监视点(当表达式的值改变时)。
      • watchpoint set expression my_var
    • watchpoint list: 查看监视点。
    • watchpoint delete <id>: 删除监视点。
    • memory read <address>x <address>: 检查内存内容。
      • memory read -fx -c16 -s4 &my_array: 从 my_array 地址开始,以十六进制格式 (-fx) 显示 16 个 (-c16) 4字节 (-s4) 单元。
  • 查看栈信息 (thread backtrace, bt, frame select)
    • thread backtracebt: 显示当前线程的函数调用栈。
    • frame select <frame_num>fr s <frame_num>: 选择指定的栈帧。
    • frame info: 显示当前栈帧信息。
  • 查看源代码 (source list, l)
    • source listl: 显示当前停止点附近的源代码。
    • source list -l <line_num>: 显示指定行号附近的源代码。
    • source list -n <function_name>: 显示指定函数开始处的源代码。
  • 修改变量值 (expression)
    • expression <variable_name> = <value>
      • expr my_var = 10
  • 其他
    • help [command]: 获取帮助信息。
    • quitexit: 退出 LLDB。
    • platform shell <command>: 执行 shell 命令。
    • process attach --pid <pid>attach -p <pid>: 附加到进程。
    • detach: 脱离进程。
    • settings set target.disassembly-flavor intel/att: 设置汇编风格。

4. .lldbinit文件

与 GDB 类似,LLDB 也有初始化文件:

  • ~/.lldbinit: 用户全局的初始化文件。
  • ./.lldbinit: 项目目录下的初始化文件(默认可能不加载,需要设置 target.load-cwd-lldbinit true)。
 # ~/.lldbinit 示例
 settings set target.disassembly-flavor intel
 settings set term-colors.enable true
 
 # 命令别名
 command alias py exec

三、GDB 与 LLDB 的简单对比

特性

GDB

LLDB

来源

GNU Project

LLVM Project

主要编译器

GCC

Clang

平台

Linux, macOS, Windows (MinGW/Cygwin)

macOS (default), Linux, Windows

命令风格

传统,简洁

结构化 (<noun> <verb>), 更一致

脚本

GDB Python API, Guile

LLDB Python API (更现代)

性能

在某些复杂场景下可能稍慢

通常被认为性能较好,尤其在处理大型调试信息时

表达式解析

C-like

C++/Objective-C like, 更强大

插件化

较弱

强,基于 LLVM 架构

对于大多数C语言调试任务,两者都能胜任。选择哪个通常取决于个人偏好、操作系统和使用的编译器。

四、调试技巧与策略

  1. 理解问题:在开始调试前,尝试复现问题,并明确你期望程序做什么,以及它实际做了什么。
  2. 缩小范围:通过二分法或逐步添加打印语句(或断点)来定位问题发生的代码区域。
  3. 检查边界条件:很多 bug 发生在数组边界、循环边界、空指针、最大/最小值等情况。
  4. 验证假设:使用调试器检查变量的值,验证你对程序状态的假设是否正确。
  5. 单步跟踪:仔细地单步执行有问题的代码段,观察变量如何变化。
  6. 查看调用栈:当程序崩溃或行为异常时,调用栈可以告诉你错误发生在哪条路径上。
  7. 使用条件断点和监视点:对于难以复现或只在特定条件下发生的问题非常有用。
  8. 不要害怕阅读汇编:有时,理解底层汇编代码可以帮助发现编译器优化或内存布局导致的问题。
  9. 版本控制:当修复 bug 时,确保你的代码在版本控制之下,方便回溯和比较。
  10. 编写可测试的代码:单元测试可以帮助你更快地发现和定位 bug。

掌握 GDB 或 LLDB 的基本使用是C程序员必备的技能。通过实践,你会越来越熟练地运用它们来解决各种复杂的程序问题。

原文链接:,转发请注明来源!