2. GDB核心概念

调试器这东西,说白了就是让你能「看穿」程序运行时的内部状态。GDB 的核心能力,就围绕这几个概念展开。我个人习惯把它们分成两类:一类是「停下来」的手段(断点、观察点、捕获点),另一类是「动起来」的手段(单步、继续、查看变量)。

嗯,咱们一个一个来拆解。

2.1 断点(Breakpoint)—— 最常用的暂停键

断点就是告诉 GDB:「执行到某一行,给我停住」。这是最基础、最常用的功能。我在项目中遇到过无数次这样的场景:程序跑飞了,不知道在哪崩的,先在所有可疑函数入口打上断点,然后一步步缩小范围。

核心用法:

  • break main —— 在 main 函数入口打断点
  • break 42 —— 在当前文件的第 42 行打断点
  • break file.c:100 —— 在 file.c 的第 100 行打断点
  • break func if x == 5 —— 条件断点,x 等于 5 时才停

你想想看,条件断点有多实用?比如你有个循环跑 10000 次,但只在第 5000 次出问题。你总不能手动按 5000 次 continue 吧?break loop_line if i == 5000,一行搞定。

我的小技巧: 调试多线程程序时,可以用 break func thread 2 指定只在某个线程中生效。这能省掉大量干扰信息。

2.2 观察点(Watchpoint)—— 盯着变量不放

断点是盯着代码行,观察点是盯着变量。只要变量的值发生变化,GDB 就会自动停下来。我曾经花了一整个下午追一个野指针问题,最后就是用观察点抓到的。

为什么会这样?因为断点只能告诉你「执行到哪一行了」,但有时候你根本不知道是哪一行改坏了变量。观察点就不一样了——它像雷达一样,变量一变就报警。

核心用法:

  • watch var —— 监视 var 变量,值变化时停住
  • rwatch var —— 监视 var 被读取时停住
  • awatch var —— 监视 var 被读或写时都停住

注意: 观察点是有代价的。硬件观察点数量有限(通常 4 个),软件观察点会让程序跑得极慢。别一口气设太多,否则调试体验会非常痛苦。

2.3 捕获点(Catchpoint)—— 抓异常和事件

捕获点比断点更「高级」一点。它不关心代码行,而是关心事件。比如 C++ 抛异常了、程序 fork 子进程了、动态库加载了——这些都能用捕获点抓住。

我记得有一次调试一个诡异的 SIGSEGV,程序总是在某个随机时刻崩溃。用断点根本没法复现,后来我用 catch signal SIGSEGV,一抓一个准。

核心用法:

  • catch throw —— 捕获 C++ 异常抛出
  • catch signal SIGSEGV —— 捕获段错误信号
  • catch fork —— 捕获 fork 系统调用
  • catch syscall write —— 捕获 write 系统调用

2.4 单步执行(Step & Next)—— 一步一步来

停下来了,然后呢?你得「走」起来。GDB 提供了两种走法:step 和 next。区别很简单:step 会钻进函数内部,next 会跳过函数调用。

我个人习惯是:调试自己写的函数用 step,调试别人写的库函数用 next。你想想看,要是 printf 也 step 进去,那得看到多少汇编代码?

常用命令:

  • step 或 s —— 单步进入
  • next 或 n —— 单步跳过
  • finish —— 一直执行到当前函数返回
  • until 100 —— 一直执行到第 100 行

避坑指南: 我曾经在调试时一直用 next,结果发现某个变量的值被改了,但怎么也找不到是谁改的。后来才意识到,那个修改发生在另一个线程里,next 根本不会切线程。这时候就得用 thread apply all next 或者观察点。

2.5 继续执行(Continue)—— 从暂停到继续

单步走太慢了,有时候你想直接跑到下一个断点。这时候就用 continue(简写 c)。

嗯,这里要注意:continue 不是「从头开始跑」,而是「从当前位置继续跑,直到遇到下一个断点或程序结束」。

变体用法:

  • continue —— 继续执行
  • continue 5 —— 忽略当前断点 5 次,第 6 次才停
  • signal SIGINT —— 向程序发送一个信号(比如模拟 Ctrl+C)

2.6 查看变量(Print & Display)—— 看穿内存

停下来了,也走起来了,但你看不到变量的值,那调试个啥?print 命令就是干这个的。

说白了,print 就是 GDB 的「眼睛」。它能打印变量、表达式、甚至调用函数。

核心用法:

  • print x —— 打印变量 x 的值
  • print &x —— 打印变量 x 的地址
  • print *ptr —— 打印指针指向的内容
  • print array[0]@10 —— 打印数组的前 10 个元素
  • display x —— 每次停下来都自动打印 x

我的经验: 调试结构体时,print *my_struct 会打印所有成员。但如果成员太多,可以用 print my_struct->field 只打印你关心的那个。另外,display 配合单步执行简直是绝配——每走一步,自动显示关键变量的变化。

2.7 知识体系总览

下面这张图,是我自己总结的 GDB 核心概念关系图。你可以把它当作调试时的「导航地图」。

GDB 核心概念体系 GDB 调试会话 暂停手段 断点 (break) 观察点 (watch) 捕获点 (catch) 执行手段 单步 (step/next) 继续 (continue) 查看手段 打印 (print) 自动显示 (display) 三大手段配合使用,覆盖 90% 的调试场景 常见调试场景与推荐手段 🔍 定位崩溃位置 → 断点 + 单步 🔍 变量被意外修改 → 观察点 🔍 异常/信号处理 → 捕获点 🔍 循环中特定迭代 → 条件断点 🔍 跟踪变量变化 → 自动显示 🔍 跳过已知正确代码 → 继续 + 断点

2.8 实战小贴士

最后,分享几个我自己的调试习惯:

  • 先打断点,再 run —— 别让程序直接跑完,否则你连停的机会都没有
  • 条件断点比手动 continue 高效 10 倍 —— 尤其是循环次数多的时候
  • 观察点不要设太多 —— 硬件观察点有限,软件观察点慢到怀疑人生
  • print 可以调用函数 —— 比如 print strlen(str),但要注意副作用
  • display 和 step 是黄金搭档 —— 每走一步,自动看变量,省得你反复敲 print

曾经踩过的坑: 我在调试一个嵌入式程序时,设了 6 个观察点,结果程序跑一步要等 3 秒。后来才发现,软件观察点是通过单步执行 + 比较值来实现的,设多了就是灾难。从那以后,我最多只设 2 个观察点,其他的用条件断点替代。

嗯,这一章的内容就到这。核心就是记住:断点管位置,观察点管变量,捕获点管事件,单步和继续管流程,print 和 display 管查看。把这几个概念吃透了,GDB 就算入门了。