4、调试工具链:GDB调试崩溃、AddressSanitizer (ASan) 检测内存错误、ThreadSanitizer (TSan) 检测数据竞争

说实话,很多C++程序员写代码时信心满满,一遇到崩溃就慌了神。我见过太多人遇到段错误就加printf,然后重新编译——这效率太低了。你想想看,真正的高手,手里都有一套趁手的调试工具链。

今天我就把这套工具链的核心三件套讲透:GDB、AddressSanitizer (ASan)、ThreadSanitizer (TSan)。这三样东西,基本覆盖了90%以上的运行时问题。

核心观点:调试不是靠猜,而是靠证据。工具链就是你的取证专家。

调试工具链三件套 程序崩溃/内存错误/数据竞争 GDB 调试崩溃、查看堆栈 AddressSanitizer 检测内存越界、泄漏 ThreadSanitizer 检测数据竞争 bt / frame / info 断点 / 单步 / 变量 堆溢出 / 栈溢出 use-after-free 读写冲突检测 锁顺序分析 编译时开启 -fsanitize=address / thread,运行时用 GDB 兜底

4.1 GDB:崩溃现场的第一响应人

GDB 这东西,说白了就是你的「事后诸葛亮」。程序崩了,它帮你还原现场。我个人习惯是:只要程序崩溃,第一反应不是看代码,而是跑 GDB。

最基本的用法,你肯定知道:

gdb ./your_program core_dump
(gdb) bt
(gdb) frame 3
(gdb) info locals
(gdb) list

但我想强调几个容易被忽略的点:

  • core dump 要开:ulimit -c unlimited,不然崩了就白崩了。我在项目中遇到过好几次,同事说「程序崩了,但没 core 文件」——其实就是没开这个限制。
  • 多线程调试用 thread apply all bt:所有线程的堆栈一次打出来,哪个线程崩了一目了然。
  • 条件断点:break foo if x == 42,省得你手动按几百次 continue。

小技巧:GDB 里可以用 set print pretty on 让结构体输出更整齐。我每次进 GDB 第一件事就是敲这个。

我曾经遇到过一个诡异的崩溃:程序跑几个小时才崩一次,而且堆栈每次都不同。后来我用 GDB 的 watch 命令监控了一个关键指针,发现是某个线程在释放内存后还在写——典型的 use-after-free。

4.2 AddressSanitizer (ASan):内存错误的照妖镜

ASan 是我最爱的工具,没有之一。它能在编译时插入检测代码,运行时一旦发生内存错误,立刻报错并打印堆栈。说白了,它把你的「隐性问题」变成了「显式崩溃」。

用法很简单:

g++ -fsanitize=address -g -O1 -o test test.cpp
./test

ASan 能检测的错误类型包括:

错误类型 典型表现 ASan 输出特征
堆缓冲区溢出 写越界、读越界 heap-buffer-overflow
栈缓冲区溢出 局部数组越界 stack-buffer-overflow
全局缓冲区溢出 全局数组越界 global-buffer-overflow
use-after-free 释放后继续使用 heap-use-after-free
内存泄漏 未释放堆内存 direct leak / indirect leak

注意:ASan 会显著增加内存占用(约2倍)和运行时间(约2-3倍)。所以只在调试阶段开启,不要带到生产环境。

我记得有一次,一个同事排查了三天没找到的内存泄漏,我用 ASan 一跑,直接定位到某个 std::vector 在循环里不断 push_back 但没 clear。ASan 的输出里明确写了 direct leak of 1024 byte(s),还附带了分配时的堆栈——这效率,你想想看。

4.3 ThreadSanitizer (TSan):数据竞争的侦探

多线程程序最难搞的问题就是数据竞争。两个线程同时读写同一个变量,结果不确定,bug 时隐时现。TSan 就是专门干这个的。

用法:

g++ -fsanitize=thread -g -O1 -pthread -o test test.cpp
./test

TSan 能检测到:

  • 数据竞争:多个线程同时访问同一内存位置,至少有一个是写操作,且没有同步。
  • 锁顺序问题:可能导致死锁的锁获取顺序不一致。
  • 不正确的原子操作:比如该用 atomic 却用了普通变量。

举个例子:

int counter = 0;
void thread_func() {
    for (int i = 0; i < 100000; ++i) {
        counter++;  // 数据竞争!
    }
}

这段代码用 TSan 跑,会直接报 WARNING: ThreadSanitizer: data race,并告诉你哪两个线程在争抢 counter。

避坑指南:我曾经在一个项目中,用 TSan 检测到一个「看起来没问题」的数据竞争——两个线程分别读写不同的成员变量,但这两个变量在同一个结构体里,恰好在一个缓存行上。TSan 连这种「伪共享」都能检测出来,确实厉害。

4.4 三件套的配合使用

在实际项目中,我一般这样用:

  1. 开发阶段:默认开启 ASan 和 TSan,跑单元测试和集成测试。
  2. 崩溃发生时:先用 GDB 看堆栈,定位崩溃位置。
  3. 定位到疑似内存错误:用 ASan 复现,确认具体错误类型。
  4. 定位到疑似数据竞争:用 TSan 复现,确认竞争位置。

这三者不是替代关系,而是互补关系。GDB 告诉你「哪里崩了」,ASan 告诉你「内存哪里坏了」,TSan 告诉你「哪里在打架」。配合使用,基本没有查不出的问题。

个人建议:把这三样东西写进你的 CI/CD 流程。每次提交代码,自动用 ASan 和 TSan 跑一遍测试。别等到上线了再查,那成本就高了。

嗯,工具链就讲到这里。记住一句话:不要用手去摸电门,用万用表去量。调试工具就是你的万用表。

可可精品资料库