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 | 删除所有 |
4.3 watch命令:数据变化的哨兵
有时候你不想一步步跟,只想让程序跑着,等某个变量发生变化时自动停下来。这就是 watch 命令的用武之地。
三种watchpoint
- 普通watchpoint: 变量被写入时触发
- 读watchpoint(rwatch): 变量被读取时触发
- 读写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 编号 | 重新启用 |
set can-use-hw-watchpoints 0 强制使用软件方式。
4.4 查看内存:x命令
print 看变量很方便,但有时候你需要直接看内存里的原始数据。比如检查缓冲区内容、查看堆栈布局、或者分析数据结构的内存分布。这时候用 x 命令(examine memory)。
基本语法
x/[数量][格式][单位大小] 地址
格式和单位大小:
| 格式 | 说明 | 单位大小 | 说明 |
|---|---|---|---|
| x | 十六进制 | b | 1字节 |
| d | 十进制 | h | 2字节(halfword) |
| u | 无符号十进制 | w | 4字节(word) |
| o | 八进制 | g | 8字节(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 知识体系总览
下面这张图帮你理清这四种数据查看工具的关系和适用场景:
4.6 实战经验总结
最后,分享几个我多年调试积累下来的小经验:
- print 和 display 搭配使用: 先
p确认关键变量,然后把需要持续关注的用display挂上。别一上来就display一堆,容易乱。 - watchpoint 的坑: 在多线程环境下,watchpoint 可能会因为线程切换而失效。我曾经遇到过硬件watchpoint被其他线程覆盖的情况,后来改用软件watchpoint才稳定。
- x 命令的妙用: 检查结构体对齐时,用
x/32xb &struct_var看内存布局,再对比sizeof和成员偏移量,对齐问题一目了然。 - 格式化输出要养成习惯: 看指针用
/x,看字符串用/s,看数组用/d或/u。别偷懒,格式对了,一眼就能发现问题。
可可精品资料库