28. 常见指针错误调试:段错误的定位、使用 GDB 调试指针问题、打印指针值

指针这东西,写的时候很爽,出问题的时候也很“爽”。尤其是段错误(Segmentation Fault),几乎是每个 C 语言开发者都绕不开的噩梦。我记得刚入行那会儿,一看到 "Segmentation fault (core dumped)" 就头皮发麻。后来经验多了,发现其实大部分段错误都是有迹可循的。

说白了,段错误就是程序试图访问它“不该碰”的内存区域。比如访问了 NULL 指针、访问了已经释放的内存、或者写入了只读区域。今天我们就来聊聊怎么定位这些问题,以及怎么用 GDB 这把利器来“捉鬼”。

28.1 段错误的常见“元凶”

我总结了一下,90% 的指针相关段错误,逃不出下面这几类:

  • 解引用 NULL 指针:这是最常见的。你声明了一个指针,没给它赋值,或者赋了 NULL,然后直接往里面写数据。
  • 访问已释放的内存(野指针):free 之后没有把指针置为 NULL,后续又继续使用。这是悬空指针(Dangling Pointer)的典型表现。
  • 数组越界:C 语言不检查数组边界。你写了个 `arr[100]`,但数组只定义了 10 个元素。运气好没事,运气不好就覆盖了别的数据,甚至触发段错误。
  • 栈溢出:递归太深,或者局部变量太大,导致栈空间耗尽。这时候程序会直接崩溃。
  • 修改字符串常量:比如 `char *p = "hello"; p[0] = 'H';`。字符串常量是只读的,你试图修改它,系统会直接给你一个段错误。
⚠️ 警告: 我曾经在一个项目中,因为一个全局指针在多个线程中被同时读写,导致偶发的段错误。这种问题最难查,因为它不是每次都能复现。所以,多线程环境下操作指针,一定要加锁。

28.2 如何快速定位段错误?

遇到段错误,别慌。先看看能不能复现。如果能稳定复现,那就好办了。我个人习惯按以下步骤来:

  1. 看错误信息:如果程序打印了 "Segmentation fault",并且生成了 core dump 文件,那是最好的情况。
  2. 用 GDB 加载 core 文件:`gdb ./your_program core`。然后输入 `bt`(backtrace),GDB 会告诉你程序崩溃时正在执行哪一行代码。
  3. 如果没有 core 文件:用 GDB 直接运行程序,让它崩溃在 GDB 里。这样也能看到调用栈。
  4. 检查指针值:在 GDB 里用 `print p` 打印指针的值。如果显示 `0x0`,那就是 NULL 指针。如果显示一个奇怪的地址(比如 `0xdeadbeef`),那可能是野指针。

你想想看,很多时候我们写代码时觉得逻辑没问题,但程序就是不听话。这时候,GDB 就是你的“照妖镜”。

28.3 使用 GDB 调试指针问题

GDB 的功能非常强大,但对付指针问题,我们只需要掌握几个核心命令就够了。下面我通过一个例子来演示。

假设我们有下面这段有问题的代码:

#include <stdio.h>
#include <stdlib.h>

void cause_segfault() {
    int *p = NULL;
    *p = 42;  // 这里会触发段错误
}

int main() {
    printf("Before crash\n");
    cause_segfault();
    printf("After crash\n");
    return 0;
}

编译时记得加上 `-g` 选项,这样才能保留调试信息:

gcc -g -o test test.c

然后启动 GDB:

gdb ./test

在 GDB 里输入 `run`,程序会崩溃。你会看到类似这样的信息:

Program received signal SIGSEGV, Segmentation fault.
0x0000555555555156 in cause_segfault () at test.c:6
6           *p = 42;

看到了吗?GDB 直接告诉你在 `test.c` 的第 6 行出错了。这时候,你可以:

  • 输入 bt 查看调用栈,确认是谁调用了 `cause_segfault`。
  • 输入 print p 查看指针的值。你会看到 `p = (int *) 0x0`,果然是 NULL 指针。
  • 输入 info locals 查看当前函数的所有局部变量。
💡 小技巧: 我经常用 watch 命令来监视一个指针变量。比如 watch *p,当 p 指向的内存内容发生变化时,GDB 会自动停下来。这在调试复杂的内存修改问题时非常有用。

28.4 打印指针值:不仅仅是地址

很多人觉得打印指针值就是看个地址,其实不然。通过指针值,我们能获取很多信息。

在 GDB 里,`print p` 会打印指针的地址。但如果你想知道指针指向的内容,要用 `print *p`。如果 p 是一个结构体指针,`print *p` 会打印整个结构体的内容。

举个例子:

typedef struct {
    int id;
    char name[32];
} User;

User *user = malloc(sizeof(User));
user->id = 100;
strcpy(user->name, "Alice");

在 GDB 里:

  • print user 会输出类似 `(User *) 0x5555555592a0` 的地址。
  • print *user 会输出 `{id = 100, name = "Alice"}`。
  • print user->id 直接输出 `100`。

嗯,这里要注意一点:如果你打印 `*user` 时,GDB 显示 ``,那说明这个指针指向的内存已经无效了——大概率是已经被 free 了。

28.5 避坑指南:我曾经踩过的坑

最后,分享几个我亲身经历过的“血泪史”:

  • 忘记初始化指针:声明一个局部指针变量,它的初始值是随机的(栈上的垃圾值)。如果你不小心解引用了它,那就是一个随机地址的访问,必崩无疑。所以,声明指针时,要么立即赋值,要么赋为 NULL。
  • free 之后没有置 NULL:free 只是释放了内存,但指针变量本身的值还在。如果你后续不小心又用了它,那就是访问野指针。我现在的习惯是:free 之后立即把指针赋为 NULL。
  • 在函数返回局部变量的地址:比如你写了一个函数,返回了一个局部数组的地址。函数返回后,那个数组的栈空间就被回收了。调用方拿到的是一个无效地址。这种错误在编译时可能不会报错,但运行时随时可能崩溃。
🔑 核心要点: 调试指针问题,本质上就是“确认指针指向的内存是否合法”。用 GDB 打印指针值、查看调用栈、检查内存内容,这三板斧能解决 90% 的问题。

好了,关于段错误的定位和 GDB 调试,我们就聊到这里。记住,遇到段错误不要怕,它只是程序在告诉你:“嘿,你碰了不该碰的内存!” 学会用工具,学会分析,你就能和它和平共处。


公众号:蓝海资料掘金营,微信deep3321