4. GDB数据查看
调试的核心是什么?说白了,就是看数据。
你设了断点,程序停下来了。然后呢?你得知道变量值对不对,内存里到底写了什么,结构体有没有被踩坏。GDB里最常用的就是print命令,但很多人只用了个皮毛。今天我把这块掰开揉碎了讲清楚。
4.1 print命令的基本用法
print,缩写是p。用法很简单:p 变量名。但这里有几个细节,我当年踩过坑。
(gdb) p i
$1 = 42
看到那个$1了吗?这是GDB自动给结果编的号。每次print,结果都会被存到GDB的“历史记录”里。你可以用$1、$2来引用之前的结果。
print /f 或者直接用 call 命令。
print还能直接算表达式:
(gdb) p i + j
$2 = 100
(gdb) p &i
$3 = (int *) 0x7fffffffe4a0
嗯,这里要注意:取地址操作符 & 在GDB里完全可用。我经常用它来检查指针是否指向了非法区域。
4.2 显示格式控制
默认情况下,print按变量的类型来显示。但有时候你需要看别的格式。比如一个整数,你想看它的十六进制、二进制,或者字符表示。
格式控制符用斜杠加字母:p /格式 变量
| 格式符 | 含义 | 示例 |
|---|---|---|
| /x | 十六进制 | p /x i |
| /d | 有符号十进制 | p /d i |
| /u | 无符号十进制 | p /u i |
| /o | 八进制 | p /o i |
| /t | 二进制 | p /t i |
| /c | 字符 | p /c i |
| /f | 浮点数 | p /f f |
| /s | 字符串(对指针) | p /s ptr |
举个例子,你有个寄存器值,想看看它的二进制位:
(gdb) p /t 0xABCD
$4 = 1010101111001101
我在调试嵌入式驱动时,经常用/t来看状态寄存器的各个位。一眼就能看出哪个标志位被置了。
4.3 查看内存:x命令
print看的是变量,x命令看的是内存。x是examine的缩写。
语法:x /[数量][格式][大小] 地址
比如:
(gdb) x /10xb 0x7fffffffe4a0
0x7fffffffe4a0: 0x2a 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7fffffffe4a8: 0x00 0x00
这里:
- 10:显示10个单位
- x:十六进制格式
- b:每个单位1字节(byte)
大小选项:
- b:1字节
- h:2字节(halfword)
- w:4字节(word)
- g:8字节(giant)
格式选项和print一样:x、d、u、o、t、c、f、s、i(指令)
看字符串数组特别方便:
(gdb) x /s 0x400600
0x400600: "Hello, GDB!"
看指令(反汇编):
(gdb) x /5i $pc
=> 0x400526 <main+4>: mov $0x0,%eax
0x40052b <main+9>: mov $0x400600,%edi
0x400530 <main+14>: callq 0x400400 <puts@plt>
这个/i格式在调试汇编代码时简直是神器。我经常用它来确认当前执行到了哪条指令。
4.4 自动显示:display命令
每次单步执行后,你想自动看到某些变量的值?用display。
(gdb) display i
1: i = 42
(gdb) display /x j
2: j = 0x64
每次执行next、step、continue后,GDB会自动打印这些表达式。
常用display命令:
- display 表达式:添加自动显示
- info display:列出所有自动显示项
- delete display 编号:删除指定项
- disable display 编号:临时禁用
- enable display 编号:重新启用
- undisplay 编号:同delete display
举个例子,你调试一个循环:
(gdb) display i
(gdb) display sum
(gdb) break 20
(gdb) continue
...
1: i = 0
2: sum = 0
(gdb) next
1: i = 1
2: sum = 1
(gdb) next
1: i = 2
2: sum = 3
每次执行next,GDB自动打印i和sum的值。省得你每次手动print。
4.5 实战:调试一个缓冲区溢出
我记得有一次,一个同事说他的程序在某个函数返回时崩溃。我帮他调试,用到了今天讲的这些命令。
场景:一个简单的字符串拷贝,但目标缓冲区太小。
#include <stdio.h>
#include <string.h>
void vulnerable() {
char buffer[8];
char *input = "Hello, GDB! This is a test.";
strcpy(buffer, input);
printf("buffer: %s\n", buffer);
}
int main() {
vulnerable();
return 0;
}
编译时加-g,然后调试:
(gdb) break vulnerable
(gdb) run
(gdb) p buffer
$1 = "Hello, G"
(gdb) x /20xb buffer
0x7fffffffe4a0: 0x48 0x65 0x6c 0x6c 0x6f 0x2c 0x20 0x47
0x7fffffffe4a8: 0x44 0x42 0x21 0x20 0x54 0x68 0x69 0x73
0x7fffffffe4b0: 0x20 0x69 0x73 0x20
看到了吗?buffer只有8字节,但数据已经写到了0x7fffffffe4a8之后。这就是溢出。用x命令看内存,能直观地看到数据覆盖了相邻的栈空间。
好了,这一章的内容就这些。print、格式控制、x命令、display,每个都是调试时的利器。你想想看,如果只会用print,遇到内存问题是不是就抓瞎了?所以这些命令都得练熟。
可可精品资料库