2. GDB入门:GDB基本命令、断点管理、单步执行、查看变量与内存

调试器这东西,说白了就是给程序装了个「慢放镜头」。你写代码时觉得逻辑没问题,跑起来却崩了,这时候GDB就是你的救命稻草。我刚开始做嵌入式那会儿,全靠printf大法,后来被一个野指针折磨了三天,才老老实实学GDB。嗯,今天咱们就把GDB的底裤扒干净。

2.1 启动GDB的三种姿势

先别急着敲命令,你得知道怎么把GDB叫出来。我个人习惯用这三种方式:

启动方式 命令 适用场景
直接调试可执行文件 gdb ./my_program 最常见,程序还没跑
附加到正在运行的进程 gdb -p <PID> 程序已经跑起来了,突然卡死
调试core dump文件 gdb ./my_program core 程序崩了,留下案发现场

这里有个坑——编译时一定要加 -g 选项。不加的话,GDB看到的全是汇编地址,你连变量名都找不到。我曾经在客户现场调试一个release版本,那叫一个酸爽,全是裸地址。

注意:嵌入式交叉编译时,记得用对应的 arm-linux-gnueabihf-gdb 或类似工具链版本,别拿PC的GDB去调ARM的程序,会报「格式不对」。

2.2 断点管理——让程序在你想停的地方停

断点是调试的灵魂。你想想看,程序跑得飞快,你肉眼根本跟不上。让它在关键位置停下来,你才能慢慢看。

2.2.1 设置断点

(gdb) break main              # 在main函数入口停
(gdb) break 42                 # 在当前文件的第42行停
(gdb) break file.c:100         # 在file.c的第100行停
(gdb) break func_name          # 在某个函数入口停
(gdb) break *0x400520          # 在某个内存地址停(汇编级调试用)

我个人最常用的是 break file.c:行号,配合 list 命令先看看代码位置,再下断点,效率很高。

2.2.2 条件断点

有时候你只想在某个条件成立时停下来。比如循环1000次,你只关心第500次的情况:

(gdb) break 42 if i == 500
(gdb) break func if x > 10 && y < 5

我在项目中遇到过一个问题:一个中断服务函数被频繁触发,每次进来都停,根本没法调试。后来加了条件断点,只让它在特定标志位为1时停下,问题一下就定位到了。

2.2.3 管理断点

命令 作用
info breakpoints 列出所有断点及其编号
disable 2 临时禁用2号断点(不删除)
enable 2 重新启用2号断点
delete 2 删除2号断点
clear func 删除func处的断点
小技巧:断点编号不会复用。你删了2号,再新建一个,编号是5。用 info b 可以快速查看当前所有断点状态。

2.3 单步执行——像放慢镜头一样看代码

断点停下来了,然后呢?你得一步一步走,看程序是怎么跑的。GDB提供了几个「步进」命令,各有各的用处。

命令 缩写 作用
step s 步入函数内部
next n 单步跳过(不进入函数)
finish fin 一直执行到当前函数返回
continue c 继续执行到下一个断点
until 行号 — 执行到指定行(跳过循环体)

举个例子,假设你在 main() 里调用了 foo():

  • 用 next,直接跳过 foo() 执行下一行。
  • 用 step,会钻进 foo() 内部,一行一行看。
  • 如果已经钻进 foo() 了,想快速出来,用 finish。

我曾经在调试一个状态机的时候,用 step 一步步跟踪状态跳转,发现某个case分支里少了一个break,导致状态跑飞了。这种bug,光看代码很难发现,但单步执行一眼就能看出来。

核心原则:不确定函数内部有没有问题时用 step,确定没问题时用 next 跳过,提高调试效率。

2.4 查看变量与内存——把程序的「内脏」翻出来看

停下来了,也一步步走了,但变量值对不对?内存里到底写了什么?这才是调试的关键。

2.4.1 查看变量

(gdb) print x              # 打印变量x的值
(gdb) print &x             # 打印x的地址
(gdb) print *ptr           # 打印指针指向的内容
(gdb) print array[5]       # 打印数组第6个元素
(gdb) print struct_var     # 打印整个结构体

你还可以用 display 命令让某个变量自动显示,每次单步执行后都打印出来:

(gdb) display x
(gdb) display /x y         # 以十六进制显示y
(gdb) info display         # 查看所有自动显示的变量
(gdb) undisplay 1          # 取消1号自动显示

2.4.2 查看内存

变量只是表象,内存才是真相。用 x 命令(examine)直接看内存:

(gdb) x/10x ptr           # 以十六进制显示ptr开始的10个单元
(gdb) x/10d ptr           # 以十进制显示
(gdb) x/10c ptr           # 以字符形式显示
(gdb) x/10s ptr           # 以字符串形式显示
(gdb) x/10i ptr           # 以汇编指令形式显示

格式说明:x/数量格式地址。比如 x/20xb buf 表示查看buf开始的20个字节,以十六进制显示。

避坑指南:我曾经在调试一个缓冲区溢出问题时,用 x/100x buf 查看内存,发现buf后面紧跟着的变量被改写了。如果你怀疑内存越界,多往后看几十个字节,往往能发现线索。

2.4.3 修改变量

调试时临时改个值,不用重新编译:

(gdb) set var x = 42
(gdb) set {int}0x400520 = 0xff    # 直接修改内存地址的值

这个功能在测试边界条件时特别有用。比如某个函数只在 count == 0 时才会触发特殊逻辑,你可以在断点处直接 set var count = 0,然后继续执行,看看效果。

2.5 GDB调试全景图

下面这张图把GDB的核心流程串起来了。你从启动开始,设置断点,运行到断点,然后单步执行、查看变量,发现问题后修改变量或直接改代码,再继续调试。这是一个闭环。

GDB调试核心流程 启动GDB 设置断点 运行程序 (run) 单步执行 (step/next) 查看变量/内存 (print/x) 修改变量 (set var) 常用命令速查 run / r — 运行 break / b — 断点 step / s — 步入 next / n — 跳过

2.6 实战小案例:快速定位段错误

段错误(Segmentation Fault)是嵌入式开发中最常见的崩溃。用GDB定位它,其实就三步:

  1. 编译加 -g:gcc -g -o test test.c
  2. 运行GDB:gdb ./test,然后输入 run
  3. 看崩溃位置:程序崩了后,输入 bt(backtrace)查看调用栈,或者 frame N 切换到指定栈帧,再用 print 查看局部变量
(gdb) run
Starting program: /home/test
Program received signal SIGSEGV, Segmentation fault.
0x0804841b in foo (p=0x0) at test.c:10
10        *p = 42;
(gdb) bt
#0  0x0804841b in foo (p=0x0) at test.c:10
#1  0x0804843f in main () at test.c:15
(gdb) frame 1
#1  0x0804843f in main () at test.c:15
15        foo(NULL);
(gdb) print p
$1 = (int *) 0x0

看到了吗?p 是空指针,foo() 里直接解引用,不崩才怪。这种问题,用printf你得加多少行才能定位到?GDB一行命令就搞定了。

我的习惯:每次写完一个新模块,我都会用GDB单步跑一遍核心路径,确保每个分支都走对了。虽然花点时间,但比后期debug省心十倍。

好了,GDB的基本功就这些。你只要记住:断点让程序停,单步让程序慢走,print和x让你看到程序的内脏。把这几个命令练熟,大部分调试场景你都能应付。剩下的,就是在实战中慢慢积累经验了。