指针调试技巧: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的 printbacktracewatch 是调试指针的三板斧
  • x/ 命令能让你看到内存的原始字节,对调试指针偏移和端序问题特别有用
  • Valgrind的 memcheck 工具能自动检测内存泄漏、越界、未初始化等问题
  • 先Valgrind扫一遍,再用GDB精确定位,这是最高效的工作流

四、知识体系图

下面这张图总结了本章的核心内容,方便你快速回顾:

指针调试技巧知识体系 GDB调试指针 • print 打印指针值 • backtrace 查看调用栈 • watch 观察点 • 条件断点 • x/ 查看内存字节 • 格式化打印 实战技巧 段错误 → print 所有指针 野指针 → watch 观察变化 打印地址与值 • print p → 地址 • print *p → 值 • print &p → 自身地址 • x/4xb → 十六进制字节 • x/4dw → 十进制整数 • void * 需强制转换 常见场景 数组越界 → x/ 查看边界 端序问题 → x/4xb 对比 Valgrind内存检测 • memcheck 工具 • 检测内存泄漏 • 检测越界访问 • 检测未初始化内存 • 检测重复释放 • callgrind 性能分析 最佳实践 先Valgrind扫一遍 再用GDB精确定位

说实话,调试指针问题,心态很重要。别慌,一步步来:先确认指针的值对不对,再看它指向的内存能不能访问,最后检查有没有泄漏。GDB和Valgrind就是你的左膀右臂,用熟了,什么野指针、内存泄漏都不在话下。

嗯,这一章就到这。记住:指针不是洪水猛兽,只是需要你多一份耐心和工具


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