找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4105

积分

0

好友

539

主题
发表于 5 小时前 | 查看: 4| 回复: 0

大家好,我是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],越界
}

可以这样来定位问题:

  1. 在 buffer 数组的越界位置(第 11 个元素)设置观察点:

    (gdb) watch buffer[10]
  2. 运行程序,当 buffer[10] 被访问时,程序便会自动停在出错的那一行。

  3. 使用 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:在单步执行(如 stepnext)时自动锁定调度器。

例如,当你想专心调试某个线程的内部逻辑而不被其他线程干扰时:

(gdb) set scheduler-locking on

这样,当你用 stepnext 时,只有当前被调试的线程会执行,其他线程将保持冻结状态。

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)往往架构不同,这就涉及到交叉调试。

  1. 首先,需要用交叉编译工具链编译目标程序:

    arm-linux-gnueabihf-gcc -g -o my_program my_program.c
  2. 在 ARM 目标机上启动 gdbserver

    gdbserver :1234 ./my_program
  3. 最后,在 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 本身并不直接支持内存泄漏检测,但可以通过一些手段辅助分析:

  1. 使用内存分配钩子函数。
  2. 跟踪 malloc / free 的调用。
  3. 分析内存分配的模式。

更有效的方法是使用 Valgrind 这一类的专业工具:

valgrind --tool=memcheck --leak-check=full ./program

6.3 缓冲区溢出调试

调试缓冲区溢出的常规步骤如下:

  1. 尝试定位内存越界的位置:

    (gdb) watch buffer[10]
  2. 当程序被观察点停住后,分析调用栈:

    (gdb) bt
  3. 检查内存状态,观察栈是否被破坏:

    (gdb) x/20xw $esp

通过这一系列技巧,就能精准定位到导致缓冲区溢出的那行代码。

掌握这些 GDB 进阶技巧,无论是面对复杂的多线程竞争、诡异的内存越界,还是进行便捷的远程交叉调试,都能让你事半功倍。如果你在调试路上有什么独门心得,也欢迎常来云栈社区一同交流分享。




上一篇:告别混乱切换:Peak Code如何为Claude Code/Codex等7大代码代理打造统一可视化操作台
下一篇:Codex接入DeepSeek保姆级教程:无需手机验证,费用更低
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-6-19 08:13 , Processed in 0.819123 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表