4、GDB数据查看:print命令详解、display命令、watch命令、查看内存

调试的核心是什么?说白了,就是搞清楚程序在运行过程中,数据到底变成了什么鬼样子。你设了断点,停下来了,然后呢?你得看变量、看内存、看表达式。这一章,我就带你把这几个最常用的数据查看工具玩透。

4.1 print命令:你的第一把探照灯

print 命令,简写 p,是GDB里最基础的查看命令。它的作用就是打印表达式的值。

基本用法

(gdb) p variable_name
(gdb) p &variable_name
(gdb) p *pointer
(gdb) p array[index]
(gdb) p my_struct.field

我个人习惯,每次停下来第一件事就是 p local_var,先确认当前函数的局部变量是不是符合预期。有一次我调试一个网络协议栈,发现一个计数器莫名其妙变成了负数,用 p 一看,果然是个无符号整型被错误赋值了。

格式化输出

GDB支持多种输出格式,用 /格式 来指定:

格式说明示例
/x十六进制p/x 255 → 0xff
/d有符号十进制p/d 0xff → 255
/u无符号十进制p/u -1 → 4294967295
/o八进制p/o 64 → 0100
/t二进制p/t 5 → 101
/c字符p/c 65 → 'A'
/f浮点数p/f 3.14
/s字符串p/s buffer
小技巧: 查看指针指向的字符串时,用 p/s ptr 比 p ptr 直观得多。我经常用这个来检查字符串缓冲区有没有被截断。

打印复杂表达式

print 不只是打印变量,它可以计算任何合法的C/C++表达式:

(gdb) p a + b * c
(gdb) p sizeof(struct my_struct)
(gdb) p my_array[10] + my_array[20]
(gdb) p (int)my_float  // 类型转换

嗯,这里要注意:GDB里的表达式求值是在当前上下文中进行的,如果你在函数A里,想访问函数B的局部变量,那是不行的。除非你用 frame 命令切换栈帧。

4.2 display命令:自动刷新的监视器

print 每次都要手动敲,调试循环或者频繁单步时很烦人。这时候 display 就派上用场了。它会在每次程序停下来时,自动打印你指定的表达式。

基本用法

(gdb) display variable
(gdb) display /x variable
(gdb) display *pointer
(gdb) display array[0]@10  // 打印数组前10个元素

举个例子,你在调试一个循环:

(gdb) display i
(gdb) display /x buffer[i]
(gdb) n
1: i = 5
2: /x buffer[i] = 0x4a
(gdb) n
1: i = 6
2: /x buffer[i] = 0x4b

每次单步执行后,GDB自动显示这两个表达式的值。省了多少事!

管理display列表

命令作用
info display列出所有display项
undisplay 编号删除指定编号的display
disable display 编号临时禁用,不删除
enable display 编号重新启用
delete display删除所有
注意: display 项太多会影响调试速度,尤其是表达式涉及复杂计算或函数调用时。我曾经在一个嵌入式项目里设了十几个display,结果单步一次要等好几秒……后来学乖了,只保留最关键的2-3个。

4.3 watch命令:数据变化的哨兵

有时候你不想一步步跟,只想让程序跑着,等某个变量发生变化时自动停下来。这就是 watch 命令的用武之地。

三种watchpoint

  1. 普通watchpoint: 变量被写入时触发
  2. 读watchpoint(rwatch): 变量被读取时触发
  3. 读写watchpoint(awatch): 变量被读取或写入时触发
(gdb) watch variable        // 写入时停
(gdb) rwatch variable       // 读取时停
(gdb) awatch variable       // 读写都停

实战场景

我曾经调试过一个多线程程序,一个全局变量莫名其妙被改成了错误值。不知道是哪个线程干的。怎么办?

(gdb) watch global_var
Hardware watchpoint 1: global_var
(gdb) continue
Continuing.
Thread 3 hit Hardware watchpoint 1: global_var
Old value = 42
New value = 0
main () at buggy.c:123
123     global_var = 0;  // 就是这行!

GDB不仅停下来了,还告诉你哪个线程、哪行代码、旧值和新值。这就是watchpoint的威力。

管理watchpoint

命令作用
info watchpoints列出所有watchpoint
delete 编号删除指定watchpoint
disable 编号临时禁用
enable 编号重新启用
硬件watchpoint vs 软件watchpoint: 硬件watchpoint由CPU支持,速度快,但数量有限(通常4个左右)。软件watchpoint通过单步执行+值比较实现,速度慢但数量不限。GDB会自动选择,你也可以用 set can-use-hw-watchpoints 0 强制使用软件方式。

4.4 查看内存:x命令

print 看变量很方便,但有时候你需要直接看内存里的原始数据。比如检查缓冲区内容、查看堆栈布局、或者分析数据结构的内存分布。这时候用 x 命令(examine memory)。

基本语法

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

格式和单位大小:

格式说明单位大小说明
x十六进制b1字节
d十进制h2字节(halfword)
u无符号十进制w4字节(word)
o八进制g8字节(giant)
t二进制
c字符
f浮点数
s字符串
i指令(反汇编)

常用示例

(gdb) x/10xb buffer      // 查看buffer前10个字节,十六进制
(gdb) x/20dw &array      // 查看array前20个int,十进制
(gdb) x/5gf &double_arr  // 查看5个double,浮点数格式
(gdb) x/10i $pc          // 查看当前指令附近10条汇编指令
(gdb) x/s 0x7fffffff     // 查看某个地址的字符串

我个人最常用的是 x/32xb 配合 p 来检查内存对齐问题。有一次发现结构体成员之间多了几个字节的填充,用 x 一看就明白了——编译器为了对齐插入了padding。

小技巧: 用 x/gx 查看64位指针的值,比用 p 更直观。另外,x/10i $pc 在分析汇编代码时特别好用,尤其是没有源码的情况下。

4.5 知识体系总览

下面这张图帮你理清这四种数据查看工具的关系和适用场景:

GDB数据查看工具体系 数据查看 print / p display watch / rwatch / awatch x (examine) 一次性查看 手动输入表达式 支持格式化输出 可计算复杂表达式 适合临时查看 自动刷新 每次停顿时自动打印 可管理列表 适合循环/单步 注意性能影响 数据变化监控 写入/读取/读写触发 硬件/软件两种模式 显示旧值和新值 适合找谁改了变量 内存原始数据 直接查看内存 多种格式/单位 可反汇编指令 适合底层分析 使用建议 日常调试:print + display 组合使用 数据异常:watch 定位修改点 底层分析:x 命令查看内存布局

4.6 实战经验总结

最后,分享几个我多年调试积累下来的小经验:

  • print 和 display 搭配使用: 先 p 确认关键变量,然后把需要持续关注的用 display 挂上。别一上来就display一堆,容易乱。
  • watchpoint 的坑: 在多线程环境下,watchpoint 可能会因为线程切换而失效。我曾经遇到过硬件watchpoint被其他线程覆盖的情况,后来改用软件watchpoint才稳定。
  • x 命令的妙用: 检查结构体对齐时,用 x/32xb &struct_var 看内存布局,再对比 sizeof 和成员偏移量,对齐问题一目了然。
  • 格式化输出要养成习惯: 看指针用 /x,看字符串用 /s,看数组用 /d 或 /u。别偷懒,格式对了,一眼就能发现问题。
一句话总结: print 是手动挡,display 是自动挡,watch 是报警器,x 是显微镜。四个工具配合使用,没有查不到的数据。

可可精品资料库