指针调试:从段错误到内存泄漏的实战指南
指针这东西,写的时候很爽,调试的时候真想砸键盘。我做了十几年嵌入式开发,敢说90%的崩溃都跟指针有关。今天咱们就聊聊怎么用工具把这些妖魔鬼怪揪出来。
GDB调试指针:你的第二双眼睛
GDB是我最常用的调试器,没有之一。它就像个显微镜,能让你看到程序运行时内存里的每一个细节。
基本指针查看命令
(gdb) p ptr # 查看指针的值(地址)
(gdb) p *ptr # 查看指针指向的内容
(gdb) p &ptr # 查看指针变量本身的地址
(gdb) x/4xb ptr # 以十六进制查看ptr指向的4个字节
(gdb) x/4wd ptr # 以十进制查看ptr指向的4个字节
我个人习惯先用p ptr看看指针是不是NULL,再用p *ptr看内容。如果p *ptr报错,那基本就是野指针了。
追踪指针链
链表调试是GDB的强项。假设你有个链表节点:
struct node {
int data;
struct node *next;
};
你可以这样遍历:
(gdb) p head
$1 = (struct node *) 0x601050
(gdb) p *head
$2 = {data = 42, next = 0x601070}
(gdb) p *head->next
$3 = {data = 100, next = 0x0}
define命令可以自定义宏,比如定义一个plist命令来打印整个链表,省得一次次敲。
Valgrind:内存问题的照妖镜
Valgrind是我在项目中用过最靠谱的内存检测工具。它不需要重新编译程序,直接跑就行。
基本用法
$ gcc -g -o test test.c
$ valgrind --tool=memcheck --leak-check=full ./test
输出示例:
==12345== Invalid write of size 4
==12345== at 0x4005E4: main (test.c:10)
==12345== Address 0x5204040 is 0 bytes after a block of size 10 alloc'd
==12345== at 0x4C2B800: malloc (vg_replace_malloc.c:299)
==12345== by 0x4005D5: main (test.c:8)
看到没?它直接告诉你:第10行写了不该写的地方,那块内存是在第8行用malloc申请的,大小是10字节,你写到了它后面。
常见Valgrind报告解读
| 报告类型 | 含义 | 常见原因 |
|---|---|---|
| Invalid read/write | 读写非法内存 | 数组越界、野指针 |
| Use of uninitialised value | 使用了未初始化的变量 | 忘记赋值、malloc后没memset |
| Memory leak | 内存泄漏 | malloc后没free |
| Mismatched free | 释放方式不匹配 | 用free释放new的对象 |
段错误的定位与修复
段错误(Segmentation Fault)是C程序员最熟悉的陌生人。说白了就是程序访问了不属于它的内存。
快速定位段错误
方法一:用GDB捕获
$ gcc -g -o test test.c
$ gdb ./test
(gdb) run
Program received signal SIGSEGV, Segmentation fault.
0x00000000004005a3 in main () at test.c:15
15 *ptr = 100;
(gdb) print ptr
$1 = (int *) 0x0
看到没?ptr是NULL,第15行试图往NULL地址写东西,不崩才怪。
方法二:用core dump
$ ulimit -c unlimited
$ ./test
Segmentation fault (core dumped)
$ gdb ./test core
(gdb) bt # 查看调用栈
#0 0x00000000004005a3 in main () at test.c:15
我曾经遇到的一个坑
有一次调试一个网络协议栈,程序跑着跑着就段错误,而且不是每次都复现。查了两天,最后发现是某个回调函数里用了全局指针,但那个指针在另一个线程里被释放了。嗯,这就是典型的"释放后使用"(Use-After-Free)。
修复方法很简单:在释放指针的地方加个NULL赋值,然后在所有使用的地方检查NULL。
指针越界的检测方法
指针越界比段错误更隐蔽,因为它不一定立即崩溃,而是悄悄破坏数据,等程序跑远了才出问题。
静态检测:编译器警告
$ gcc -Wall -Wextra -O2 -fsanitize=address test.c
-fsanitize=address是GCC的AddressSanitizer,它会在编译时插入检测代码,运行时发现越界就报错。我建议所有开发环境都开启这个选项。
动态检测:边界标记法
在嵌入式系统里,有时候没法用Valgrind(资源不够),我会自己写一个简单的边界检测:
#define GUARD_SIZE 4
#define GUARD_PATTERN 0xDEADBEEF
void *safe_malloc(size_t size) {
size_t total = size + 2 * GUARD_SIZE;
unsigned int *ptr = (unsigned int *)malloc(total);
// 设置前哨
for (int i = 0; i < GUARD_SIZE / sizeof(int); i++) {
ptr[i] = GUARD_PATTERN;
}
// 设置后哨
for (int i = (total - GUARD_SIZE) / sizeof(int);
i < total / sizeof(int); i++) {
ptr[i] = GUARD_PATTERN;
}
return (void *)(ptr + GUARD_SIZE / sizeof(int));
}
void check_guard(void *ptr) {
unsigned int *p = (unsigned int *)ptr;
// 检查前哨
if (p[-1] != GUARD_PATTERN) {
printf("前哨被破坏!可能发生了越界写\n");
}
// 检查后哨
// ... 类似逻辑
}
这个方法虽然简陋,但在资源受限的MCU上特别好用。我在一个RTOS项目里就用它抓到了好几个栈溢出问题。
常见越界场景
- 数组遍历越界:循环条件写错,多走了一步
- 字符串操作:strcpy、sprintf没考虑结尾的'\0'
- 结构体指针偏移:用指针算术访问结构体成员时算错了偏移
- 缓冲区溢出:往固定大小的缓冲区写入了超长数据
offsetof宏来算,绝不手算。
本章知识体系
下面这张图总结了指针调试的核心工具和方法:
调试指针问题,说白了就是要有耐心和工具。GDB让你看到内存的实时状态,Valgrind帮你找出隐藏的内存问题,段错误定位让你快速找到崩溃点,越界检测则防患于未然。把这四样工具用好,指针问题基本都能搞定。
记住一点:指针本身不可怕,可怕的是你不知道它指向哪里。用好这些工具,你就能随时知道指针在干什么,问题自然迎刃而解。
公众号:蓝海资料掘金营,微信deep3321