4. 变量与内存查看:print格式化输出、x命令查看内存、display自动显示变量、set var修改变量值、call调用函数

调试的核心是什么?说白了,就是搞清楚程序在运行时的「状态」。而状态最直接的体现,就是变量值和内存内容。GDB 在这块提供了非常强大的工具,今天我们就来逐个攻破。

4.1 print 命令:不只是打印那么简单

print 命令,简称 p,是调试时用得最频繁的命令。但很多人只用它来打印变量值,其实它的格式化输出功能非常实用。

基本用法很简单:

(gdb) p my_var
$1 = 42

这里 $1 是 GDB 自动分配的历史变量编号。你可以直接用 $1 来引用这个值,这在后续调试中很方便。

格式化输出才是重点。GDB 支持多种格式:

格式 说明 示例
/x 十六进制 p/x 100 → 0x64
/d 有符号十进制 p/d 0x64 → 100
/u 无符号十进制 p/u -1 → 4294967295
/o 八进制 p/o 64 → 0100
/t 二进制 p/t 5 → 101
/c 字符 p/c 65 → 'A'
/f 浮点数 p/f 3 → 3.0
我的习惯:调试嵌入式底层代码时,我几乎只用 /x 和 /t。寄存器值、内存地址,用十六进制看最直观。二进制则用来检查位域或标志位。

举个例子,假设你有一个结构体指针:

(gdb) p *p_dev
$2 = {base_addr = 0x40020000, irq_num = 67, flags = 0x1a}
(gdb) p/x *p_dev
$3 = {base_addr = 0x40020000, irq_num = 0x43, flags = 0x1a}

看到区别了吗?/x 让所有字段都以十六进制显示,一眼就能看出 irq_num 是 0x43,对应十进制 67。这在对照芯片手册时特别有用。

4.2 x 命令:直接查看内存

x 命令(examine 的缩写)用来查看指定地址的内存内容。它的语法是:

x/[数量][格式][单位大小] 地址

单位大小很关键:

单位 说明
b 1 字节 (byte)
h 2 字节 (halfword)
w 4 字节 (word)
g 8 字节 (giant)

来看几个实际例子:

(gdb) x/10xb 0x40020000
0x40020000: 0x00  0x00  0x00  0x40  0x02  0x00  0x00  0x00
0x40020008: 0x43  0x00

(gdb) x/5xw 0x40020000
0x40020000: 0x40000000  0x00000002  0x00000043  0x00000000
0x40020010: 0x00000000
我曾经踩过的坑:有一次调试 DMA 传输,数据总是对不上。我用 x/10xb 看内存,发现字节顺序和预期相反。后来才意识到,ARM 内核是小端模式,低地址存的是低字节。从那以后,我每次看内存都会先确认字节序。

x 命令还支持用变量名作为地址:

(gdb) x/10xb &buffer
0x7fffffffe000: 0x41  0x42  0x43  0x44  0x00  0x00  0x00  0x00
0x7fffffffe008: 0x00  0x00

嗯,这里要注意,&buffer 取的是数组首地址,不是指针变量的地址。初学者容易搞混。

4.3 display:让变量自动更新

单步调试时,每次都要手动 p 一下变量,很烦对吧?display 命令就是干这个的。它会在每次程序暂停时,自动打印你指定的表达式。

(gdb) display/x counter
1: /x counter = 0x0a
(gdb) display/i $pc
2: x/i $pc = 0x4004f2 <main+10>: movl   $0x0,-0x4(%rbp)

每次执行 next 或 step 后,GDB 会自动显示这些信息:

(gdb) next
1: /x counter = 0x0b
2: x/i $pc = 0x4004f6 <main+14>: cmpl   $0x63,-0x4(%rbp)

常用命令:

  • display 表达式 — 添加自动显示项
  • info display — 列出所有自动显示项
  • undisplay 编号 — 删除指定编号的显示项
  • delete display 编号 — 同上
  • disable display 编号 — 临时禁用,不删除
  • enable display 编号 — 重新启用
我建议:调试循环时,把循环变量和关键指针都 display 出来。比如调试链表遍历,我会同时 display 当前节点指针和节点数据,一眼就能看出遍历是否正确。

4.4 set var:调试时修改变量

有时候你想「如果这个变量是另一个值,程序会怎样?」。不用重新编译,set var 直接改:

(gdb) set var counter = 100
(gdb) p counter
$4 = 100

也可以简写:

(gdb) set counter = 200

对于指针和结构体,同样支持:

(gdb) set var p_node->data = 0xff
(gdb) set var *p_buffer = 0xdeadbeef
注意:set var 改变的是程序运行时的内存值。如果你修改了不该改的地方(比如只读数据段),程序可能会崩溃。我曾经在调试一个 RTOS 任务时,不小心改了任务控制块的优先级,结果调度器直接乱套了。嗯,从那以后我改变量前都会先确认一下地址范围。

4.5 call:在调试中调用函数

call 命令让你在调试会话中直接调用程序里的函数。这在测试某个函数的行为时特别有用。

(gdb) call my_func(10, 20)
$5 = 30

也可以调用库函数:

(gdb) call printf("hello from gdb\n")
hello from gdb
$6 = 18

甚至可以用来修改程序状态:

(gdb) call memset(buffer, 0, 256)
(gdb) call free(ptr)
我的经验:调试内存泄漏时,我经常用 call 调用自定义的内存统计函数。比如 call print_mem_usage(),直接在调试器里看当前堆内存分配情况,比加日志方便多了。

不过要注意,call 会真正执行函数,如果函数有副作用(比如修改全局变量、操作硬件),那后果是真实的。我曾经在调试一个电机驱动时,不小心 call 了一个启动电机的函数,结果调试器里的电机真的转起来了...还好只是空载测试。

知识体系总览

下面这张图总结了本章的核心内容,方便你快速回顾:

变量与内存查看核心命令 GDB 调试命令 print (p) 格式化输出 /x 十六进制 /t 二进制 /d 十进制 /c 字符 x (examine) 内存查看 b 1字节 h 2字节 w 4字节 g 8字节 display 自动显示变量 info display 列出 undisplay 删除 disable/enable 开关 set var / call 修改变量 / 调用函数 set var 改值 call 调用函数 注意副作用 核心思路:看清变量 → 看透内存 → 动态修改 调试的本质:在程序运行时,获得对状态的完全控制

这五个命令组合起来,基本覆盖了调试过程中 90% 的变量和内存操作需求。我个人习惯是:先 display 关键变量,然后单步执行,偶尔用 x 确认内存布局,遇到需要测试的场景就用 set var 或 call。这套组合拳打下来,大部分 bug 都无所遁形。

一句话总结:print 让你看清变量,x 让你看透内存,display 帮你自动跟踪,set var 和 call 让你在运行时「动手脚」。掌握这五个命令,调试效率至少提升一倍。

可可精品资料库