调试是软件开发过程中不可或缺的一环,它能帮助我们找出程序中的错误(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 breakpoints 或 i 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)
- next 或 n: 执行下一行代码(如果当前行是函数调用,则执行整个函数,不进入函数内部)。
- step 或 s: 执行下一行代码(如果当前行是函数调用,则进入函数内部)。
- finish: 继续执行直到当前函数返回。
- until <location>: 继续执行直到程序到达指定位置(或跳出当前循环)。
- until my_program.c:20
- 继续执行 (continue, c)
- continue 或 c: 继续执行程序,直到遇到下一个断点或程序结束。
- 查看数据 (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)
- backtrace 或 bt: 显示当前的函数调用栈。
- bt full: 显示更详细的调用栈信息,包括局部变量。
- frame <frame_num> 或 f <frame_num>: 选择指定的栈帧。栈顶为 0。
- up <n>: 向上移动 n 个栈帧。
- down <n>: 向下移动 n 个栈帧。
- info frame: 显示当前栈帧的详细信息。
- info locals: 显示当前栈帧的局部变量。
- info args: 显示当前栈帧的函数参数。
- 查看源代码 (list, l)
- list 或 l: 显示当前停止点附近的源代码。
- 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]: 获取帮助信息。
- quit 或 q: 退出 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-entry 或 r --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 list 或 br 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-...)
- next 或 n 或 thread step-over: 执行下一行(不进入函数)。
- step 或 s 或 thread step-in: 执行下一行(进入函数)。
- finish 或 thread step-out: 执行直到当前函数返回。
- thread until <line_num>: 执行到指定行。
- 继续执行 (continue, c)
- continue 或 c 或 process 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 backtrace 或 bt: 显示当前线程的函数调用栈。
- frame select <frame_num> 或 fr s <frame_num>: 选择指定的栈帧。
- frame info: 显示当前栈帧信息。
- 查看源代码 (source list, l)
- source list 或 l: 显示当前停止点附近的源代码。
- source list -l <line_num>: 显示指定行号附近的源代码。
- source list -n <function_name>: 显示指定函数开始处的源代码。
- 修改变量值 (expression)
- expression <variable_name> = <value>
- expr my_var = 10
- 其他
- help [command]: 获取帮助信息。
- quit 或 exit: 退出 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语言调试任务,两者都能胜任。选择哪个通常取决于个人偏好、操作系统和使用的编译器。
四、调试技巧与策略
- 理解问题:在开始调试前,尝试复现问题,并明确你期望程序做什么,以及它实际做了什么。
- 缩小范围:通过二分法或逐步添加打印语句(或断点)来定位问题发生的代码区域。
- 检查边界条件:很多 bug 发生在数组边界、循环边界、空指针、最大/最小值等情况。
- 验证假设:使用调试器检查变量的值,验证你对程序状态的假设是否正确。
- 单步跟踪:仔细地单步执行有问题的代码段,观察变量如何变化。
- 查看调用栈:当程序崩溃或行为异常时,调用栈可以告诉你错误发生在哪条路径上。
- 使用条件断点和监视点:对于难以复现或只在特定条件下发生的问题非常有用。
- 不要害怕阅读汇编:有时,理解底层汇编代码可以帮助发现编译器优化或内存布局导致的问题。
- 版本控制:当修复 bug 时,确保你的代码在版本控制之下,方便回溯和比较。
- 编写可测试的代码:单元测试可以帮助你更快地发现和定位 bug。
掌握 GDB 或 LLDB 的基本使用是C程序员必备的技能。通过实践,你会越来越熟练地运用它们来解决各种复杂的程序问题。