4、GDB数据查看:print命令、display命令、格式化输出、查看内存、查看寄存器
调试的核心是什么?说白了,就是看数据。
你设了断点,程序停下来了。然后呢?你得知道变量值对不对,内存里写了什么,寄存器状态是否正常。GDB 提供了一整套数据查看工具,用好它们,调试效率能翻倍。我个人习惯是:先看变量,再看内存,最后确认寄存器——这个顺序能帮你快速定位大多数问题。
4.1 print 命令:最基础的变量查看
print(简写 p)是 GDB 里最常用的命令。用法很简单:
(gdb) print variable_name
(gdb) p variable_name
(gdb) p/x variable_name # 十六进制显示
(gdb) p/d variable_name # 十进制显示
(gdb) p/t variable_name # 二进制显示
我在项目中遇到过一个问题:一个指针变量打印出来是 0x7fff12345678,看着挺正常。但当我用 p *ptr 解引用时,程序直接崩溃了。后来才发现,这个地址已经被释放了——典型的野指针问题。所以记住:打印指针时,先看地址是否在合理范围内。
p a + b、p *ptr + 10、甚至 p func() 都可以。但注意,调用函数可能会改变程序状态,谨慎使用。
4.2 display 命令:自动跟踪变量
单步调试时,每次都要手动 p 一下变量,太麻烦了。这时候 display 就派上用场了。
(gdb) display variable_name
(gdb) display /x variable_name # 带格式
(gdb) info display # 查看所有 display 项
(gdb) undisplay 1 # 删除编号为1的 display
(gdb) delete display 2 # 删除编号为2的 display
每次执行 step 或 next 后,GDB 会自动打印所有 display 的变量值。嗯,这里要注意:display 太多会影响调试速度,我一般控制在 3-5 个以内。
watch 命令(后面会讲)才解决。所以,display 适合变量少、步数少的场景。
4.3 格式化输出:让数据更直观
GDB 支持多种输出格式,用 / 加格式字符指定:
| 格式 | 说明 | 示例 |
|---|---|---|
/x |
十六进制 | p/x 255 → 0xff |
/d |
有符号十进制 | p/d 0xff → 255 |
/u |
无符号十进制 | p/u -1 → 4294967295 |
/t |
二进制 | p/t 5 → 101 |
/c |
字符 | p/c 65 → 'A' |
/f |
浮点数 | p/f 3.14 → 3.140000 |
你想想看,调试网络协议时,看十六进制比看十进制直观多了。我习惯在 .gdbinit 里设置默认格式:
set print pretty on
set print array on
set print elements 200
这样打印结构体和数组时,输出会漂亮很多。
4.4 查看内存:x 命令
有时候变量不够看,得直接看内存。用 x(examine)命令:
(gdb) x/nfu address
参数说明:
- n:重复次数(显示多少个单元)
- f:格式(x 十六进制、d 十进制、s 字符串、i 指令)
- u:单元大小(b 字节、h 半字、w 字、g 双字)
举个例子:
(gdb) x/10xb 0x7fff12345678 # 查看10个字节,十六进制显示
(gdb) x/5dw buffer # 查看5个有符号整数
(gdb) x/s 0x7fff12345678 # 查看字符串
(gdb) x/10i $pc # 查看当前指令附近的10条汇编指令
我曾经调试一个内存越界问题,就是用 x/100xb 查看数组前后的内存,发现有个结构体被覆盖了。嗯,这种问题用 print 根本看不出来,必须直接看内存。
0x0 或 0xdeadbeef,那肯定是野指针。另外,x/s 打印字符串时,遇到 \0 会停止,所以如果字符串被截断,可能是有非打印字符。
4.5 查看寄存器:info registers
寄存器是 CPU 的「工作台」,所有计算都在这里完成。用 info registers 查看:
(gdb) info registers # 查看所有寄存器
(gdb) info registers rax rbx # 查看指定寄存器
(gdb) p $rax # 打印单个寄存器
(gdb) p/x $rsp # 查看栈指针
常见的寄存器:
- $rax:返回值寄存器
- $rbp:基址指针(栈帧底部)
- $rsp:栈指针(栈帧顶部)
- $rip:指令指针(当前执行位置)
- $eflags:标志寄存器(进位、零、符号等标志)
我记得有一次调试一个函数调用崩溃,用 p $rsp 发现栈指针指向了一个奇怪的地址。再结合 x/10gx $rsp 查看栈内容,发现栈被写穿了——典型的栈溢出。
p $rdi 查看第一个参数,不用去翻源码。
4.6 综合示例:调试一个数组越界
假设我们有这样一段代码:
#include <stdio.h>
int main() {
int arr[5] = {1, 2, 3, 4, 5};
int i;
for (i = 0; i <= 5; i++) { // 越界!
arr[i] = arr[i] * 2;
}
printf("Done\n");
return 0;
}
调试步骤:
- 在
arr[i] = arr[i] * 2;处设断点 - 用
display i和display arr[i]跟踪 - 当
i == 5时,p arr[5]显示垃圾值 - 用
x/10dw arr查看数组周围内存,发现arr[5]实际上是i变量的地址 - 用
p &i确认地址一致——越界写把循环变量给改了!
你看,只用 print 是不够的,必须结合内存查看才能定位这种「写自己」的诡异问题。
4.7 本章知识体系
下面这张图总结了 GDB 数据查看的核心命令和它们的关系:
这张图把四个核心命令放在一起对比。你调试时,按这个顺序走一遍,大部分数据问题都能现形。