指针调试技巧:GDB调试指针问题、打印指针地址与值、内存检测工具(Valgrind)
指针这东西,写的时候很爽,调试的时候真想砸键盘。我见过太多同事,代码编译通过就以为万事大吉,结果一跑就崩。说实话,指针问题往往是「静如处子,动如脱兔」——平时不报错,关键时刻给你来个段错误。
这一章,我就把压箱底的调试技巧掏出来。咱们分三块聊:GDB怎么抓指针的现行、怎么把指针的地址和值看得明明白白、Valgrind这个内存检测神器怎么用。嗯,都是实战中摔出来的经验。
一、GDB调试指针问题:让野指针无处遁形
GDB是我最常用的调试器,没有之一。你想想看,一个指针到底指向哪,打印出来看看不就完了?
1. 基本调试命令
先来个最基础的例子:
#include <stdio.h>
int main() {
int a = 42;
int *p = &a;
int *q = NULL;
printf("a = %d\n", a);
// 这里故意写个错误:解引用空指针
*q = 100; // 段错误!
return 0;
}
编译时记得加 -g 选项:
gcc -g -o test test.c
然后启动GDB:
gdb ./test
(gdb) run
程序崩了,GDB会停在出错的地方。这时候我一般先看指针的值:
(gdb) print p
$1 = (int *) 0x7fffffffe4c4
(gdb) print *p
$2 = 42
(gdb) print q
$3 = (int *) 0x0
(gdb) print *q
Cannot access memory at address 0x0
看到了吧?q是0x0,也就是NULL。解引用NULL指针,不崩才怪。
print 所有指针变量。哪个是0x0,哪个是0xdeadbeef(野指针常见值),一目了然。
2. 查看调用栈
有时候指针问题不是当前行引起的,是之前就埋下了雷。这时候用 backtrace:
(gdb) backtrace
#0 0x0000555555554716 in main () at test.c:8
#1 0x00007ffff7a05b97 in __libc_start_main () from /lib/x86_64-linux-gnu/libc.so.6
#2 0x00005555555546da in _start ()
嗯,#0就是当前崩溃的位置。如果函数调用很深,你能看到完整的调用链。我曾经靠这个追过一个三层回调里的野指针问题——那个bug藏了两周才找到。
3. 条件断点与观察点
如果指针在循环里被改来改去,手动一步步跟太累了。用条件断点:
(gdb) break test.c:12 if p == 0x0
(gdb) run
或者用 watch 观察指针的变化:
(gdb) watch p
(gdb) continue
只要 p 的值变了,GDB就会停下来。这招抓野指针特别管用——你想想看,指针突然指向一个奇怪地址,watch点会立刻告诉你是在哪一行变的。
二、打印指针地址与值:把内存扒开看
GDB里打印指针,其实有很多花样。我刚开始学的时候只会 print,后来才发现有这么多好用的命令。
1. 格式化打印
| 命令 | 作用 | 示例输出 |
|---|---|---|
print p |
打印指针的值(地址) | 0x7fffffffe4c4 |
print *p |
打印指针指向的内容 | 42 |
print &p |
打印指针变量本身的地址 | 0x7fffffffe4c8 |
x/4xb p |
以十六进制查看内存 | 0x7fffffffe4c4: 0x2a 0x00 0x00 0x00 |
x/4dw p |
以十进制查看内存 | 0x7fffffffe4c4: 42 |
我个人最常用的是 x/4xb,因为看字节级别的数据,能发现很多端序相关的问题。有一次我调试一个网络协议栈,发现收到的数据总是反的,用 x/4xb 一看——哦,大小端没转换。
2. 查看数组和字符串
char str[] = "Hello, Pointer!";
char *p = str;
(gdb) print str
$1 = "Hello, Pointer!"
(gdb) print p
$2 = 0x7fffffffe4b0 "Hello, Pointer!"
(gdb) x/16bc p
0x7fffffffe4b0: 72 'H' 101 'e' 108 'l' 108 'l' 111 'o' 44 ',' 32 ' ' 80 'P'
0x7fffffffe4b8: 111 'o' 105 'i' 110 'n' 116 't' 101 'e' 114 'r' 33 '!' 0 '\0'
看到没?print p 直接就把字符串内容也打印出来了,GDB很智能。但如果是 void * 指针,它只会打印地址,不会自动解引用。这时候需要强制类型转换:
(gdb) print *(int *)void_ptr
void * 指针时,GDB不知道它指向什么类型。一定要先转换成正确的类型再解引用,否则打印出来的数据是错的。
三、Valgrind:内存问题的照妖镜
GDB是动态调试,Valgrind是静态分析。两者配合,基本能覆盖90%的内存问题。我个人的工作流是:先用Valgrind跑一遍,发现问题再用GDB精确定位。
1. 安装与基本使用
# Ubuntu/Debian
sudo apt-get install valgrind
# 使用
valgrind --tool=memcheck ./test
跑一个带内存泄漏的例子:
#include <stdlib.h>
void leak_memory() {
int *p = (int *)malloc(10 * sizeof(int));
// 忘记 free!
}
int main() {
leak_memory();
return 0;
}
Valgrind输出:
==12345== HEAP SUMMARY:
==12345== in use at exit: 40 bytes in 1 blocks
==12345== total heap usage: 1 allocs, 0 frees, 40 bytes allocated
==12345==
==12345== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
==12345== at 0x4C2B800: malloc (vg_replace_malloc.c:299)
==12345== by 0x4004E7: leak_memory (test.c:4)
==12345== by 0x4004F7: main (test.c:9)
==12345==
==12345== LEAK SUMMARY:
==12345== definitely lost: 40 bytes in 1 blocks
看到了吧?它告诉你:leak_memory 函数第4行 malloc 了40字节,没释放。这就是「definitely lost」——确定泄漏了。
2. 常见错误检测
Valgrind能检测的内存问题可不止泄漏:
- 使用未初始化的内存:
Conditional jump or move depends on uninitialised value(s) - 越界访问:
Invalid write of size 4 - 重复释放:
Invalid free() / delete / delete[] / realloc() - 内存泄漏:
definitely lost/indirectly lost
我曾经接手过一个项目,代码里有个诡异的崩溃,一周都没找到原因。我抱着试试看的心态跑了Valgrind,结果它报了一堆「Invalid read of size 8」——原来是一个结构体指针偏移算错了,读到了隔壁的内存区域。嗯,从那以后,我每个项目上线前都会跑一遍Valgrind。
3. 高级选项
Valgrind有很多实用选项,我常用的几个:
# 显示详细信息
valgrind --leak-check=full ./test
# 显示所有泄漏,包括可能泄漏的
valgrind --leak-check=full --show-leak-kinds=all ./test
# 跟踪子进程
valgrind --trace-children=yes ./test
# 生成调用图(配合gprof2dot可视化)
valgrind --tool=callgrind ./test
核心要点:
- GDB的
print、backtrace、watch是调试指针的三板斧 x/命令能让你看到内存的原始字节,对调试指针偏移和端序问题特别有用- Valgrind的
memcheck工具能自动检测内存泄漏、越界、未初始化等问题 - 先Valgrind扫一遍,再用GDB精确定位,这是最高效的工作流
四、知识体系图
下面这张图总结了本章的核心内容,方便你快速回顾:
说实话,调试指针问题,心态很重要。别慌,一步步来:先确认指针的值对不对,再看它指向的内存能不能访问,最后检查有没有泄漏。GDB和Valgrind就是你的左膀右臂,用熟了,什么野指针、内存泄漏都不在话下。
嗯,这一章就到这。记住:指针不是洪水猛兽,只是需要你多一份耐心和工具。
公众号:蓝海资料掘金营,微信deep3321