2、GDB入门:GDB的安装与启动、基本命令

调试器这东西,说白了就是程序员的「后悔药」。你写了个bug,程序跑飞了,core dump了,一脸懵的时候——GDB就是那个能让你倒带、暂停、逐帧查看的利器。我个人习惯在项目一开始就把GDB环境搭好,而不是等到出问题了才手忙脚乱。

2.1 安装GDB

GDB的安装其实没什么技术含量,但不同平台略有差异。我曾在一次客户现场调试中,发现对方的嵌入式Linux系统里居然没装GDB,当时那个尴尬……后来我学乖了,第一件事就是检查工具链。

Ubuntu/Debian系:

sudo apt-get update
sudo apt-get install gdb

CentOS/RHEL系:

sudo yum install gdb

macOS(通过Homebrew):

brew install gdb

嵌入式交叉编译环境:

如果你用的是ARM、RISC-V这类平台,需要安装对应的交叉调试器,比如 arm-none-eabi-gdb 或 riscv64-unknown-elf-gdb。嗯,这里要注意:别装错了版本,否则连不上目标板。

小提示:安装完成后,在终端输入 gdb --version 确认版本。我建议至少用GDB 8.0以上版本,新特性对嵌入式调试更友好。

2.2 启动GDB

启动GDB的方式有好几种,我常用的就两种:

  1. 直接调试可执行文件:
gdb ./my_program
  1. 附加到正在运行的进程:
gdb -p 1234   # 1234是进程PID

为什么会有第二种方式?我记得有一次线上服务突然卡死,又不能重启,我就用 gdb -p 挂上去,直接看线程堆栈,瞬间定位到死锁位置。这招在实战中非常管用。

注意:附加到进程需要足够的权限。如果提示 "Operation not permitted",试试 sudo gdb -p PID。但生产环境慎用sudo,最好提前配好权限。

2.3 基本命令详解

GDB的命令很多,但入门阶段掌握这7个就够用了。我当年刚学GDB时,就靠这几个命令啃下了好几个难缠的bug。

2.3.1 run —— 启动程序

进入GDB后,输入 run 或简写 r 就能启动程序。如果需要传参,直接跟在后面:

(gdb) run arg1 arg2

如果程序之前已经运行过,再次 run 会重新启动。我曾经犯过一个低级错误:调试到一半忘了加参数,结果程序行为完全不对,排查了半天……后来我习惯在 .gdbinit 里预设好参数。

2.3.2 break —— 设置断点

断点是调试的灵魂。你可以对函数、行号、条件设置断点:

(gdb) break main          # 在main函数入口
(gdb) break 42             # 在第42行
(gdb) break file.c:100     # 在file.c的第100行
(gdb) break func if x > 5 # 条件断点,x大于5时触发

我个人最常用的是条件断点。比如在一个循环里,我只想在第100次迭代时停下来,用 break loop if i == 100 就搞定了,省得手动按 continue 按到手抽筋。

2.3.3 next —— 单步跳过

next(简写 n)会执行当前行,如果遇到函数调用,会一口气执行完整个函数,停在下一行。说白了就是「不进入函数内部」。

(gdb) next

什么时候用 next?当你确定被调用的函数没问题时,直接跳过,节省时间。

2.3.4 step —— 单步进入

step(简写 s)和 next 的区别在于:遇到函数调用时,step 会进入函数内部,一行一行执行。

(gdb) step

你想想看,如果你怀疑某个函数内部有bug,那就必须用 step 进去看。我曾经调试一个内存泄漏问题,就是用 step 一步步跟踪malloc和free的调用链,才发现有个分支忘了释放。

2.3.5 continue —— 继续执行

当程序停在断点处,你想让它继续跑,就用 continue(简写 c)。它会一直执行,直到遇到下一个断点或程序结束。

(gdb) continue

嗯,这里有个小技巧:如果你设置了多个断点,可以用 continue 配合 ignore 命令跳过前N次命中。比如 ignore 1 100 表示忽略断点1的前100次触发。

2.3.6 print —— 打印变量值

print(简写 p)是调试时最频繁使用的命令。你可以打印变量、表达式、甚至调用函数:

(gdb) print x
(gdb) print &x            # 打印变量地址
(gdb) print array[5]      # 打印数组元素
(gdb) print *ptr          # 打印指针指向的值
(gdb) print func()        # 调用函数并打印返回值

为什么我强调要掌握 print?因为很多时候你不需要单步跟踪,直接打印关键变量的值就能发现问题。比如指针的值是0x0,那十有八九是空指针解引用了。

核心技巧:用 print /x 以十六进制显示,print /d 以十进制显示,print /s 以字符串显示。调试嵌入式底层时,十六进制用得最多。

2.3.7 backtrace —— 查看调用栈

程序崩溃时,backtrace(简写 bt)是你最好的朋友。它会打印出当前函数的调用链,从最内层到最外层:

(gdb) backtrace
#0  handler (sig=11) at crash.c:15
#1  <signal handler called>
#2  main () at crash.c:30

看到这个输出,你就能知道程序是在哪里崩溃的,以及是谁调用了它。我曾经接手一个遗留项目,程序动不动就段错误,我第一件事就是打开core dump,然后用 bt 看堆栈,半小时就定位到了问题——一个野指针在释放后被再次使用。

2.4 知识体系图

下面这张图帮你理清GDB入门阶段的核心逻辑。说白了就是:启动→设断点→单步/继续→查变量→看堆栈,形成一个闭环。

GDB入门核心流程 1. 安装GDB 2. 启动GDB 3. break 设置断点 next / step continue print 查变量 backtrace 看堆栈 6. 定位并修复bug

2.5 避坑指南

最后分享几个我踩过的坑:

  • 编译时忘记加 -g 选项:没有调试符号,GDB就看不到源码和变量名,只能看到一堆汇编。我曾经在客户现场折腾了半小时才发现这个问题……后来我在Makefile里默认加上 -g -O0。
  • 优化级别太高:用 -O2 编译的代码,单步执行时可能会跳来跳去,因为编译器重排了指令。调试阶段建议用 -O0。
  • 忘记设置断点就 run:程序直接跑完退出,GDB提示 "Program exited normally"。嗯,这不算bug,但确实浪费时间。
  • print 大数组时卡死:如果你打印一个100MB的数组,GDB会卡住。可以用 print *array@10 只打印前10个元素。
我的习惯:每次启动GDB后,先 list 一下看看源码是否加载成功,再 break main 设个保险断点,最后才 run。这套流程走下来,基本不会出幺蛾子。

可可精品资料库