Valgrind、Google Sanitizer

Valgrind

Valgrind 的官方标准发音为:/ˈvælɡrɪnd/

发音拆解与要点:

  • Val-:读作 [væl],类似 value 开头的发音(“瓦尔”)。
  • -grind:读作 [ɡrɪnd],中间是短元音 [ɪ],押韵于 grinned而不是平时背单词 grind 里的双元音 [aɪ](不读成类似 blind / find 的音)。
  • 中文谐音近似“瓦尔·格林德”(重音在第一个音节)。

“Valgrind”这个词的意思,可以从神话起源技术指代两个层面来理解:

1. 字面意思与神话起源(Norse Mythology) 在北欧神话中,Valgrind(瓦尔格林德) 是通往 英灵殿(Valhalla,瓦尔哈拉) 的那扇神圣的外层大门。 这扇大门极其古老且坚固,传说中只有那些在战场上英勇战死的灵魂(英灵),才有资格通过这扇大门进入神殿。因此,它的字面本意可以理解为“神圣的筛选之门”或“死者之门”。

2. 程序员眼中的隐喻 Valgrind 这个内存检测工具的最初作者 Julian Seward 借用了这个神话典故,赋予了它一个非常极客的硬核隐喻: 在代码的世界里,Valgrind 就是一扇无情的“质检大门”。它负责把那些带有内存泄漏、越界访问、未初始化变量等致命 Bug 的“烂代码”统统挡在门外。只有经过了它严苛的检验,证明自己是没有内存缺陷的“健壮代码”,才有资格进入发布环节(如同进入代码的英灵殿)。


在 C/C++ 的开发世界里,内存问题往往是最令人头疼的幽灵,而 Valgrind 堪称我们排查这些问题的“保命神器”。

很多开发者以为 Valgrind 只是一个查内存泄漏的工具,但实际上,它是一个动态二进制插桩(Dynamic Binary Instrumentation)框架。我们平时常说的“用 Valgrind 查内存”,其实是指它默认携带的、也是最著名的子工具 —— Memcheck

01 Memcheck

在实际开发中,主要利用 Valgrind 的 Memcheck 来捕捉以下四类极难复现的内存问题:

  1. 内存泄漏 (Memory Leak): 这是最基础的功能。当程序退出时,Valgrind 会给出一份详细的泄漏报告,并将泄漏分为几类:
    • Definitely lost(绝对泄漏): 毫无疑问,指向该内存的指针已经彻底丢失了,必须修复。
    • Indirectly lost(间接泄漏): 指向该内存的结构体本身也泄漏了(比如二叉树的根节点丢了,子节点就是间接泄漏)。
    • Possibly lost(可能泄漏): 内存指针可能存在偏移,Valgrind 不确定。
    • Still reachable(仍然可达): 程序结束时内存没释放,但指针还在(通常发生在全局变量或单例模式中),操作系统会回收,一般不算严重 Bug。
  2. 非法的内存访问 (Invalid Read/Write):
    • 越界访问: 读写了 malloc/new 分配出来的堆内存边界之外的数据(Heap Out-of-Bounds)。
    • Use-After-Free(悬垂指针): 内存已经被 free/delete 了,但程序还在读写它。这在多线程并发环境下极其容易引发崩溃。
  3. 使用了未初始化的变量 (Use of Uninitialised Values): 如果你定义了一个局部变量(在栈上)却没有赋初值,然后直接用它去控制 if 分支或者作为数组下标,Valgrind 会立刻精准报警。
  4. 非法的内存释放 (Mismatched Free): 比如你用 new[] 申请了数组,却用 delete(而不是 delete[])去释放;或者用 malloc 申请,却用 delete 释放;亦或是Double Free(重复释放同一块内存)

03 其它子工具

  • Helgrind / DRD: 专门用于多线程并发错误检测它可以精准抓取到两个线程在没有加锁的情况下同时读写同一个共享变量产生的数据竞争(Data Race),以及死锁问题。
  • Callgrind / Cachegrind: 性能缓存分析工具。它可以生成函数调用图,统计每一行代码消耗的 CPU 指令数,以及极其硬核的 CPU L1/L2 Cache 命中率分析。

02 底层原理

面试官通常会问:“为什么 Valgrind 不需要重新编译代码(甚至不需要源码),直接跑可执行文件就能查出 Bug?

它的核心原理是 构建了一个虚拟的 CPU 环境

  1. 动态重编译: 你的程序不是直接运行在真实的 CPU 上的。Valgrind 会把你的机器码(二进制指令)实时翻译成一种中间表示(IR)。
  2. 影子内存 (Shadow Memory): 这是 Memcheck 的核心魔法。对于程序申请的每一个字节(Byte)的内存,Valgrind 都会在底层默默为它分配两组比特位:
    • V bits (Valid-Value bits): 记录这块内存里的数据是否已经被合法初始化了。
    • A bits (Valid-Address bits): 记录这块内存的地址是否允许被程序读写。
  3. 指令插桩: Valgrind 会在翻译后的 IR 代码中强行插入大量的检查指令。每当你的代码发生内存读写,它都会先去查一下影子内存里的 A bits 和 V bits。如果发现你读了一个不该读的地址,或者用了一个没初始化的值,立刻触发警告。

简而言之就是,它的原理是在一个轻量级虚拟 CPU 环境中运行你的可执行文件,动态重写机器码并监控每一块内存的分配与访问。代价是程序运行速度会慢 10~50 倍

04 痛点与现代替代方案

虽然 Valgrind 很强大,但它在工程实践中有一个致命的弱点:极其缓慢因为所有的指令都要经过 翻译插桩,运行速度通常会下降 10 倍到 50 倍,内存消耗也会翻倍。这导致它很难用在体积庞大的真实业务服务的线上压测中。

因此,在现代 C++ 开发中,我们越来越倾向于使用 GCC / Clang 编译器自带的 AddressSanitizer (ASan)

  • 对比优势: ASan 是在 编译期 进行插桩的,它只需要你在 CMake 里加上 -fsanitize=address 编译选项。它的运行速度通常只下降 2 倍左右,比 Valgrind 快得多,而且同样能精准定位内存越界和泄漏。

05 使用示例

检测内存泄漏

  Valgrind 可以用来检测程序在哪个位置发生内存泄漏,例如下面的程序:

#include <cstdlib>

int main()
{
    int *array = (int *)malloc(sizeof(int));
    return 0;
}

  编译程序时,需要加上 -g 选项:

$ gcc -g -o main_c main.c

  使用 Valgrind 检测内存使用情况:

$ valgrind --tool=memcheck --leak-check=full  ./main_c
==31416== Memcheck, a memory error detector
==31416== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==31416== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==31416== Command: ./main_c
==31416==
==31416==
==31416== HEAP SUMMARY:
==31416==     in use at exit: 4 bytes in 1 blocks
==31416==   total heap usage: 1 allocs, 0 frees, 4 bytes allocated
==31416==
==31416== 4 bytes in 1 blocks are definitely lost in loss record 1 of 1
==31416==    at 0x4C2DBF6: malloc (vg_replace_malloc.c:299)
==31416==    by 0x400537: main (main.c:5)
==31416==
==31416== LEAK SUMMARY:
==31416==    definitely lost: 4 bytes in 1 blocks
==31416==    indirectly lost: 0 bytes in 0 blocks
==31416==      possibly lost: 0 bytes in 0 blocks
==31416==    still reachable: 0 bytes in 0 blocks
==31416==         suppressed: 0 bytes in 0 blocks
==31416==
==31416== For counts of detected and suppressed errors, rerun with: -v
==31416== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

  先看看输出信息中的 HEAP SUMMARY,它表示程序在堆上分配内存的情况,其中的 1 allocs 表示程序分配了 1 次内存,0 frees 表示程序释放了 0 次内存,4 bytes allocated 表示分配了 4 个字节的内存。
  另外,Valgrind 也会报告程序是在哪个位置发生内存泄漏。例如,从下面的信息可以看到,程序发生了一次内存泄漏,位置是 main.c 文件的第 5 行:

==31416== 4 bytes in 1 blocks are definitely lost in loss record 1 of 1
==31416==    at 0x4C2DBF6: malloc (vg_replace_malloc.c:299)
==31416==    by 0x400537: main (main.c:5)

  Valgrind 也可以用来检测 C++ 程序的内存泄漏,下面是一个正常的 C++ 程序,没有发生内存泄漏:

#include <string>

int main()
{
    auto ptr = new std::string("Hello, World!");
    delete ptr;

    return 0;
}

  使用 Valgrind 分析这段程序:

$ valgrind --tool=memcheck --leak-check=full --show-leak-kinds=all ./main_cpp
==31438== Memcheck, a memory error detector
==31438== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==31438== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==31438== Command: ./main_cpp
==31438==
==31438==
==31438== HEAP SUMMARY:
==31438==     in use at exit: 72,704 bytes in 1 blocks
==31438==   total heap usage: 2 allocs, 1 frees, 72,736 bytes allocated
==31438==
==31438== 72,704 bytes in 1 blocks are still reachable in loss record 1 of 1
==31438==    at 0x4C2DBF6: malloc (vg_replace_malloc.c:299)
==31438==    by 0x4EC3EFF: ??? (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)
==31438==    by 0x40104E9: call_init.part.0 (dl-init.c:72)
==31438==    by 0x40105FA: call_init (dl-init.c:30)
==31438==    by 0x40105FA: _dl_init (dl-init.c:120)
==31438==    by 0x4000CF9: ??? (in /lib/x86_64-linux-gnu/ld-2.23.so)
==31438==
==31438== LEAK SUMMARY:
==31438==    definitely lost: 0 bytes in 0 blocks
==31438==    indirectly lost: 0 bytes in 0 blocks
==31438==      possibly lost: 0 bytes in 0 blocks
==31438==    still reachable: 72,704 bytes in 1 blocks
==31438==         suppressed: 0 bytes in 0 blocks
==31438==
==31438== For counts of detected and suppressed errors, rerun with: -v
==31438== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

  使用 Valgrind 分析 C++ 程序时,有一些问题需要留意。例如,这个程序并没有发生内存泄漏,但是从 HEAP SUMMARY 可以看到,程序分配了 2 次内存,但却只释放了 1 次内存,为什么会这样呢?

实际上这是由于 C++ 在分配内存时,为了提高效率,使用了它自己的内存池。当程序终止时,内存池的内存才会被操作系统回收,所以 Valgrind 会将这部分内存报告为 reachable 的,需要注意,reachable 的内存不代表内存泄漏,例如,从上面的输出中可以看到,有 72704 个字节是 reachable 的,但没有报告内存泄漏。

检测越界访问

C++ 程序经常出现的 Bug 就是数组越界访问,例如下面的程序出现了越界访问:

#include <vector>
#include <iostream>

int main()
{
    std::vector<int> v(10, 0);
    std::cout << v[10] << std::endl;

    return 0;
}

使用 Valgrind 分析这段程序,Valgrind 会提示越界访问:

$ g++ -std=c++11 -g -o main_cpp main.cpp
$ valgrind --tool=memcheck --leak-check=full ./main_cpp
==31523== Memcheck, a memory error detector
==31523== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==31523== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==31523== Command: ./main_cpp
==31523==
==31523== Invalid read of size 4
==31523==    at 0x400AD7: main (main.cpp:7)
==31523==  Address 0x5ab5ca8 is 0 bytes after a block of size 40 alloc'd
==31523==    at 0x4C2E216: operator new(unsigned long) (vg_replace_malloc.c:334)
==31523==    by 0x4010D3: __gnu_cxx::new_allocator<int>::allocate(unsigned long, void const*) (new_allocator.h:104)
==31523==    by 0x401040: std::allocator_traits<std::allocator<int> >::allocate(std::allocator<int>&, unsigned long) (alloc_traits.h:491)
==31523==    by 0x400F91: std::_Vector_base<int, std::allocator<int> >::_M_allocate(unsigned long) (stl_vector.h:170)
==31523==    by 0x400E7E: std::_Vector_base<int, std::allocator<int> >::_M_create_storage(unsigned long) (stl_vector.h:185)
==31523==    by 0x400D1E: std::_Vector_base<int, std::allocator<int> >::_Vector_base(unsigned long, std::allocator<int> const&) (stl_vector.h:136)
==31523==    by 0x400C11: std::vector<int, std::allocator<int> >::vector(unsigned long, int const&, std::allocator<int> const&) (stl_vector.h:291)
==31523==    by 0x400AB9: main (main.cpp:6)

  Invalid read of size 4表示越界读取 4 个字节,这个操作出现在main.cpp文件的第 7 行。另外可以看到,vector分配了一块 40 字节的内存,程序越界访问紧急着这块内存之后的 4 个字节。

检测未初始化的内存

  另一种经常出现的 Bug,就是程序访问了未初始化的内存。例如:

#include <iostream>

int main()
{
    int x;
    if (x == 0)
    {
        std::cout << "X is zero" << std::endl;
    }

    return 0;
}

  使用 Valgrind 检测这个程序:

❯ g++ -std=c++11 -g -o tmp tmp.cpp
❯ valgrind --tool=memcheck --leak-check=full ./tmp
==779108== Memcheck, a memory error detector
==779108== Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al.
==779108== Using Valgrind-3.22.0 and LibVEX; rerun with -h for copyright info
==779108== Command: ./tmp
==779108== 
==779108== Conditional jump or move depends on uninitialised value(s)
==779108==    at 0x109179: main (tmp.cpp:6)
==779108== 
==779108== 
==779108== HEAP SUMMARY:
==779108==     in use at exit: 0 bytes in 0 blocks
==779108==   total heap usage: 1 allocs, 1 frees, 73,728 bytes allocated
==779108== 
==779108== All heap blocks were freed -- no leaks are possible
==779108== 
==779108== Use --track-origins=yes to see where uninitialised values come from
==779108== For lists of detected and suppressed errors, rerun with: -s
==779108== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

  输出中提示了 tmp.cpp 文件的第 6 行访问了未初始化的内存。

参考资料

https://zhuanlan.zhihu.com/p/15101814919

Sanitizer

Sanitizer 的标准发音为:/ˈsænɪtaɪzər/

发音拆解与要点:

  • San-:读作 [sæn],类似 sand 开头的发音(“散”)。
  • -i-:读作 [ɪ] 或轻微弱化的 [ə](轻读“尼”)。
  • -tiz-:读作 [taɪz],中间是双元音 [aɪ](“泰兹”)。
  • -er:读作 [ər](卷舌“儿”)。
  • 中文谐音近似散尼·泰泽(重音在最前面的 San)。

既然 Valgrind 的名字充满了北欧神话的古典浪漫色彩,那 Sanitizer 的命名风格就截然不同了,它走的是极其现代、硬核的“医学与工业”风。

  1. 字面意思 在英文中,Sanitizer 的本意是“消毒剂、净化器、杀菌剂”。 它源于动词 sanitize(消毒、净化)。在日常生活中,大家最常碰到的词大概就是 Hand Sanitizer(免洗洗手液)。它的核心作用就是消灭细菌和病毒,保持卫生。
  2. 程序员眼中的绝妙隐喻(代码“大消杀”) 在编程世界里,软件缺陷被叫做 Bug(虫子)。而内存越界、野指针、未定义行为、数据竞争这些底层问题,就像是隐藏在代码血液里的“隐形病毒”或“污染源”。

之前聊到 Valgrind 时我也提到了,Valgrind 虽然强大,但 10 倍到 50 倍的性能损耗和内存开销让它很难用于带有真实负载的线上压测。而 Google 团队开发的 Sanitizer 家族(通常通过编译选项 -fsanitize 开启),凭借其极低的性能损耗(通常只有 2 倍左右)和极高的报错精准度,已经是现代 C++ 开发和 CI/CD 流程中的标配。

  • Sanitizers 是由 Google 开源、目前深度集成在 GCC 和 Clang/LLVM 中的一组编译期插桩 + 运行时检测工具
  • 相比 Valgrind 的虚拟机动态模拟,Sanitizer 的原理是编译期直接向机器码中注入检查指令(Shadow Memory 机制),因此速度极快(通常只慢 1.5~3 倍),

核心家族主要包含这几位:

  1. ASan (AddressSanitizer)
    • 作用:抓内存越界(堆、栈、全局变量)、野指针(Use-After-Free)、双重释放(Double Free)。
    • 编译参数-fsanitize=address -g
  2. LSan (LeakSanitizer)
    • 作用:专注抓内存泄漏。ASan 在 x86_64 Linux 默认自带开启 LSan。
    • 编译参数-fsanitize=leak -g
  3. UBSan (UndefinedBehaviorSanitizer)
    • 作用:抓各种 C/C++ 未定义行为,如整数溢出、空指针解引用、除以零、移位越界、枚举值越界等。
    • 编译参数-fsanitize=undefined -g
  4. TSan (ThreadSanitizer)
    • 作用:多线程数据竞争(Data Race)和死锁检测。
    • 编译参数-fsanitize=thread -g注意:TSan 不能与 ASan 同时开启)。

01 实战用法

Sanitizer 的使用比 Valgrind 简单得多,它不需要额外的运行时环境,因为它是在编译期直接插桩的

在 GCC 或 Clang 中,通常只需要在编译和链接参数中加上这几个黄金选项:

g++ -g -O1 my_program.cpp -o my_program \
	-fsanitize=address -fno-omit-frame-pointer 

这里有几个极其关键的工程细节参数:

  • -g:必须加!保留调试符号,这样报错时才能看到具体是哪个文件的第几行,而不是一堆冷冰冰的十六进制内存地址。
  • -fno-omit-frame-pointer极其重要。它强制编译器在寄存器中保留帧指针(Frame Pointer)。这能让 Sanitizer 在报错时,极其快速且准确地展开函数调用栈(Stack Trace)。如果不加,调用栈可能会断裂或缺失。
  • -O1-O0:通常建议开启基本的 -O1 优化,这能让编译出的代码性能更好一点,且不会像 -O3 那样因为过度优化(如内联展开)导致报错行号错位。

编译完成后,直接运行 ./my_program 即可。一旦触发内存错误,程序会立刻 Crash 并向终端喷吐极为详细的错误报告。

02 底层原理与互斥坑点

  • 编译期插桩 (Compile-time Instrumentation): 与 Valgrind 在运行期翻译机器码不同,ASan 是在编译器生成汇编代码的阶段,直接在每一次内存读写指令(load/store)前后,插入了检查代码。
  • 影子内存 (Shadow Memory) 的极致优化: ASan 将虚拟内存映射为两部分:主内存和影子内存。它采用了一个极其高效的公式:Shadow = (Mem >> 3) + Offset。这意味着每次检查内存状态,CPU 只需要做一次移位和一次加法运算,速度快得惊人。
  • ⚠️ 致命坑点(面试高频): ASan 和 TSan 绝对不能同时开启! 也就是说,你不能编译一个既带 -fsanitize=address 又带 -fsanitize=thread 的程序。因为它们都在底层大规模地接管和篡改了内存分配器(如 malloc/free),同时也都占用了庞大的影子内存空间,强行一起开会导致链接报错或运行时直接崩溃。

总而言之,如果是日常开发和小规模测试,我会顺手用 Valgrind 跑一下(不用重新编译);但如果是长周期的线上环境压测,或者 CI/CD 流水线里的自动化测试,我一定会编译一个带有 -fsanitize=address 的版本进行高压运行。

03 使用示例

AddressSanitizer (ASan)

检测目标:内存越界(堆/栈/全局变量)、释放后使用(Use-After-Free)、双重释放(Double Free)等内存破坏问题。

代码示例 (越界访问)

// asan_test.cpp
int main() {
    int *array = new int[100];
    array[100] = 1; // 错误:数组越界(有效索引是 0-99)
    delete[] array;
    return 0;
}

编译与运行

# 加上 -fsanitize=address 和 -g(获取行号信息)
g++ -O0 -g -fsanitize=address 1.cpp -o app 
./app

报错特征

=================================================================
==387823==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7254e19e01d0 at pc 0x568679ded265 bp 0x7fff5d7ef7a0 sp 0x7fff5d7ef790
WRITE of size 4 at 0x7254e19e01d0 thread T0
    #0 0x568679ded264 in main /root/tmp/cmake_test/main.cpp:12
    #1 0x7514e282a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #2 0x7514e282a28a in __libc_start_main_impl ../csu/libc-start.c:360
    #3 0x568679ded144 in _start (/root/tmp/cmake_test/app+0x1144) (BuildId: 9e570591a8a295f904cfea2537a5f3ccef43d974)

0x7254e19e01d0 is located 0 bytes after 400-byte region [0x7254e19e0040,0x7254e19e01d0)
allocated by thread T0 here:
    #0 0x7514e312ca7f in operator new[](unsigned long) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:111
    #1 0x568679ded21e in main /root/tmp/cmake_test/main.cpp:11
    #2 0x7514e282a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #3 0x7514e282a28a in __libc_start_main_impl ../csu/libc-start.c:360
    #4 0x568679ded144 in _start (/root/tmp/cmake_test/app+0x1144) (BuildId: 9e570591a8a295f904cfea2537a5f3ccef43d974)

SUMMARY: AddressSanitizer: heap-buffer-overflow /root/tmp/cmake_test/main.cpp:12 in main
Shadow bytes around the buggy address:
  0x7254e19dff00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7254e19dff80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7254e19e0000: fa fa fa fa fa fa fa fa 00 00 00 00 00 00 00 00
  0x7254e19e0080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7254e19e0100: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x7254e19e0180: 00 00 00 00 00 00 00 00 00 00[fa]fa fa fa fa fa
  0x7254e19e0200: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x7254e19e0280: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x7254e19e0300: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x7254e19e0380: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x7254e19e0400: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==387823==ABORTING

可以看到报错 heap-buffer-overflow:

=================================================================
==387823==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7254e19e01d0 at pc 0x568679ded265 bp 0x7fff5d7ef7a0 sp 0x7fff5d7ef790
WRITE of size 4 at 0x7254e19e01d0 thread T0
    #0 0x568679ded264 in main /root/tmp/cmake_test/main.cpp:12
    #1 0x7514e282a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #2 0x7514e282a28a in __libc_start_main_impl ../csu/libc-start.c:360
    #3 0x568679ded144 in _start (/root/tmp/cmake_test/app+0x1144) (BuildId: 9e570591a8a295f904cfea2537a5f3ccef43d974)

ThreadSanitizer (TSan)

检测目标:多线程数据竞争(Data Races)、死锁等并发问题。

代码示例 (数据竞争)

// tsan_test.cpp
#include <iostream>
#include <thread>

int Global = 0;

void Thread1() {
    Global = 42; // 线程 1 写入
}

int main() {
    // 创建并启动线程,直接传入函数名
    std::thread t(Thread1);
    
    Global = 43; // 主线程同时写入,产生数据竞争!
    
    // 等待线程执行完毕
    t.join();
  
    return 0;
}

编译与运行

g++ -g -fsanitize=thread 1.cpp -o app -lpthread
./app

报错特征

==================
WARNING: ThreadSanitizer: data race (pid=387984)
  Write of size 4 at 0x55555555901c by main thread:
    #0 main /root/tmp/cmake_test/main.cpp:14 (app+0x138a) (BuildId: b9ce04e3912b122b9b1c15ff1aa578730f93e1ac)

  Previous write of size 4 at 0x55555555901c by thread T1:
    #0 Thread1() /root/tmp/cmake_test/main.cpp:7 (app+0x132b) (BuildId: b9ce04e3912b122b9b1c15ff1aa578730f93e1ac)
    #1 void std::__invoke_impl<void, void (*)()>(std::__invoke_other, void (*&&)()) /usr/include/c++/13/bits/invoke.h:61 (app+0x209e) (BuildId: b9ce04e3912b122b9b1c15ff1aa578730f93e1ac)
    #2 std::__invoke_result<void (*)()>::type std::__invoke<void (*)()>(void (*&&)()) /usr/include/c++/13/bits/invoke.h:96 (app+0x1ff3) (BuildId: b9ce04e3912b122b9b1c15ff1aa578730f93e1ac)
    #3 void std::thread::_Invoker<std::tuple<void (*)()> >::_M_invoke<0ul>(std::_Index_tuple<0ul>) /usr/include/c++/13/bits/std_thread.h:292 (app+0x1f48) (BuildId: b9ce04e3912b122b9b1c15ff1aa578730f93e1ac)
    #4 std::thread::_Invoker<std::tuple<void (*)()> >::operator()() /usr/include/c++/13/bits/std_thread.h:299 (app+0x1eea) (BuildId: b9ce04e3912b122b9b1c15ff1aa578730f93e1ac)
    #5 std::thread::_State_impl<std::thread::_Invoker<std::tuple<void (*)()> > >::_M_run() /usr/include/c++/13/bits/std_thread.h:244 (app+0x1e9c) (BuildId: b9ce04e3912b122b9b1c15ff1aa578730f93e1ac)
    #6 <null> <null> (libstdc++.so.6+0xf5c78) (BuildId: 5aa872344e015275864652857acd58813f07a15f)

  Location is global 'Global' of size 4 at 0x55555555901c (app+0x501c)

  Thread T1 (tid=387986, finished) created by main thread at:
    #0 pthread_create ../../../../src/libsanitizer/tsan/tsan_interceptors_posix.cpp:1078 (libtsan.so.2+0x5cc82) (BuildId: daf2df8924f8e8b74a3e13b95a859e8ebb3a94f9)
    #1 std::thread::_M_start_thread(std::unique_ptr<std::thread::_State, std::default_delete<std::thread::_State> >, void (*)()) <null> (libstdc++.so.6+0xf5d70) (BuildId: 5aa872344e015275864652857acd58813f07a15f)
    #2 main /root/tmp/cmake_test/main.cpp:12 (app+0x137b) (BuildId: b9ce04e3912b122b9b1c15ff1aa578730f93e1ac)

SUMMARY: ThreadSanitizer: data race /root/tmp/cmake_test/main.cpp:14 in main
==================
ThreadSanitizer: reported 1 warnings

MemorySanitizer (MSan)

检测目标:读取未初始化的内存。 注意:MSan 主要由 Clang 支持,并且要求所有链接的库也需要用 MSan 编译(否则会有误报)。

代码示例 (使用未初始化的值)

// msan_test.cpp
#include <iostream>

int main() {
    int x[10];
    if (x[5]) { // 错误:x[5] 未初始化就被用于条件判断
        std::cout << "x[5] is non-zero\n";
    }
    return 0;
}

编译与运行

clang++ -O0 -g -fsanitize=memory -fPIE -pie msan_test.cpp -o msan_test
./msan_test

报错特征

WARNING: MemorySanitizer: use-of-uninitialized-value ...

LeakSanitizer (LSan)

检测目标:内存泄漏。 注意:LSan 实际上已经默认集成在 ASan 中,但你也可以单独运行它来只查内存泄漏,不查越界。

代码示例 (内存泄漏)

// lsan_test.cpp
#include <stdlib.h>

void *p;

int main() {
    p = malloc(7); // 分配了内存但未释放
    p = nullptr;   // 丢失了唯一指向该内存的指针
    return 0;
}

编译与运行

g++ -g -fsanitize=leak lsan_test.cpp -o lsan_test
./lsan_test

报错特征

ERROR: LeakSanitizer: detected memory leaks ...` `Direct leak of 7 byte(s) in 1 object(s) allocated from:

UndefinedBehaviorSanitizer (UBSan)

检测目标未定义行为(如整数溢出、除以零、空指针解引用、对齐问题等)。

代码示例 (有符号整数溢出)

// ubsan_test.cpp
int main() {
    int k = 0x7fffffff; // 32位 signed int 的最大值
    k += 1;             // 错误:有符号整数溢出(未定义行为)
    return 0;
}

编译与运行

g++ -g -fsanitize=undefined ubsan_test.cpp -o ubsan_test
./ubsan_test

报错特征

ubsan_test.cpp:3:7: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'

最佳实践建议

通常在日常开发和 CI/CD 测试中,我们会组合使用它们。最经典的组合是开启 ASan 和 UBSan:

-g -O1 \
-fsanitize=address,undefined \
-fno-omit-frame-pointer

(注意:TSan 不能和 ASan/LSan/MSan 同时开启,因为它们对内存的拦截机制会发生冲突,必须分不同的测试构建来跑 TSan)。

posted @ 2026-09-08 21:12  光風霽月  阅读(5)  评论(0)    收藏  举报