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处的断点 |
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的核心流程串起来了。你从启动开始,设置断点,运行到断点,然后单步执行、查看变量,发现问题后修改变量或直接改代码,再继续调试。这是一个闭环。
2.6 实战小案例:快速定位段错误
段错误(Segmentation Fault)是嵌入式开发中最常见的崩溃。用GDB定位它,其实就三步:
- 编译加 -g:
gcc -g -o test test.c - 运行GDB:
gdb ./test,然后输入run - 看崩溃位置:程序崩了后,输入
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的基本功就这些。你只要记住:断点让程序停,单步让程序慢走,print和x让你看到程序的内脏。把这几个命令练熟,大部分调试场景你都能应付。剩下的,就是在实战中慢慢积累经验了。