调试技巧:GDB高级用法、Core Dump分析、AddressSanitizer、UndefinedBehaviorSanitizer

调试这事儿,说白了就是跟程序里的bug斗智斗勇。我做了这么多年C/C++,见过太多人只会printf大法,遇到段错误就抓瞎。今天咱们聊聊几个真正能提升效率的调试工具和技巧。

GDB高级用法:不止是断点和print

GDB大家都会用,但大多数人只用了它20%的功能。我个人习惯把GDB当成一个交互式调试器,而不是简单的断点查看器。

条件断点与断点命令

你想想看,一个循环跑100万次,你只想在第999999次停下来看看。手动按c按到手指抽筋?不存在的。

(gdb) break foo.c:42 if i == 999999
(gdb) commands 1
> silent
> printf "i = %d, value = %d\n", i, value
> continue
> end

我在项目中遇到过一个问题:某个数据结构在特定条件下才会被破坏。用条件断点配合commands,每次命中时自动打印关键变量并继续运行,最后通过日志定位到了问题。

反向调试:时光倒流

GDB的reverse debugging是个被低估的功能。说白了就是让程序往回跑。

(gdb) target record-full
(gdb) continue
... 程序崩溃了 ...
(gdb) reverse-step
(gdb) reverse-continue
注意:反向调试会记录每条指令的执行状态,内存开销很大。我建议只在关键路径上开启,别全程开着。

多线程调试

多线程程序出bug,那叫一个头疼。我曾经花了一周时间追一个死锁问题,最后发现是线程A和线程B在互相等待对方释放锁。

(gdb) info threads
(gdb) thread 2
(gdb) bt
(gdb) thread apply all bt

嗯,这里有个小技巧:用thread apply all bt full可以打印所有线程的完整调用栈和局部变量,对分析死锁特别有用。

Core Dump分析:崩溃后的最后一根稻草

程序崩溃了,但现场还在。Core dump就是程序死的时候留下的“遗书”。

开启Core Dump

# 查看当前限制
ulimit -c

# 设置无限制
ulimit -c unlimited

# 设置core文件命名格式
echo "/tmp/core.%e.%p.%t" > /proc/sys/kernel/core_pattern
建议:生产环境一定要开启core dump,但要注意磁盘空间。我一般会设置一个定时任务,清理超过7天的core文件。

分析Core Dump

gdb /path/to/binary /path/to/core

(gdb) bt
(gdb) info registers
(gdb) frame 0
(gdb) list

拿到core文件后,第一件事就是看backtrace。如果看到??或者地址,说明栈被破坏了。这时候可以看看寄存器的值,特别是RSP和RBP。

我记得有一次,core dump显示程序在free()里崩溃了。但看代码逻辑,那个指针明明已经检查过非空了。后来用info locals一看,发现指针的值被改写了——典型的use-after-free。

AddressSanitizer:内存错误的照妖镜

ASan是Google开发的工具,能检测出各种内存错误。说白了就是给每个内存分配加个“围栏”,越界了立刻报错。

启用ASan

gcc -fsanitize=address -g -O1 -o test test.c
./test

就这么简单。ASan会在程序退出时报告所有内存泄漏,运行时检测越界、use-after-free等问题。

常见错误示例

// 堆越界
int *p = (int*)malloc(10 * sizeof(int));
p[10] = 42;  // ASan会报错

// 栈越界
void foo() {
    int arr[5];
    arr[5] = 1;  // ASan会报错
}

// use-after-free
int *p = (int*)malloc(sizeof(int));
free(p);
*p = 42;  // ASan会报错

我的经验:ASan的检测能力很强,但运行速度会慢2-3倍。我一般在单元测试和CI中开启,生产环境不开启。另外,ASan和某些第三方库可能不兼容,遇到奇怪的问题可以先关掉ASan试试。

UndefinedBehaviorSanitizer:未定义行为的猎手

C/C++里有很多未定义行为,编译器可以“自由发挥”。UBSan就是专门抓这些的。

启用UBSan

gcc -fsanitize=undefined -g -O1 -o test test.c
./test

常见未定义行为

行为类型示例UBSan检测
整数溢出int x = INT_MAX + 1;
除零int x = 1 / 0;
移位越界int x = 1 << 32;
空指针解引用int *p = NULL; *p = 1;
类型双关float f = 1.0; int i = *(int*)&f;

你想想看,这些未定义行为在编译器优化下可能产生完全意想不到的结果。我曾经遇到过一个bug:某个函数在Debug模式下运行正常,Release模式下就崩溃。最后用UBSan一查,发现是整数溢出导致优化器生成了错误的代码。

知识体系总览

下面这张图总结了本章的核心内容,你可以把它当成调试工具箱的目录:

C/C++ 调试工具箱 GDB 高级用法 • 条件断点与断点命令 • 反向调试(时光倒流) • 多线程调试 • 调用栈与寄存器分析 Core Dump 分析 • 开启与配置 core dump • 使用 GDB 加载 core 文件 • 栈回溯与寄存器分析 • 生产环境最佳实践 AddressSanitizer • 堆/栈越界检测 • use-after-free 检测 • 内存泄漏报告 • 运行速度影响 2-3x UndefinedBehaviorSanitizer • 整数溢出检测 • 除零检测 • 移位越界检测 • 空指针解引用检测 相互补充,覆盖不同场景 调试不是玄学,是科学。用好工具,事半功倍。

实战建议

说了这么多,到底该怎么用?我个人的工作流是这样的:

  1. 开发阶段:开启ASan和UBSan,跑单元测试和集成测试
  2. 调试阶段:遇到崩溃先用GDB分析core dump,必要时用反向调试
  3. CI阶段:所有测试都开启ASan和UBSan,确保没有内存错误和未定义行为
  4. 生产环境:开启core dump,关闭ASan(性能原因),保留UBSan的部分检测
小贴士:ASan和UBSan可以同时开启:-fsanitize=address,undefined。但要注意,某些组合可能产生误报,需要根据实际情况调整。

调试这事儿,说白了就是经验和工具的结合。工具给你线索,经验帮你解读线索。多练、多用、多总结,你也能成为调试高手。


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