第1章:GDB基本命令——启动与退出、设置断点、单步执行、查看变量、backtrace调用栈
各位同学好,我是你们的嵌入式调试讲师。今天咱们聊聊GDB最基础、也是最核心的几个命令。说实话,我见过太多工程师拿着GDB只会设个断点、打个print,遇到复杂bug就抓瞎。其实啊,把基础命令用熟了,80%的问题都能搞定。
我个人习惯把GDB调试比作「手术刀」——你得知道怎么下刀、怎么观察病灶、怎么追溯病因。这一章,咱们就把这把刀磨锋利。
1.1 启动与退出:别小看这一步
启动GDB的方式有好几种,我工作中最常用的是这两种:
# 方式一:直接调试可执行文件
gdb ./my_program
# 方式二:先启动GDB,再加载文件
gdb
(gdb) file ./my_program
嗯,这里有个坑。我曾经在调试一个交叉编译的ARM程序时,直接敲了gdb ./arm_binary,结果GDB报错说格式不对。你想想看,本地GDB怎么能调试ARM指令呢?后来我换成了arm-linux-gnueabihf-gdb才搞定。
退出GDB很简单:
(gdb) quit
# 或者简写
(gdb) q
如果程序还在运行,GDB会问你是否要终止。我一般直接y确认。
1.2 设置断点:调试的起点
断点就是让程序停在你关心的位置。说白了,没有断点,调试就无从谈起。
常用的断点设置方式:
| 命令 | 说明 | 示例 |
|---|---|---|
break 行号 |
在指定行设置断点 | break 42 |
break 函数名 |
在函数入口设置断点 | break main |
break 文件:行号 |
在指定文件的某行设断点 | break src/main.c:88 |
break 文件:函数名 |
在指定文件的函数设断点 | break src/utils.c:parse_data |
我在项目中遇到过一种情况:一个bug只在循环第100次时才出现。你总不能手动按100次continue吧?这时候条件断点就派上用场了:
(gdb) break 42 if i == 100
这样程序只会在i等于100时停下来,省时省力。
info break查看所有断点,用delete 断点编号删除指定断点。我习惯给断点起名字,比如break main if argc > 1,方便记忆。
1.3 单步执行:一步步看程序怎么跑
设好断点后,程序停下来了。接下来怎么走?GDB提供了几个「步进」命令:
next(或n):单步执行,遇到函数调用不进入,直接跳过。step(或s):单步执行,遇到函数调用会进入函数内部。finish:一直执行到当前函数返回。continue(或c):继续执行,直到下一个断点。
举个例子,假设你有这么一段代码:
int main() {
int a = 10;
int b = add_one(a); // 第3行
printf("%d\n", b); // 第4行
return 0;
}
在第3行停下时:
- 敲
next,直接执行完add_one,停在printf那行。 - 敲
step,会跳进add_one函数内部,一行行看它是怎么算的。
我个人习惯是:确认函数没问题就用next跳过;怀疑函数有bug就用step进去看。你想想看,如果每次都step进标准库函数,那得累死。
1.4 查看变量:调试的核心目的
断点停了,步进走了,最终目的是什么?看变量值对不对啊!
最常用的命令:
(gdb) print 变量名
# 例如
(gdb) print i
$1 = 42
(gdb) print &i
$2 = (int *) 0x7fffffffe4c
(gdb) print array[0]
$3 = 100
还可以用display命令让变量自动显示:
(gdb) display i
1: i = 42
(gdb) display array[0]
2: array[0] = 100
这样每次单步执行时,GDB会自动打印这些变量的值。我曾经调试一个内存泄漏问题,就是靠display盯着指针地址的变化,才发现有个地方忘了释放。
print *ptr可以打印指针指向的内容。用print struct_var.field可以访问结构体成员。
1.5 backtrace调用栈:追根溯源的法宝
程序崩溃了,停在某个函数里。你怎么知道它是怎么走到这一步的?这时候backtrace(简写bt)就派上用场了。
(gdb) bt
#0 0x00005555555546a7 in divide (a=10, b=0) at calc.c:12
#1 0x00005555555546d3 in main () at main.c:8
这个输出告诉我们:程序当前在divide函数里(第0帧),是main函数调用了它(第1帧)。每一帧都显示了函数名、参数、文件和行号。
如果想看更详细的信息,可以用:
(gdb) bt full
#0 0x00005555555546a7 in divide (a=10, b=0) at calc.c:12
result = 0
#1 0x00005555555546d3 in main () at main.c:8
x = 10
y = 0
这样连局部变量值都一起打印出来了。
我曾经遇到一个递归调用栈溢出的bug。程序跑着跑着就崩了,用bt一看,好家伙,调用栈有3000多层!原来递归函数忘了写终止条件。没有backtrace,这种问题排查起来就像大海捞针。
-g选项,bt只能看到地址,看不到函数名和行号。我曾经因为这个浪费了半天时间——编译时忘了加-g,调试信息全丢了。
本章小结
这一章咱们把GDB最基础的几个命令过了一遍:
- 启动退出:
gdb ./program和quit - 设置断点:
break、条件断点、info break - 单步执行:
next、step、finish、continue - 查看变量:
print、display - 调用栈:
backtrace、bt full
这些命令就像工具箱里的螺丝刀、扳手——单个用可能不起眼,组合起来威力巨大。下一章咱们会深入讲讲「条件断点」和「观察点」的高级用法,到时候你会发现,原来调试可以这么爽。
记住:调试不是碰运气,而是有方法论的。把基础打牢,遇到再诡异的bug也不慌。