大家好,我是bug菌。
很久以前曾在公众号中写过一篇关于GDB调试上篇的文章。最近再用GDB,又攒下了一些新的心得与体会,于是赶紧把这下篇补全,希望对大家有所帮助。
GDB 实在是太强大了。在嵌入式世界里,无论是裸机、RTOS 还是 Linux 内核或应用层,它都能很好地适配:裸机调试可配合硬件调试器;RTOS(比如 FreeRTOS)有对应的 GDB 适配插件;Linux 应用和内核调试更是它的拿手好戏。这比许多专用工具都要灵活,而且完全免费。
1. 条件断点技巧
条件断点是 GDB 的一项强大功能,它能让断点仅在特定条件满足时才触发,从而显著提升调试效率。
1.1 条件断点设置
设置条件断点的基本语法如下:
(gdb) break <location> if <condition>
(gdb) b sum.c:10 if i == 5
条件断点的主要特点:
- 只有当条件表达式的值为真(非零)时,断点才会触发。
- 条件表达式可以是任何有效的 C/C++ 表达式。
- 可以直接使用程序中的变量、函数调用等。
1.2 为现有断点添加条件
如果你已经设置好了断点,也可以使用 condition 命令后期为其补充条件:
(gdb) condition <breakpoint number> <expression>
(gdb) condition 1 i > 10
这在调试循环中的特定场景时特别管用。比如在一个很大的循环里,我们通常只关心循环变量达到某个特定值时的程序状态。
1.3 忽略断点次数
ignore 命令允许为断点设置一个“忽略次数”,让断点在触发前自动跳过指定的次数:
(gdb) ignore <breakpoint number> <count>
(gdb) ignore 1 5
这个功能在以下场景中特别好用:
- 调试循环时,想跳过前几次迭代。
- 断点在循环中被反复触发,但你只关心某一次特定的迭代。
- 想提高调试效率,避免在不感兴趣的断点处频繁停下。
1.4 断点命令列表
GDB 允许你为一个断点绑定一系列命令,当该断点触发时,这些命令会被自动执行:
(gdb) break <location>
(gdb) commands <breakpoint number>
commands
> print variable
> continue
end
举个例子,我们可以创建一个断点,让它触发时自动打印某个变量的值然后继续运行:
(gdb) b select_sort
(gdb) commands 1
commands
> p arr[min_idx]
> c
end
这样一来,每次命中断点时,GDB 都会自动打印 arr[min_idx] 的值并继续执行,省去了你反复手动敲命令的麻烦。
2. 观察点 Watchpoint 的使用
观察点是调试内存相关问题的利器,它能帮你监控某个变量或表达式的值何时发生了变化。
2.1 观察点类型
GDB 支持三种类型的观察点:
写观察点(Watchpoint):
(gdb) watch <expression>
当表达式被写入(值发生改变)时暂停程序。
读观察点(Read Watchpoint):
(gdb) rwatch <expression>
当表达式被读取时暂停程序。
访问观察点(Access Watchpoint):
(gdb) awatch <expression>
当表达式无论是被读取还是写入,都会暂停程序。
2.2 观察点使用示例
假设我们调试的代码存在数组越界访问:
int buffer[10];
for (int i = 0; i <= 10; i++) {
buffer[i] = i; // 这里会访问buffer[10],越界
}
可以这样来定位问题:
-
在 buffer 数组的越界位置(第 11 个元素)设置观察点:
(gdb) watch buffer[10]
-
运行程序,当 buffer[10] 被访问时,程序便会自动停在出错的那一行。
-
使用 info watchpoints 可以查看所有观察点的状态。
2.3 观察点的限制
使用观察点时需要注意以下几点:
- 硬件限制:大多数系统的硬件观察点数量有限,通常是 4 个。
- 性能影响:软件观察点会显著拖慢程序运行速度,因为它需要单步执行并每次检查变量值。
- 数据类型限制:像
double 这种宽度较大的类型,可能因为超出了硬件支持的范围而无法设置硬件观察点。
3. 多线程调试
多线程程序的调试比单线程要复杂得多,好在 GDB 为此提供了一套专门的功能。
3.1 线程相关命令
查看所有线程:
(gdb) info threads
这条命令会显示所有线程的信息,包括线程 ID、当前状态、当前栈帧等。
切换线程:
(gdb) thread <thread-id>
切换到指定 ID 的线程,让你可以继续调试它。
对所有线程执行命令:
(gdb) thread apply all <command>
这让你能一次性地对所有线程执行某个 GDB 命令,比如查看所有线程的调用栈:
(gdb) thread apply all backtrace
线程特定断点:
(gdb) break <location> thread <thread-id>
只在某个特定线程中触发断点。
3.2 线程调度控制
调试多线程时,控制线程的调度逻辑至关重要。scheduler-locking 模式就是为此而生:
(gdb) set scheduler-locking <mode>
调度锁模式有三种:
- off:无锁,所有线程自由调度(这是默认行为)。
- on:只有当前线程继续运行,其他所有线程被暂停。
- step:在单步执行(如
step、next)时自动锁定调度器。
例如,当你想专心调试某个线程的内部逻辑而不被其他线程干扰时:
(gdb) set scheduler-locking on
这样,当你用 step 或 next 时,只有当前被调试的线程会执行,其他线程将保持冻结状态。
4. 信号处理调试
信号是 UNIX 系统中进程间通信的重要机制,GDB 对信号处理也提供了强大的调试支持。
4.1 查看信号处理
查看信号信息:
(gdb) info signals
这会显示所有信号的当前处理方式。
查看特定信号:
(gdb) info signal SIGINT
只查看指定信号(例如 SIGINT)的处理方式。
4.2 捕获信号
GDB 可以让你捕获特定信号,在信号发生时暂停程序:
(gdb) handle <signal> <action>
常用的 action 包括:
- stop:接收到信号时暂停程序。
- noprint:不打印信号相关信息。
- nostop:不暂停程序,但依然捕获信号。
- pass / nopass:决定是否将该信号传递给程序本身。
例如,要捕获最经典的 SIGSEGV(段错误)信号,可以这样:
(gdb) handle SIGSEGV stop
4.3 生成信号
在调试过程中,你也可以主动向被调试的程序发送信号,模拟某种场景:
(gdb) signal <signal>
例如,向程序发送一个中断信号 SIGINT:
(gdb) signal SIGINT
5. 远程调试
远程调试是 GDB 的另一大强大特性,它允许你在本地机器上调试运行在远端机器或嵌入式设备上的程序。
5.1 远程调试架构
远程调试使用 gdbserver 作为中间的代理:
- 目标机:运行
gdbserver 和被调试程序。
- 主机:运行 GDB,通过网络或串口连接到目标机上的
gdbserver。
5.2 启动 gdbserver
在目标机上启动 gdbserver 的基本命令是:
gdbserver <host:port> <program> [arguments]
比如,让它监听本地端口 1234:
gdbserver :1234 ./my_program
或者通过串口进行连接:
gdbserver /dev/ttyS0 ./my_program
5.3 连接到远程目标
在主机端,使用 GDB 连接远程目标:
(gdb) target remote <host:port>
例如,连接到 IP 为 192.168.1.100 的设备:
(gdb) target remote 192.168.1.100:1234
连接成功后,你就可以像调试本地程序一样使用标准的 GDB 命令了。
5.4 交叉调试
在嵌入式开发中,主机(如 x86)和目标机(如 ARM)往往架构不同,这就涉及到交叉调试。
-
首先,需要用交叉编译工具链编译目标程序:
arm-linux-gnueabihf-gcc -g -o my_program my_program.c
-
在 ARM 目标机上启动 gdbserver:
gdbserver :1234 ./my_program
-
最后,在 PC 主机上使用对应的交叉调试器进行连接:
arm-linux-gnueabihf-gdb
(gdb) target remote 192.168.1.100:1234
6. 内存调试技巧
内存问题大概是程序中最难排查的一类问题了。GDB 提供了一些基础的内存调试功能,但通常需要配合 Valgrind 等专门工具一起使用。
6.1 内存查看技巧
查看内存内容:
(gdb) x/20xb buffer
以字节形式查看数组或缓冲区的内容。
查看动态分配的内存:
(gdb) p *(int *)0x600850
通过地址来检查和打印动态分配的内存内容。
6.2 内存泄漏检测
GDB 本身并不直接支持内存泄漏检测,但可以通过一些手段辅助分析:
- 使用内存分配钩子函数。
- 跟踪
malloc / free 的调用。
- 分析内存分配的模式。
更有效的方法是使用 Valgrind 这一类的专业工具:
valgrind --tool=memcheck --leak-check=full ./program
6.3 缓冲区溢出调试
调试缓冲区溢出的常规步骤如下:
-
尝试定位内存越界的位置:
(gdb) watch buffer[10]
-
当程序被观察点停住后,分析调用栈:
(gdb) bt
-
检查内存状态,观察栈是否被破坏:
(gdb) x/20xw $esp
通过这一系列技巧,就能精准定位到导致缓冲区溢出的那行代码。
掌握这些 GDB 进阶技巧,无论是面对复杂的多线程竞争、诡异的内存越界,还是进行便捷的远程交叉调试,都能让你事半功倍。如果你在调试路上有什么独门心得,也欢迎常来云栈社区一同交流分享。