调试技巧: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
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一查,发现是整数溢出导致优化器生成了错误的代码。
知识体系总览
下面这张图总结了本章的核心内容,你可以把它当成调试工具箱的目录:
实战建议
说了这么多,到底该怎么用?我个人的工作流是这样的:
- 开发阶段:开启ASan和UBSan,跑单元测试和集成测试
- 调试阶段:遇到崩溃先用GDB分析core dump,必要时用反向调试
- CI阶段:所有测试都开启ASan和UBSan,确保没有内存错误和未定义行为
- 生产环境:开启core dump,关闭ASan(性能原因),保留UBSan的部分检测
-fsanitize=address,undefined。但要注意,某些组合可能产生误报,需要根据实际情况调整。
调试这事儿,说白了就是经验和工具的结合。工具给你线索,经验帮你解读线索。多练、多用、多总结,你也能成为调试高手。
公众号:蓝海资料掘金营,微信deep3321