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
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 了一个启动电机的函数,结果调试器里的电机真的转起来了...还好只是空载测试。
知识体系总览
下面这张图总结了本章的核心内容,方便你快速回顾:
这五个命令组合起来,基本覆盖了调试过程中 90% 的变量和内存操作需求。我个人习惯是:先 display 关键变量,然后单步执行,偶尔用 x 确认内存布局,遇到需要测试的场景就用 set var 或 call。这套组合拳打下来,大部分 bug 都无所遁形。
可可精品资料库