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 解引用时,程序直接崩溃了。后来才发现,这个地址已经被释放了——典型的野指针问题。所以记住:打印指针时,先看地址是否在合理范围内。

小技巧:print 支持表达式计算。比如 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 个以内。

避坑指南:我曾经在调试一个循环时,display 了一个局部变量。结果循环跑了 10000 次,GDB 每次都要打印,调试变得奇慢无比。后来我改用 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 查看栈内容,发现栈被写穿了——典型的栈溢出。

实用技巧:在 x86-64 架构下,函数参数前 6 个通过寄存器传递:$rdi、$rsi、$rdx、$rcx、$r8、$r9。调试时可以直接 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;
}

调试步骤:

  1. 在 arr[i] = arr[i] * 2; 处设断点
  2. 用 display i 和 display arr[i] 跟踪
  3. 当 i == 5 时,p arr[5] 显示垃圾值
  4. 用 x/10dw arr 查看数组周围内存,发现 arr[5] 实际上是 i 变量的地址
  5. 用 p &i 确认地址一致——越界写把循环变量给改了!

你看,只用 print 是不够的,必须结合内存查看才能定位这种「写自己」的诡异问题。

4.7 本章知识体系

下面这张图总结了 GDB 数据查看的核心命令和它们的关系:

GDB 数据查看命令体系 数据查看 print / p display x (examine) info registers p/x p/d p/t p/c p/f 表达式计算 info display undisplay / delete x/10xb x/5dw x/s x/10i $pc p $rax p $rsp info registers rax 调试流程:先 print 看变量 → display 跟踪变化 → x 看内存 → info registers 确认寄存器状态

这张图把四个核心命令放在一起对比。你调试时,按这个顺序走一遍,大部分数据问题都能现形。

最后提醒:数据查看不是目的,目的是找到问题根源。我曾经花了一下午盯着一个变量的值看,后来才发现是另一个线程改了它。所以,数据异常时,先想想「谁改的」而不是「值是多少」。

可可精品资料库