GDB 调试指南
[ English | 简体中文 ]
一、概述
本指南旨在为开发者提供一份全面而实用的 GNU Debugger (GDB) 操作手册。无论您是初学者还是希望深化嵌入式调试技能的开发者,都能从中获益。
1、GDB 是什么
GDB (GNU Debugger) 是 GNU 项目提供的标准化调试工具。它功能强大,能与 GCC 编译器紧密协作,帮助开发者深入理解和修复程序中的问题。
2、GDB 的核心能力
GDB 主要提供以下四项核心能力,赋予您完全控制程序执行的能力:
- 控制执行:按照您的设定启动并运行程序,包括传递命令行参数和设置环境变量。
- 设置断点:在代码的任意位置(如特定行号、函数入口)设置断点,或创建在满足特定条件时才触发的条件断点。
- 检查状态:当程序暂停时,您可以检查变量的当前值、查看内存内容、回溯函数调用栈以及检查寄存器状态。
- 动态修改:在运行时动态改变变量的值,或修改内存内容,以测试不同的代码路径和修复假设。
二、准备工作
在开始调试前,必须确保您的程序包含了 GDB 所需的调试信息。
1、编译时加入调试信息
使用 GCC 编译代码时,必须添加 -g 选项。为了获得最详尽的调试信息(包括宏定义),推荐使用 -g3:
# 示例编译命令
gcc -g3 -o my_program my_program.c
在 openvela 的构建系统中,您可以通过 Kconfig 开启调试选项,系统会自动为编译器添加合适的标志。
2、安装 GDB
对于跨平台嵌入式开发(例如,在 x86 主机上调试 ARM 目标板),您需要一个支持多架构的 GDB 版本。
# 在 Ubuntu/Debian 系统上安装 gdb-multiarch
sudo apt install gdb-multiarch
对于特定的 ARM 目标,您也可以使用对应的工具链中的 GDB,例如 arm-none-eabi-gdb。
三、GDB 核心命令参考
本章节将命令按功能分组,帮助您快速掌握 GDB 的核心操作。
1、启动与退出
| 命令 | 描述 |
|---|---|
gdb ./nuttx |
加载 nuttx 可执行文件并进入 GDB 交互界面。 |
attach <PID> |
附加到已在后台运行的进程。 您需要先用 ps -ef | grep nuttx 找到进程 ID (PID)。 |
target remote <IP>:<Port> |
在 GDB 内执行,用于连接到远程目标机上运行的 GDB Server。 例如, target remote :1234 连接到本地的 1234 端口。 |
gdb -q ./nuttx |
启动 GDB 但不显示冗长的版本和版权信息。 |
quit |
q,退出 GDB 会话。 |
kill |
终止被调试的程序。 |
2、控制程序执行
| 命令 | 描述 |
|---|---|
run |
r,启动或重新启动程序。程序将一直运行,直到遇到断点、观察点、异常或手动中断。 |
continue |
c,从当前暂停位置继续执行,直到下一个断点或程序结束。 |
next |
n,单步执行 (Step Over)。执行当前行代码。如果当前行是函数调用,它会执行整个函数,然后停在下一行。 |
step |
s,单步进入 (Step In)。如果当前行是函数调用,它会进入该函数内部并停在函数的第一行。 |
stepi |
si,单条指令执行。用于汇编级别的单步调试。 |
finish |
fin,完成当前函数。执行当前函数剩余部分,然后停在函数返回后的下一条语句。 |
(回车) |
重复执行上一条命令(对于 n、s 等命令非常有用)。 |
3、管理断点与观察点
断点使程序在特定位置暂停,观察点则在变量或内存地址被访问时暂停。
| 命令 | 描述 |
|---|---|
break <location> |
b,在指定位置设置断点。 location 可以是行号 (b main.c:25)、函数名 (b my_function) 或地址 (b *0xdeadbeef)。 |
break ... if <condition> |
设置条件断点。 仅当 condition 为真时,断点才会被触发。例如: b 15 if i == 10。 |
info breakpoints |
info b,显示所有断点及其状态(编号、是否启用、命中次数等)。 |
delete <num> |
del,删除指定编号的断点。若不带编号,则删除所有断点。 |
disable <num> |
dis,禁用指定编号的断点,但不会删除它。 |
enable <num> |
en,重新启用一个被禁用的断点。 |
watch <expr> |
设置观察点 (Write Watchpoint)。 当表达式 expr(通常是一个变量名或内存地址)的值被写入时,程序暂停。 |
rwatch <expr> |
设置读观察点 (Read Watchpoint)。 当 expr 被读取时,程序暂停。 |
awatch <expr> |
设置访问观察点 (Access Watchpoint)。 当 expr 被读取或写入时,程序暂停。 |
4、检查程序状态
当程序暂停时,这些命令帮助您探查问题所在。
| 命令 | 描述 |
|---|---|
print <expr> |
p,打印变量或表达式的值。 例如 p my_var 或 p *my_ptr。 |
x/<nfu> <addr> |
检查内存 (Examine)。 n 是数量 f 是格式 (x=十六进制, d=十进制, c=字符, s=字符串),u 是单位 (b=字节, h=半字, w=字, g=双字)例如, x/16xw 0x1000 表示从地址 0x1000 开始显示 16 个字的十六进制内容。 |
backtrace |
bt,显示当前的函数调用栈,帮助您追溯代码是如何执行到当前位置的。 |
frame <num> |
f,切换到指定编号的栈帧。 结合 bt 使用,可以查看调用栈中任意层级的局部变量和参数。 |
info locals |
显示当前栈帧中的所有局部变量。 |
info args |
显示当前栈帧中的所有函数参数。 |
info registers |
显示所有 CPU 寄存器的当前值。 |
info threads |
在多线程程序(如 openvela)中,显示所有线程及其 ID。 |
thread <id> |
切换到指定 ID 的线程上下文。 |
ptype <expr> |
显示变量或类型的数据结构定义。 例如 ptype struct my_struct。ptype /o 命令可以打印出指定类型的完整内存布局,清晰地展示每个成员的偏移量(offset)和大小(size)。 |
set var <name>=<value> |
在运行时修改变量的值。 例如 set var i = 10。不引起歧义时,可使用 set i = 10。 |
5、与源码和汇编交互
| 命令 | 描述 |
|---|---|
list |
l,显示当前位置附近的源代码。 |
layout src |
进入文本用户界面 (TUI) 模式,分屏显示源代码。 |
layout asm |
分屏显示汇编代码。 |
layout split |
分屏同时显示源代码和汇编代码。 在此模式下,可使用 focus 命令或快捷键 Ctrl + X + O 在不同窗口间切换焦点。 |
Ctrl + X, A |
退出 TUI 模式,返回标准 GDB 命令行界面。 |
disassemble <func> |
disas,反汇编指定的函数。/m 参数表示混合显示源代码与反汇编代码。 |
6、GDB 环境与 Shell 交互
| 命令 | 描述 |
|---|---|
help <command> |
显示指定 GDB 命令的帮助信息。 |
pipe <cmd> | <shell_cmd> |
将 GDB 命令 cmd 的输出通过管道传递给 Shell 命令。例如 pipe bt | less。 |
shell <shell_cmd> |
在 GDB 内部执行一个 Shell 命令。 例如 shell ls -l。 |
四、openvela 典型调试场景
本节将理论应用于实践,展示如何使用 GDB 解决在 openvela(尤其是 sim 和硬件)开发中遇到的具体问题。
场景一:分析程序崩溃(Hard Fault, Crash)
问题描述
程序在 sim 或开发板上运行时突然崩溃,例如出现 ASan 错误、段错误或硬件异常(Data Abort, Prefetch Abort)。
调试策略
- 复现问题:使用
gdb ./nuttx启动调试会话,然后输入r运行程序直至其崩溃。 - 定位崩溃点:程序崩溃后 GDB 会自动暂停。使用
bt命令查看调用栈,最顶层的栈帧通常就是导致崩溃的直接原因。 - 分析上下文:使用
frame <num>切换到可疑的栈帧,然后用p <var>和info locals检查当时的变量值,分析崩溃原因。 - 硬件异常分析:对于硬件异常,
info registers查看 PC (Program Counter), LR (Link Register) 等寄存器的值至关重要。使用disassemble /m <PC_value>可以查看崩溃时正在执行的汇编指令及其对应的源代码行。
场景二:程序卡死或死锁
问题描述
程序运行后终端无响应,CPU 占用率高,疑似进入死循环或死锁。
调试策略
-
中断程序:
- 若程序在前台运行,直接在 GDB 中按
Ctrl + C。 - 若程序在后台运行,先用
ps找到 PID,然后执行sudo gdb attach <PID>。附加成功后,程序会自动暂停。 - 对于
sim环境,也可在新终端执行pkill -SIGSTOP nuttx来暂停进程。
- 若程序在前台运行,直接在 GDB 中按
-
检查所有线程:输入
info threads查看所有线程的状态,是否有线程处于异常状态或都在等待某个资源。 -
分析每个线程:使用
thread <id>逐一切换到每个线程,然后用bt查看其调用栈,判断它们正在执行什么任务。这通常能快速定位到死循环或死锁的位置。
场景三:追踪变量的意外修改
问题描述
一个全局变量的值在某个时刻被错误地修改,但代码中有多处可能修改它的地方。
调试策略
- 设置观察点:启动 GDB 后,使用
watch my_global_variable或watch *<address_of_variable>为该变量设置一个写观察点。 - 运行并等待:输入
run或者continue启动或继续运行程序。当该变量的值被修改时,程序会立即暂停。 - 定位修改者:GDB 会报告变量的新旧值,并停在修改该变量的代码行。使用
bt查看调用栈,即可找到修改变量的代码。
场景四:分析被编译器优化的变量
问题描述
在开启优化(如 -O2, -O3)后,尝试用 print 查看某个局部变量时,GDB 提示 <optimized out>。
原因分析
编译器为了效率,可能已将该变量优化掉,或将其值存放在了 CPU 寄存器中,而不是内存(栈)中。
调试策略
-
查看变量地址信息:使用
info address your_var。 -
解读输出:GDB 会告诉您该变量当前位于何处。
- 如果它显示 "in register r5",意味着变量的值就在
r5寄存器中,您可以用p $r5来查看。 - 如果它显示在某个栈地址,您仍然可以尝试用
x/w <address>来查看内存。
- 如果它显示 "in register r5",意味着变量的值就在
-
终极手段:如果必须精确调试,请临时使用
-O0关闭优化重新编译。
场景五:远程调试 Coredump 文件
问题描述
服务器上编译的程序在目标设备上崩溃并生成 coredump,您需要在本地分析,但源码路径不匹配。
调试策略
-
加载 Coredump:
gdb ./nuttx /path/to/coredump -
查看原始路径:使用
info source查看编译时记录的源码路径。 -
映射路径:使用
set substitute-path <original_path> <local_path>命令进行路径替换。# 示例:将服务器路径映射到本地路径 set substitute-path /home/build-server/project/ /home/user/my_project/ -
开始分析:路径映射后,即可正常使用
bt、list等命令查看源码。
五、GDB 工作原理
1、本地调试
在本地调试中,GDB 和被调试的程序运行在同一台计算机上。GDB 通过操作系统提供的 ptrace (在 Linux 上) 等系统调用来控制和检查目标进程。
2、远程调试
远程调试是嵌入式开发中最常见的模式。它采用客户端/服务器 (Client/Server) 架构:
- GDB Client:运行在您的开发主机上(例如,您的 PC)。
- GDB Server:一个轻量级程序,运行在目标设备上(例如,ARM 开发板)。
- 通信协议:两者之间通过 GDB Remote Serial Protocol (RSP) 进行通信,通信介质可以是串口或网络 (TCP/IP)。
当您在 GDB Client 中输入 continue 时,Client 会将对应的 RSP 命令发送给 Server,Server 接收后控制目标程序继续执行。当程序遇到断点时,Server 会暂停程序,并将状态信息通过 RSP 回传给 Client 显示。
六、高级主题与相关实践
1、使用 GDB 脚本 (Command File)
当您需要重复执行一系列 GDB 命令时,可以将其写入一个文本文件(例如 my_setup.gdb),然后使用 source 命令加载:
source [-s] [-v] filename
-s表示在系统的PATH环境变量中搜索指定文件。-v表示开启详细模式,显示每条指令的执行过程。
更多信息请参考:Debugging with GDB - Command Files (gnu.org)
2、相关调试实践
- IDE 集成:关于如何在 VSCode 中配置 GDB 以调试
sim环境,请参考使用 VSCode 调试 SIM 环境。 - 线程感知调试:为了在 GDB 中更好地查看 openvela 的线程信息,可以利用 J-Link 的 GDB 插件,详情请参见使用 J-Link GDB 插件增强 openvela 线程调试。
七、故障排查 (Troubleshooting)
GDB 启动时报错 ModuleNotFoundError: No module named 'encodings'
问题原因
这通常是由于 GDB 依赖的 Python 版本与您系统中默认或环境变量中配置的 Python 版本不兼容导致的。GDB 内部使用 Python 脚本来增强功能(如 pretty-printing),如果找不到正确的 Python 环境,就会启动失败。
解决方案
-
确认 GDB 需要的 Python 版本:错误信息通常会暗示所需的版本(例如
python3.8)。 -
安装对应版本:确保您的系统中安装了该版本的 Python,可参考安装多个 Python 版本。
-
配置
PYTHONHOME:如果安装后问题依旧,尝试设置PYTHONHOME环境变量,强制 GDB 使用正确的 Python 解释器。
八、参考资料
- GDB Official Documentation (官方文档)
- GDB Command Cheat Sheet (常用命令速查表)
- Interrupt Blog: Debugging Firmware with GDB (面向嵌入式开发者的 GDB 优秀博文系列)