什么是 “Safety(安全性)”?

在软件工程中,“安全性”(Safety)指系统在面对错误、意外输入、边界条件、并发冲突、内存错误等情况下仍能保持 正确行为、不崩溃、不产生未定义行为(UB)、不损坏数据、不会造成系统级事故
C++ 的核心挑战是:
语言本身允许大量未定义行为(Undefined Behavior, UB)。
因此,需要通过规则、工具和设计来保证 Safety。

C++ 各类型的 Safety(逐条详解)

下面把你列出的每一个 “Safety” 都详细解释。

1. 类型安全(Type Safety)

定义:
程序不会把某种类型的对象当成另一种类型使用,也不会误用未初始化的类型对象。
典型问题:

  • 错误的 reinterpret_cast
  • 写超出 struct 的真实成员
  • 使用无效的 union 成员
  • 违反 strict aliasing
    目标:
    任何值都与其声明的类型一致,不出现 “对象看起来是 A 实际是 B” 的情况。
    核心技术:
  • variant / std::optional / any
  • 禁止裸指针强制转换
  • RAII 和类型封装

2. 内存安全(Memory Safety)

定义:
保证程序不发生以下问题:

  • use-after-free
  • double free
  • dangling pointer
  • invalid pointer dereference
  • memory leak

公式描述:
内存安全要求:
∀ p ,  if  ∗ p  is accessed then  p ∈ valid allocated region \forall p, \text{ if } *p \text{ is accessed then } p \in \text{valid allocated region} p, if p is accessed then pvalid allocated region
C++ 由于手动管理内存 → 这是最困难的部分。

3. 生命周期安全(Lifetime Safety)

定义:
对象在其生命周期内被正确创建、使用和销毁,不会:

  • 在生命周期外访问(dangling)
  • 过早销毁资源
  • 使用被移动走的对象
    例子:
std::string_view sv;
{
    std::string s = "hello";
    sv = s; // dangling after the block
}

4. 边界安全(Bounds Safety)

定义:
不会越界访问数组、vector、string、span 等。
公式化:
对数组 A A A 的访问必须满足
0 ≤ i < ∣ A ∣ 0 \leq i < |A| 0i<A
措施:

  • 使用 std::span
  • 使用 at() 或带边界检查的工具
  • 避免原始指针算术

5. 初始化安全(Initialization Safety)

定义:
使用对象之前,它必须已经初始化,并且初始化完整、正确。
错误示例:

int x;  // 未初始化
int y = x + 1;  // UB

解决:

  • 构造函数
  • = default
  • std::optional
  • struct 默认清零

6. 对象访问安全(Object Access Safety)

定义:
访问对象时必须满足:

  • 对象正在有效生命周期内
  • 对象的状态符合访问要求
  • 不跨线程未同步访问
    示例:
  • 访问已销毁对象(dangling)
  • 未锁住情况下读写共享对象

7. 线程安全(Thread Safety)

定义:
确保多线程环境中不会出现:

  • data race
  • 使用未同步共享数据
  • 不安全的发布(unsafe publication)
  • 未正确同步的读写
    公式描述 (data race):
    若两个线程 T 1 T_1 T1 T 2 T_2 T2 同时访问同一对象,并且至少一个是写,那么必须存在同步关系 S S S
    ( T 1 ↔ T 2 ) ∧ ( write ∨ read-write ) ⇒ S (T_1 \leftrightarrow T_2) \land (\text{write} \lor \text{read-write}) \Rightarrow S (T1T2)(writeread-write)S

8. 算术安全(Arithmetic Safety)

定义:
避免算术运算中的:

  • 整数溢出(signed overflow → UB)
  • 除 0
  • 浮点数 NaN/Inf 传播
  • 移位操作超出宽度(UB)
    示例(UB):
int x = INT_MAX;
x = x + 1;  // signed overflow: UB

9. 定义安全(Definition Safety)

定义:
程序行为必须完全定义,不依赖 UB、未指定行为(unspecified behavior)、或实现定义的行为。
要求:

  • 不写依赖 UB 的程序
  • 不依赖编译器扩展行为
  • 不依赖未定义的内存布局

软件供应链安全(Software Supply Chain Safety)

这关心的是:软件的来源、构建过程、可验证性、可追踪性、可再生产性

1. 可重现性(Reproducibility)

定义:
同样的源代码、同样的依赖、同样的构建环境 → 始终得到 完全相同的二进制结果(bit-identical)
公式化描述:
构建函数 B B B
B ( source , deps , env ) = binary B(\text{source}, \text{deps}, \text{env}) = \text{binary} B(source,deps,env)=binary
要求:
B ( x ) = B ( x ′ )    ⟺    x = x ′ B(x) = B(x') \iff x = x' B(x)=B(x)x=x
解决方案:

  • hermetic builds
  • pinned version
  • Nix / Bazel / Reproducible Builds

2. 可追踪性(Traceability)

定义:
任何构建物都能追踪其来源:

  • 来源代码版本
  • 依赖版本
  • 构建者
  • 构建规则
    付表现为 SBOM(Software Bill of Materials)。

3. 发布交付时间(Release Delivery Time)

定义:
当供应链被严格验证后,发布速度不能被拖慢太多,而是保持快速、安全、自动化交付:
主要能力:

  • CI/CD pipeline 完全自动
  • artifact 签名(Sigstore)
  • 快速、安全、可审计的交付

如何通过 CppCoreGuidelines 实现 C++ 的安全性?

原文说:

YOU DON’T NEED TO Achieve safety in C++ today
Achieve safety in C++ today with the CppCoreGuidelines


意思是:
只用 CppCoreGuidelines + 工具(static analysis + compiler checks + annotations)即可在今天写出安全的 C++。
CppCoreGuidelines 包含:

  • Lifetime safety (Lifetimes): 通过 GSL::span, owner<T*>
  • Type safety:禁止 reinterpret_cast
  • Bounds safety:使用 span,禁止裸数组越界
  • Initialization safety:所有对象必须初始化
  • Concurrency rules:规定线程安全访问模式
  • Arithmetic rules:避免溢出、未定义移位
    配套工具:
  • clang-tidy + guideline checks
  • MSVC Annarchy checks
  • GSL library
  • -fsanitize=address,undefined,thread 等 runtime sanitizers

下面是面向实践、逐条且详尽的说明,聚焦主题:用 CMake + clang-tidy(启用 CppCoreGuidelines 检查)来达成 C++ 的 Memory Safety 与 Arithmetic Safety

  • 配置示例(CMake / .clang-tidy
  • 必要的静态/动态工具、编译器选项
  • 常见错误与正确写法示例(可直接拷贝使用)
  • 算术安全的形式化判定条件(用你要求的公式格式)
  • 推荐的 clang-tidy 检查清单与理由(可直接启用 cppcoreguidelines-*

重要:下面涉及到的 clang-tidy 检查、sanitizer 配置与建议均基于官方文档与行业实践。关键参考已在段落尾标注。(clang.llvm.org)

一、在 CMake 中启用 clang-tidy(并用 CppCoreGuidelines)

常见写法(把 clang-tidy 作为编译器检查器):

# 在顶层 CMakeLists.txt 中添加(适用于多 target)
find_program(CLANG_TIDY_EXE NAMES clang-tidy)
if (CLANG_TIDY_EXE)
  set(CMAKE_CXX_CLANG_TIDY
      "${CLANG_TIDY_EXE};-checks=-*,cppcoreguidelines-*")
endif()

说明:-checks=-*,cppcoreguidelines-* 意味着先禁用所有检查,再开启 CppCoreGuidelines 系列。你也可以把更多 check 列出来(用逗号分隔,或在 .clang-tidy 里按多行写)。文档说明了可用检查与命名。(clang.llvm.org)
建议把更细粒度配置放在仓库根目录的 .clang-tidy 文件里(便于 CI 一致性):

Checks: >
  -*,
  cppcoreguidelines-*,
  bugprone-*,
  performance-*,
  modernize-*
WarningsAsErrors: ''
HeaderFilterRegex: 'src/|include/'
AnalyzeTemporaryDtors: false

.clang-tidy 的写法与多行列举请参考 clang-tidy 文档与社区范例。)(Stack Overflow)

二、Memory Safety(内存安全):原则、工具及实践

核心原则(一句话)

永远不要用裸的“手动资源管理 + 原始指针算术”来承担生命周期与边界检查的全部责任;用 RAII、智能指针、span、GSL 与静态检查把责任交给类型与工具。 (clang.llvm.org)

静态检查(clang-tidy 与 CppCoreGuidelines)

推荐启用的相关检查(示例):

  • cppcoreguidelines-owning-memory:检测谁拥有内存、禁止裸 new/malloc 未包装。(clang.llvm.org)
  • cppcoreguidelines-pro-bounds-array-to-pointer-decay:数组到指针衰变的检测,建议使用 std::span 替代裸数组访问。(bcain-llvm.readthedocs.io)
  • 其它相关:cppcoreguidelines-pro-type-reinterpret-castcppcoreguidelines-pro-bounds-.* 等。
    这些检查能尽早提示“潜在的越界 / 所有权 / 裸指针”问题。

动态检测(运行时 Sanitizers)

  • AddressSanitizer(ASan):检测越界、use-after-free、double-free 等。
  • UndefinedBehaviorSanitizer(UBSan)与 -fsanitize=integer:检测整数溢出 / 除 0 / shift 越界等。
  • ThreadSanitizer(TSan):检测 data races。
    例如在 CMake 中为 Debug 添加 sanitizer:
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
  add_compile_options(-g -O1 -fno-omit-frame-pointer -fsanitize=address,undefined,integer)
  add_link_options(-fsanitize=address,undefined,integer)
endif()

注意:Sanitizer 在 release 或性能敏感场景中会带来运行时开销;在 CI/Test 中启用非常值得。有关 UBSan -fsanitize=integer 的详细行为见文档(它包含 signed-integer-overflow, integer-divide-by-zero, shift 等子项)。(clang.llvm.org)

代码实践示例

错误(易出 use-after-free /裸指针):

int* p = new int(42);
delete p;
// later...
int v = *p; // use-after-free -> UB

改正(RAII / 智能指针):

#include <memory>
auto p = std::make_unique<int>(42);
// 使用 p,不需要手动 delete;所有权清晰

避免数组到裸指针衰变(边界安全):
错误:

void f(int* data, size_t n) { /* 依赖调用者保证 n 正确 */ }

更好(std::span):

#include <span>
void f(std::span<int> data) {
  for (auto &x : data) { /* 安全遍历 */ }
}

std::span 显式声明了长度,配合 clang-tidy 的 pro-bounds 检查能显著降低越界概率。(bcain-llvm.readthedocs.io)

发布 / CI 建议(Memory Safety)

  • 在 CI 上强制运行 clang-tidy(CppCoreGuidelines)并使关键规则失败构建。
  • 在单元/集成测试上启用 ASan + UBSan(并记录/收集崩溃日志)。
  • 对第三方库使用 SBOM 与固定版本(软件供应链安全相关),并在可行时做 fuzz 测试(内存相关 bug 很可能由边界/格式错误触发)。(clang.llvm.org)

三、Arithmetic Safety(算术安全):形式化、工具与实践

问题本质(一句话)

C/C++ 标准把 有符号整数溢出定义为 UB(这让编译器能做激进优化,但也会导致安全漏洞)。因此必须主动检测/避免这种 UB。关于整数安全的研究/提案文献长期存在且仍在演进。(open-std.org)

算术安全的形式化判定(用公式)

  • 无符号整型(unsigned):加法在类型宽度内按模 2 w 2^w 2w 环绕。可以通过检查溢出条件来检测:若用 a , b a,b a,b 为无符号,则溢出当且仅当
    ( a + b )   m o d   2 w ≠ a + b   ( 以数学自然数解释 ) (a + b) \bmod 2^w \ne a + b \ (\text{以数学自然数解释}) (a+b)mod2w=a+b (以数学自然数解释)
    但更实用的判别是:
    unsigned overflow    ⟺    a + b < a . \text{unsigned overflow} \iff a + b < a. unsigned overflowa+b<a.
  • 有符号整型(signed):标准视为 UB。判别 a + b a + b a+b 是否会导致溢出(以 I N T m i n , I N T m a x INT_{min}, INT_{max} INTmin,INTmax 为上下界):
    { 若  b > 0 : a > I N T m a x − b ⇒ overflow 若  b < 0 : a < I N T m i n − b ⇒ overflow \begin{cases} \text{若 } b > 0: & a > INT_{max} - b \Rightarrow \text{overflow} \ \text{若 } b < 0: & a < INT_{min} - b \Rightarrow \text{overflow} \end{cases} { b>0:a>INTmaxboverflow  b<0:a<INTminboverflow
    另一种等价的位论断(常用在实现中):若 a a a b b b 同号,但结果与它们不同号,则发生溢出:
    overflow    ⟺    sign ⁡ ( a ) = sign ⁡ ( b )   ∧   sign ⁡ ( a + b ) ≠ sign ⁡ ( a ) . \text{overflow} \iff \operatorname{sign}(a) = \operatorname{sign}(b) \ \wedge\ \operatorname{sign}(a+b) \ne \operatorname{sign}(a). overflowsign(a)=sign(b)  sign(a+b)=sign(a).
    把这些检查写成函数或使用编译器内建可安全处理。上面公式在实际代码中直接采用内建或库函数更稳妥。(open-std.org)

静态与动态检测手段

  • 静态:clang-tidy / static analyzers 有一些规则可以发现明显的溢出或危险算术,但静态分析不总能捕获运行时依赖的溢出。
  • 动态
    • UBSan:-fsanitize=integer / -fsanitize=signed-integer-overflow 能在运行时捕获已触发的溢出。(clang.llvm.org)
    • -ftrapv(GCC):对有符号溢出产生 trap(但性能很差且并非所有平台都支持);-fwrapv 则告诉编译器把有符号溢出当作按二补码 wrap(会改变优化前提,因此一般不推荐为安全补丁)。(Stack Overflow)

编程实践(避免 UB)

  1. 使用带检测的内建函数(compiler intrinsics)
    • GCC/Clang 提供 __builtin_add_overflow, __builtin_mul_overflow 等:
int a, b;
int result;
if (__builtin_add_overflow(a, b, &result)) {
  // 处理溢出
} else {
  // 安全地使用 result
}

这类函数既高效又明确避免 UB。
2. 使用宽类型或大整数库:在可能溢出的地方先提升到更高位宽(例如 int64_t 做 32-bit 的累加),或使用 boost::multiprecision
3. 显式检查范围(利用上面公式):

#include <limits>
bool add_will_overflow(int a, int b) {
  if (b > 0) return a > std::numeric_limits<int>::max() - b;
  if (b < 0) return a < std::numeric_limits<int>::min() - b;
  return false;
}
  1. 使用库:许多项目采用“Checked Integer”库(例如 Chromium 的 base::checked_cast / CheckedNumeric,或微软 / Boost 的类似工具),用来在算术链路中传播是否发生了溢出。
  2. 不靠编译器以外的假设:不要因为在某个编译器/架构上结果“看起来正确”就假设这在所有实现上都安全;UB 是可移植性的敌人。(open-std.org)

示例:错误 vs 正确

错误(依赖有符号溢出):

int x = INT_MAX;
int y = x + 1; // UB: signed overflow

正确(使用内建检测):

int x = INT_MAX;
int y;
if (__builtin_add_overflow(x, 1, &y)) {
  // 处理溢出(错误路径)
} else {
  // 使用 y
}

或者用 UBSan 在测试时捕获(-fsanitize=undefined-fsanitize=integer)。

四、把两者(Memory + Arithmetic)整合到 CMake/CI 的最佳实践(清单)

  1. .clang-tidy 中开启 cppcoreguidelines-*(并对 owning-memorypro-bounds-*pro-type-reinterpret-cast 保持严格)。(clang.llvm.org)
  2. 在 CI(Debug)matrix 中加入一项:ASan + UBSan(-fsanitize=address,undefined,integer),另一项:TSan(若并发相关)。(clang.llvm.org)
  3. 对关键路径和库采用更严格的审查(例如强制 __builtin_*_overflow 或 CheckedInt)。
  4. 在 PR 流程中把 clang-tidy 输出作为必须修复的错误(或放入 review checklist)。
  5. 对第三方二进制或源码开启 reproducible / pinned-deps 策略(这部分涉及软件供应链安全,与构建再现性/可追溯性相关)。

五、小结(要点回顾)

  • clang-tidy(启用 cppcoreguidelines-*)能显著降低类型/所有权/边界相关的内存安全问题;把规则置于 .clang-tidy 并在 CI 中强制。(clang.llvm.org)
  • 运行时 Sanitizers(ASan、UBSan 的 integer 项、TSan)是捕获难以静态发现的实际 bug 的重要手段。(clang.llvm.org)
  • 算术安全需要明确处理有符号溢出(因为它是 UB);使用 __builtin_*_overflow、CheckedInt 库或宽类型 + 范围检查是实际有效的做法。关于整数安全的社区研究/提案仍在推进,生产代码中不要依赖有符号溢出语义。(open-std.org)
    如果你愿意,我可以 立刻为你做三件可执行的事情(任选其一或多个),并在当前回复里把结果给出:
  1. 把上面的 CMake + .clang-tidy + sanitizer 建议,生成一个完整可拷贝的 CMakeLists.txt(含 Debug/Release 区分)与 .clang-tidy 文件。
  2. 为 Memory Safety 与 Arithmetic Safety 各生成 6–8 个“常见坑对照示例”(错误代码 + 修复),便于团队培训或入职文档。
  3. 生成一份可用于 CI 的检查脚本(Bash),在 GitHub Actions / GitLab CI 中启用 clang-tidy、ASan+UBSan 测试,并在发现 sanitizer 抛出错误时上传日志。
    选一个序号,我会马上生成对应内容(直接输出可复制的文件内容)。

一、常见 LLVM Sanitizers 速览(理解)

  • AddressSanitizer(ASan)
    检测堆/栈/全局内存越界、use-after-free、double-free、部分内存泄露(配合 LSan)。适合检测大多数内存错误。使用编译器选项:-fsanitize=address
  • UndefinedBehaviorSanitizer(UBSan)
    检测未定义行为(signed overflow、除零、非法移位、类型擦写等)。启用示例:-fsanitize=undefined 或更细粒度 -fsanitize=signed-integer-overflow,integer-divide-by-zero,...
  • ThreadSanitizer(TSan)
    动态检测 data race(竞态)。启用:-fsanitize=thread。TSan 会在运行时插桩大量同步/内存访问,开销大,但能发现并发错误。
  • LeakSanitizer(LSan)
    内存泄露检测。LSan 常常与 ASan 一起工作(ASan runtime 包含 LSan 功能),也可以独立使用:-fsanitize=leak
  • MemorySanitizer(MSan)
    检测未初始化内存读取(使用未初始化值)。启用:-fsanitize=memory。注意:MSan 要求对所有被测试的库(尤其 libc)进行符号级别的 Instrumentation(或使用特殊的 MSan 构建的运行时),因此使用复杂且与其他 sanitizer 不兼容(见下文)。

二、兼容性规则(要点与直观集合论表述)

许多初学者误以为可以把所有 -fsanitize= 放在同一次构建里;实际上有“可组合”的组合与“不可组合”的组合。
定义每个构建使用的一组 sanitizer 标志为 S S S(例如 S = address , undefined S={\text{address},\text{undefined}} S=address,undefined)。要合法地把多个 sanitizer 放进同一构建,所有对应 runtime / link-time 要求必须能共存。实际常见兼容性结论:

  • ASan 与 UBSan:通常 可共存(例如 -fsanitize=address,undefined 常见且可用)。
  • ASan 与 LSan可共存(LSan 常由 ASan runtime 提供或可单独启用)。
  • ASan 与 TSan不可共存threadaddress 运行时实现冲突)。
  • MSan 与 ASan/TSan不可共存(MSan 要求对运行时全面 instrument,和 ASan/TSan 的 runtime 不兼容)。
  • UBSan 与 其它多数 sanitizer:通常 可共存(UBSan 以检测为主,不像 MSan 那样需要特殊 runtime),但个别 UBSan 子检查在组合时可能带来噪声或不一致行为。
    可以用集合关系直观表达“不允许同时出现”的关系。例如若定义不兼容对集合 I I I,则某构建 S S S 合法需满足:
    ∄   ( a , b ) ∈ I  使得  a , b ⊆ S . \nexists\ (a,b)\in I\ \text{使得}\ {a,b}\subseteq S.  (a,b)I 使得 a,bS.
    现实中常用的不兼容对(示例):
    I ⊇ ( address , thread ) , ( memory , address ) , ( memory , thread ) . I \supseteq {(\text{address},\text{thread}), (\text{memory},\text{address}), (\text{memory},\text{thread})}. I(address,thread),(memory,address),(memory,thread).
    因此最佳实践是把 sanitizer 构建成多个互斥的构建矩阵(build matrix / CI jobs),而不是试图把所有 sanitizer 混在一次构建里。

三、为什么需要多次构建 / 多个 toolchain 文件?

原因总结:

  1. 运行时/链接冲突:某些 sanitizer 需要不同的 runtime 实现或特殊的库(如 MSan)。这些实现互相冲突,不能链接在同一二进制中。
  2. 编译/链接选项不同:不同 sanitizer 可能需要不同的编译和链接选项(例如 MSan 需要 instrumented libc)。
  3. 调试与性能考虑:每次构建的运行时开销很大,测试应独立运行以节省调试复杂度与资源。
  4. CI 可管理性:分成多个 job(ASan job、TSan job、MSan job)更利于定位问题及稳定性。
    因此在 CMake + CI 中常见做法:为每类 sanitizer 提供专门的 toolchain 或通过 CMake preset/Cache variable 控制,分别生成并运行不同的构建目录(build-asan/build-tsan/build-msan/ 等)。

四、在 CMake 中使用 LLVM Sanitizers(范例与 toolchain 文件)

提到的接口:

find_package(Sanitizers MODULE REQUIRED)
add_executable(use-after-free test/use-after-free.cpp)
# enable sanitizers
add_sanitizers(use-after-free)

这是某些 LLVM 提供的或第三方 CMake 模块的用法(如果你有该模块,它会在内部把 -fsanitize=... 等选到 target)。下面给出 无依赖纯 CMake / 推荐做法 的示例,以及如何使用独立 toolchain 文件的具体内容。

方式 A:通过独立 toolchain / cache variables(推荐用于 CI)

1) sanitize-address.cmake

# sanitize-address.cmake - 用于 ASan (+UBSan 可选)
set(SANITIZE_ADDRESS TRUE CACHE BOOL "" FORCE)
set(SANITIZE_UNDEFINED TRUE CACHE BOOL "" FORCE) # optional: add UBSan with ASan
# 你也可以在这里指定编译器(若有交叉编译需求)

2) sanitize-thread.cmake

# sanitize-thread.cmake - 用于 TSan
set(SANITIZE_THREAD TRUE CACHE BOOL "" FORCE)
# 禁止与 address/memory 同时使用

3) sanitize-memory.cmake

# sanitize-memory.cmake - 用于 MSan(需要 instrumented runtimes)
set(SANITIZE_MEMORY TRUE CACHE BOOL "" FORCE)
# 注意:MSan 需要构建并使用 instrumented libc / libstdc++(平台相关)

4) CMakeLists.txt(顶层) — 读取这些变量并设置 target 编译选项

cmake_minimum_required(VERSION 3.21)
project(safepp CXX)
# ... add targets, sources ...
add_executable(use-after-free test/use-after-free.cpp)
# Helper to apply sanitizers based on cache variables
function(enable_sanitizers target)
  if (SANITIZE_ADDRESS)
    target_compile_options(${target} PRIVATE -fsanitize=address)
    target_link_options(${target} PRIVATE -fsanitize=address)
  endif()
  if (SANITIZE_THREAD)
    target_compile_options(${target} PRIVATE -fsanitize=thread)
    target_link_options(${target} PRIVATE -fsanitize=thread)
  endif()
  if (SANITIZE_UNDEFINED)
    # 注意:可选地把 undefined 和其他 sanitizer 一起用
    target_compile_options(${target} PRIVATE -fsanitize=undefined)
    target_link_options(${target} PRIVATE -fsanitize=undefined)
  endif()
  if (SANITIZE_MEMORY)
    target_compile_options(${target} PRIVATE -fsanitize=memory)
    target_link_options(${target} PRIVATE -fsanitize=memory)
    # MSan 还通常需要特殊的 runtime /禁用部分 optimizations
  endif()
  if (SANITIZE_LEAK)
    target_compile_options(${target} PRIVATE -fsanitize=leak)
    target_link_options(${target} PRIVATE -fsanitize=leak)
  endif()
endfunction()
enable_sanitizers(use-after-free)

5) 构建 & 运行(示例)

# ASan + UBSan 构建
cmake -S . -B build-asan -DCMAKE_TOOLCHAIN_FILE=sanitize-address.cmake -DCMAKE_BUILD_TYPE=Debug
cmake --build build-asan -j
cd build-asan && ctest --output-on-failure
# TSan 构建(独立)
cmake -S . -B build-tsan -DCMAKE_TOOLCHAIN_FILE=sanitize-thread.cmake -DCMAKE_BUILD_TYPE=Debug
cmake --build build-tsan -j
cd build-tsan && ctest -j1  # TSan 运行时建议单线程测试或控制并发量以便可复现

说明:你也可以使用 CMAKE_C_FLAGS / CMAKE_CXX_FLAGS 来统一设置,但以 target_compile_options 更现代、更安全(目标粒度控制)。

五、使用 find_package(Sanitizers MODULE REQUIRED) / add_sanitizers 的注意点

如果你使用的是某个现成的 Sanitizers CMake 模块(社区或 LLVM 提供的变体),它通常提供 add_sanitizers(<target> [ASAN|TSAN|MSAN|UBSAN|LSAN]) 这样的 helper。这很好用但仍需注意:

  • 模块行为各不相同:在不同项目里实现可能有差异(例如是否自动加上 -fno-omit-frame-pointer、是否处理 link order、是否处理 MSan 的特殊依赖)。阅读模块实现非常重要。
  • 不要在单个 target 上混用不兼容 sanitizer(模块可能没有防护)。例如不要 add_sanitizers(myexe ASAN TSAN)
  • 模块可能提供检测不兼容并报错的功能,但也可能不会——因此最好在你的 CMake 层面做明确分离(用上面 toolchain / cache 方式)。

六、常用环境变量(运行时控制 sanitizer 行为)

在运行时你可以通过环境变量调整 sanitizer 的行为(便于 CI 策略):

  • ASAN_OPTIONS(ASan)
    • 例:ASAN_OPTIONS=detect_leaks=1:abort_on_error=1:alloc_dealloc_mismatch=1
    • 若要在 ASan 中关闭泄漏检测:ASAN_OPTIONS=detect_leaks=0(有时在与 LeakSanitizer 单独配合时有用)
  • LSAN_OPTIONS(LSan)
    • 例:LSAN_OPTIONS=verbosity=2:log_threads=1
  • UBSAN_OPTIONS
    • 例:UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1
  • TSAN_OPTIONS
    • 例:TSAN_OPTIONS=report_thread_leaks=1:log_path=tsan-%p.log
  • MSAN_OPTIONS(MSan)
    • MSan 也有一些选项,且 MSan 需要运行时链接到 instrumented runtime。
      在 CI 中把这些变量设置为 job 环境变量(或 ctest 自定义环境)能得到更可读的报错与可回放的运行环境。

七、CI 配置建议(示例矩阵)

在 CI(例如 GitHub Actions)中创建如下 job 矩阵(每项独立构建 & 测试):

  • build-and-test: asan-ubsan
    cmake -B build-asan -DCMAKE_TOOLCHAIN_FILE=sanitize-address.cmake && cmake --build ... && ctest
  • build-and-test: tsan
    cmake -B build-tsan -DCMAKE_TOOLCHAIN_FILE=sanitize-thread.cmake ...
  • build-and-test: msan (如果需要并能准备 instrumented runtime)
  • optional: asan-ubsan-release(更接近真实 workload 的测试)
    每个 job 都只启用该 job 对应的 sanitizer 以避免不兼容问题。

八、实战注意事项与陷阱提醒(要点清单)

  1. 别把 ASan 和 TSan、MSan 混到一个构建里。 若需要同时运行多类检查,请分开构建与运行。
  2. MSan 的使用门槛最高:需要 instrumented C/C++ runtime(libc、libstdc++),否则会产生大量误报或漏报。生产环境若不熟悉,慎用。
  3. TSan 测试通常需要控制并发与稳定的环境(建议在 CI 中固定 CPU 核心数、禁用 hyperthreading 或在容器里做资源限制)。
  4. ASan/UBSan 可以合并(通常非常有用)。经常把 -fsanitize=address,undefined 作为默认 debug sanitizer。
  5. 为 sanitizer 构建使用 Debug 配置-g -O1 常见),因为过度优化可能影响可诊断性。
  6. 收集 sanitizer 输出并存档(特别是在 CI 中),便于事后分析与回放。
  7. 对第三方库/系统库的限制:某些第三方二进制无法被 instrument(导致 MSan 无法信赖);ASan 对大多数库可用,但 MSan 对 runtime 要求更高。
  8. 测试并发时请用 ctest -j1 或控制并行度来提高可复现性(尤其是 TSan)。

九、示例:把多个 sanitizer 抽象成脚本(快速上手)

#!/usr/bin/env bash
set -euo pipefail
case "$1" in
  asan) tc=sanitize-address.cmake ;;
  tsan) tc=sanitize-thread.cmake ;;
  msan) tc=sanitize-memory.cmake ;;
  *) echo "Usage: $0 {asan|tsan|msan}"; exit 1 ;;
esac
builddir=build-$1
cmake -S . -B $builddir -DCMAKE_TOOLCHAIN_FILE=$tc -DCMAKE_BUILD_TYPE=Debug
cmake --build $builddir -j
(cd $builddir && ctest --output-on-failure)

十、小结(一句话提醒)

  • 把 sanitizer 当作 互斥的检测模式 来对待:ASan + UBSan 是常见组合;TSanMSan 需要各自独立的构建与专门配置。
  • 在 CMake/CI 中采用 独立 toolchain / 独立构建目录 的策略可以让你在同一仓库里并行安全地运行多种 sanitizer 检查。
  • 记录并统一 ASAN_OPTIONS / UBSAN_OPTIONS / TSAN_OPTIONS 等运行时参数以获得可复现、易分析的报告。
    如果你希望,我现在可以:
  1. 生成 四个完整的 toolchain 文件sanitize-address.cmakesanitize-thread.cmakesanitize-memory.cmakesanitize-ub.cmake),并给出一个 完整的顶层 CMakeLists.txt 示例,直接可用。
  2. 或者给出一个 GitHub Actions CI 配置示例(matrix job: asan, tsan, msan),包含如何保存日志与上传 artifacts。
    选 1 或 2(或都要),我会 立即把文件内容和使用说明贴在这里。

LLVM Sanitizers 示例讲解(逐条带例子)

xiaqiu@xz:~/test/CppCon/day459/code$ tree
.
├── CMakeLists.txt
├── cmake
│ ├── FindASan.cmake
│ ├── FindMSan.cmake
│ ├── FindSanitizers.cmake
│ ├── FindTSan.cmake
│ ├── FindUBSan.cmake
│ ├── asan-wrapper
│ └── sanitize-helpers.cmake
└── use-after-free.cpp
2 directories, 9 files
xiaqiu@xz:~/test/CppCon/day459/code$
asan-wrapper

# 检查当前运行的操作系统是否为 Linux。
# 'uname' 命令用于打印系统信息,'-s' 选项只打印内核名称。
if [ "$(uname)" != "Linux" ]
then
    # 如果平台不是 Linux,则立即退出当前脚本,并使用 'exec $@'
    # 直接执行传递给脚本的原始命令($@代表所有参数)。
    # 这确保了脚本的行为不会干扰非 Linux 平台上的应用执行。
    exec $@
fi
# -------------------------------------------------------------------
# 获取应用程序(脚本的第一个参数 $1,即要执行的程序)所使用的 libasan 库路径。
# 如果找到了 libasan,它将被添加到 LD_PRELOAD 环境变量中。
# 使用 'ldd $1' 查找程序 $1 的动态链接库依赖。
# 'grep libasan' 过滤出包含 'libasan' 的行(AddressSanitizer库)。
# 'sed "s/^[[:space:]]//"' 移除行首的空格。
# 'cut -d' ' -f1' 以空格为分隔符,截取第一个字段,即库文件的完整路径。
libasan=$(ldd $1 | grep libasan | sed "s/^[[:space:]]//" | cut -d' ' -f1)
# 检查变量 libasan 是否非空(即是否找到了 libasan 库)。
if [ -n "$libasan" ]
then
    # 如果找到了 libasan 库:
    # 检查 LD_PRELOAD 环境变量是否已经设置了值。
    if [ -n "$LD_PRELOAD" ]
    then
        # 如果 LD_PRELOAD 已经有值,则将找到的 libasan 路径添加到其前面,
        # 用冒号 ':' 分隔。这确保 libasan 优先于列表中其他库被加载。
        export LD_PRELOAD="$libasan:$LD_PRELOAD"
    else
        # 如果 LD_PRELOAD 为空,则直接将 libasan 路径赋值给它。
        export LD_PRELOAD="$libasan"
    fi
fi
# -------------------------------------------------------------------
# 执行应用程序。
# 'exec $@' 用传递给脚本的原始命令和参数替换当前的 shell 进程。
# 这样,应用程序将作为主进程运行,并继承修改后的 LD_PRELOAD 环境变量。
exec $@

FindASan.cmake

# -------------------------------------------------------------------
##  配置选项
# -------------------------------------------------------------------
# 定义一个名为 SANITIZE_ADDRESS 的 CMake 选项。
# 用户可以通过这个选项来控制是否为构建目标启用 AddressSanitizer。
# 默认值为 Off(不启用)。
# 提示信息:"Enable AddressSanitizer for sanitized targets."
option(SANITIZE_ADDRESS "Enable AddressSanitizer for sanitized targets." Off)
# -------------------------------------------------------------------
##  编译器标志候选
# -------------------------------------------------------------------
# 定义一个包含所有已知的、用于启用 AddressSanitizer 的编译器标志的列表。
set(FLAG_CANDIDATES
    # MSVC (Microsoft Visual C++) 编译器使用的标志。
    "/fsanitize=address"
    # Clang 3.2+ 和较新版本的 GCC 使用的标志。
    # "-g" 启用调试信息。
    # "-fsanitize=address" 启用 AddressSanitizer。
    # "-fno-omit-frame-pointer" 是可选的,它禁止编译器省略栈帧指针,
    # 这有助于 ASan 更精确地报告堆栈信息。
    "-g -fsanitize=address -fno-omit-frame-pointer"
    "-g -fsanitize=address"
    # 较旧的、已弃用的 ASan 标志。
    "-g -faddress-sanitizer"
)
# -------------------------------------------------------------------
##  兼容性检查
# -------------------------------------------------------------------
# 检查 AddressSanitizer (ASan) 是否与 ThreadSanitizer (TSan) 或 MemorySanitizer (MSan) 同时启用。
# 这三种 Sanitizer 通常是互斥的,不能同时应用于同一个目标。
if (SANITIZE_ADDRESS AND (SANITIZE_THREAD OR SANITIZE_MEMORY))
    # 如果检测到冲突,则抛出致命错误并停止配置。
    message(FATAL_ERROR "AddressSanitizer is not compatible with "
        "ThreadSanitizer or MemorySanitizer.")
endif ()
# -------------------------------------------------------------------
## 辅助工具和检查
# -------------------------------------------------------------------
# 引入一个外部的 CMake 脚本文件 'sanitize-helpers.cmake'。
# 这个脚本通常包含像 'sanitizer_check_compiler_flags' 和 'sanitizer_add_flags'
# 这样的辅助函数/宏。
include(sanitize-helpers)
# 如果用户通过选项启用了 AddressSanitizer:
if (SANITIZE_ADDRESS)
    # 检查编译器对候选标志的支持。
    # 'sanitizer_check_compiler_flags' (来自 sanitize-helpers) 会尝试编译一个
    # 小程序来确定当前编译器实际支持哪个 ASan 标志,并将选定的标志存储在内部变量中。
    # 参数: 标志列表, Sanitizer名称 ("AddressSanitizer"), 简称 ("ASan")。
    sanitizer_check_compiler_flags("${FLAG_CANDIDATES}" "AddressSanitizer"
        "ASan")
    # 查找 "asan-wrapper" 程序。
    # asan-wrapper 通常是一个用于正确加载 libasan 的 Shell 脚本或工具(就像您上一个问题中的脚本)。
    # 'CMAKE_MODULE_PATH' 用于指定额外的查找路径。
    find_program(ASan_WRAPPER "asan-wrapper" PATHS ${CMAKE_MODULE_PATH})
    # 将找到的变量标记为高级选项,使其在普通的 CMake GUI 中默认隐藏。
    mark_as_advanced(ASan_WRAPPER)
endif ()
# -------------------------------------------------------------------
##  应用函数
# -------------------------------------------------------------------
# 定义一个名为 'add_sanitize_address' 的 CMake 函数。
# 它的作用是专门为指定的构建目标 (TARGET) 添加 ASan 编译标志。
function (add_sanitize_address TARGET)
    # 如果 SANITIZE_ADDRESS 选项未启用,则函数立即返回,不执行任何操作。
    if (NOT SANITIZE_ADDRESS)
        return()
    endif ()
    # 调用辅助函数 'sanitizer_add_flags' (来自 sanitize-helpers)。
    # 这个函数将前面确定好的、有效的 ASan 标志应用到指定的 TARGET 上。
    # 这通常通过 target_compile_options 和 target_link_options 来实现。
    sanitizer_add_flags(${TARGET} "AddressSanitizer" "ASan")
endfunction ()

下面给出的示例都会体现:

  • 错误代码示例(有问题的)
  • CMake 示例
  • 如何用单独的 toolchain 文件分别运行 ASan / UBSan / TSan / MSan

1⃣ AddressSanitizer(ASan)示例:Use-after-free

问题代码(use-after-free.cpp)

#include <iostream>
int* makeDangling() {
    int* p = new int(42);
    delete p;
    return p;  // 返回了一个悬空指针
}
int main() {
    int* d = makeDangling();
    std::cout << *d << "\n";  // use-after-free
}

FindMSan.cmake

# -------------------------------------------------------------------
##  配置选项
# -------------------------------------------------------------------
# 定义一个名为 SANITIZE_MEMORY 的 CMake 选项。
# 用户可以通过此选项控制是否为构建目标启用 MemorySanitizer。
# 默认值为 Off(不启用)。
# 提示信息:"Enable MemorySanitizer for sanitized targets."
option(SANITIZE_MEMORY "Enable MemorySanitizer for sanitized targets." Off)
# -------------------------------------------------------------------
##  编译器标志候选
# -------------------------------------------------------------------
# 定义一个包含所有已知的、用于启用 MemorySanitizer 的编译器标志的列表。
set(FLAG_CANDIDATES
    # MSVC (Microsoft Visual C++) 编译器使用的标志。
    "/fsanitize=memory"
    # GNU (GCC) 或 Clang 编译器使用的标志。
    # "-g" 启用调试信息。
    # "-fsanitize=memory" 启用 MemorySanitizer。
    "-g -fsanitize=memory"
)
# -------------------------------------------------------------------
## 辅助工具
# -------------------------------------------------------------------
# 引入一个外部的 CMake 脚本文件 'sanitize-helpers.cmake'。
# 这个文件通常包含像 'sanitizer_check_compiler_flags' 和 'sanitizer_add_flags'
# 这样的辅助函数/宏。
include(sanitize-helpers)
# -------------------------------------------------------------------
##  启用和平台/架构限制检查
# -------------------------------------------------------------------
# 仅在用户启用了 SANITIZE_MEMORY 选项时执行后续检查。
if (SANITIZE_MEMORY)
    # 检查系统名称是否不等于 "Linux"。
    # MemorySanitizer (MSan) 主要设计并支持 Linux 系统。
    if (NOT ${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
        # 如果不是 Linux 系统,发出警告。
        message(WARNING "MemorySanitizer disabled for target ${TARGET} because "
            "MemorySanitizer is supported for Linux systems only.")
        # 强制将 SANITIZE_MEMORY 选项设置为 Off,并将其缓存,防止用户再次误用。
        set(SANITIZE_MEMORY Off CACHE BOOL
            "Enable MemorySanitizer for sanitized targets." FORCE)
    # 否则,检查指针大小是否不等于 8 字节(即是否不是 64 位系统)。
    # MSan 通常需要 64 位架构才能正常工作。
    elseif (NOT ${CMAKE_SIZEOF_VOID_P} EQUAL 8)
        # 如果不是 64 位系统,发出警告。
        message(WARNING "MemorySanitizer disabled for target ${TARGET} because "
            "MemorySanitizer is supported for 64bit systems only.")
        # 强制将 SANITIZE_MEMORY 选项设置为 Off 并缓存。
        set(SANITIZE_MEMORY Off CACHE BOOL
            "Enable MemorySanitizer for sanitized targets." FORCE)
    # 如果通过了所有检查(是 Linux 且是 64 位系统):
    else ()
        # 检查编译器对候选标志的支持。
        # 'sanitizer_check_compiler_flags' 会确定当前编译器实际支持哪个 MSan 标志,
        # 并将选定的标志存储在内部变量中。
        sanitizer_check_compiler_flags("${FLAG_CANDIDATES}" "MemorySanitizer"
            "MSan")
    endif ()
endif ()
# -------------------------------------------------------------------
##  应用函数
# -------------------------------------------------------------------
# 定义一个名为 'add_sanitize_memory' 的 CMake 函数。
# 它的作用是专门为指定的构建目标 (TARGET) 添加 MSan 编译标志。
function (add_sanitize_memory TARGET)
    # 如果 SANITIZE_MEMORY 选项未启用,则函数立即返回。
    if (NOT SANITIZE_MEMORY)
        return()
    endif ()
    # 调用辅助函数 'sanitizer_add_flags' (来自 sanitize-helpers)。
    # 这个函数将前面确定好的、有效的 MSan 标志应用到指定的 TARGET 上。
    sanitizer_add_flags(${TARGET} "MemorySanitizer" "MSan")
endfunction ()

FindTSan.cmake

# -------------------------------------------------------------------
##  配置选项
# -------------------------------------------------------------------
# 定义一个名为 SANITIZE_THREAD 的 CMake 选项。
# 用户可以通过此选项控制是否为构建目标启用 ThreadSanitizer。
# 默认值为 Off(不启用)。
# 提示信息:"Enable ThreadSanitizer for sanitized targets."
option(SANITIZE_THREAD "Enable ThreadSanitizer for sanitized targets." Off)
# -------------------------------------------------------------------
##  编译器标志候选
# -------------------------------------------------------------------
# 定义一个包含所有已知的、用于启用 ThreadSanitizer 的编译器标志的列表。
set(FLAG_CANDIDATES
    # MSVC (Microsoft Visual C++) 编译器使用的标志。
    "/fsanitize=thread"
    # GNU (GCC) 或 Clang 编译器使用的标志。
    # "-g" 启用调试信息。
    # "-fsanitize=thread" 启用 ThreadSanitizer。
    "-g -fsanitize=thread"
)
# -------------------------------------------------------------------
##  兼容性检查
# -------------------------------------------------------------------
# 检查 ThreadSanitizer (TSan) 是否与 MemorySanitizer (MSan) 同时启用。
# 这两种 Sanitizer 也是不兼容的,不能同时用于同一目标。
if (SANITIZE_THREAD AND SANITIZE_MEMORY)
    # 如果检测到冲突,则抛出致命错误并停止配置。
    message(FATAL_ERROR "ThreadSanitizer is not compatible with "
        "MemorySanitizer.")
endif ()
# -------------------------------------------------------------------
## 辅助工具
# -------------------------------------------------------------------
# 引入一个外部的 CMake 脚本文件 'sanitize-helpers.cmake'。
# 它提供了用于检查编译器标志和将其应用于目标的函数。
include(sanitize-helpers)
# -------------------------------------------------------------------
##  启用和平台/架构限制检查
# -------------------------------------------------------------------
# 仅在用户启用了 SANITIZE_THREAD 选项时执行后续检查。
if (SANITIZE_THREAD)
    # 检查系统名称是否既不是 "Linux" 也不是 "Darwin" (macOS)。
    # ThreadSanitizer (TSan) 主要支持 Linux 和 macOS。
    if (NOT ${CMAKE_SYSTEM_NAME} STREQUAL "Linux" AND
        NOT ${CMAKE_SYSTEM_NAME} STREQUAL "Darwin")
        # 如果平台不受支持,发出警告。
        message(WARNING "ThreadSanitizer disabled for target ${TARGET} because "
            "ThreadSanitizer is supported for Linux systems and macOS only.")
        # 强制将 SANITIZE_THREAD 选项设置为 Off,并将其缓存。
        set(SANITIZE_THREAD Off CACHE BOOL
            "Enable ThreadSanitizer for sanitized targets." FORCE)
    # 否则,检查指针大小是否不等于 8 字节(即是否不是 64 位系统)。
    # TSan 需要 64 位架构才能正常工作。
    elseif (NOT ${CMAKE_SIZEOF_VOID_P} EQUAL 8)
        # 如果不是 64 位系统,发出警告。
        message(WARNING "ThreadSanitizer disabled for target ${TARGET} because "
            "ThreadSanitizer is supported for 64bit systems only.")
        # 强制将 SANITIZE_THREAD 选项设置为 Off 并缓存。
        set(SANITIZE_THREAD Off CACHE BOOL
            "Enable ThreadSanitizer for sanitized targets." FORCE)
    # 如果通过了所有检查(是 Linux 或 macOS 且是 64 位系统):
    else ()
        # 检查编译器对候选标志的支持。
        # 'sanitizer_check_compiler_flags' 确定当前编译器实际支持的 TSan 标志。
        sanitizer_check_compiler_flags("${FLAG_CANDIDATES}" "ThreadSanitizer"
            "TSan")
    endif ()
endif ()
# -------------------------------------------------------------------
##  应用函数
# -------------------------------------------------------------------
# 定义一个名为 'add_sanitize_thread' 的 CMake 函数。
# 它的作用是专门为指定的构建目标 (TARGET) 添加 TSan 编译标志。
function (add_sanitize_thread TARGET)
    # 如果 SANITIZE_THREAD 选项未启用,则函数立即返回。
    if (NOT SANITIZE_THREAD)
        return()
    endif ()
    # 调用辅助函数 'sanitizer_add_flags'。
    # 这个函数将前面确定好的、有效的 TSan 标志应用到指定的 TARGET 上。
    sanitizer_add_flags(${TARGET} "ThreadSanitizer" "TSan")
endfunction ()

FindUBSan.cmake

# -------------------------------------------------------------------
##  配置选项
# -------------------------------------------------------------------
# 定义一个名为 SANITIZE_UNDEFINED 的 CMake 选项。
# 用户可以通过此选项控制是否为构建目标启用 UndefinedBehaviorSanitizer。
# 默认值为 Off(不启用)。
# 提示信息:"Enable UndefinedBehaviorSanitizer for sanitized targets."
option(SANITIZE_UNDEFINED
    "Enable UndefinedBehaviorSanitizer for sanitized targets." Off)
# -------------------------------------------------------------------
##  编译器标志候选
# -------------------------------------------------------------------
# 定义一个包含所有已知的、用于启用 UndefinedBehaviorSanitizer 的编译器标志的列表。
set(FLAG_CANDIDATES
    # MSVC (Microsoft Visual C++) 编译器使用的标志。
    "/fsanitize=undefined"
    # GNU (GCC) 或 Clang 编译器使用的标志。
    # "-g" 启用调试信息。
    # "-fsanitize=undefined" 启用 UndefinedBehaviorSanitizer。
    "-g -fsanitize=undefined"
)
# -------------------------------------------------------------------
## 辅助工具
# -------------------------------------------------------------------
# 引入一个外部的 CMake 脚本文件 'sanitize-helpers.cmake'。
# 它包含了用于检查和应用 Sanitizer 标志的辅助函数。
include(sanitize-helpers)
# -------------------------------------------------------------------
## 检查编译器支持
# -------------------------------------------------------------------
# 仅在用户启用了 SANITIZE_UNDEFINED 选项时执行后续检查。
if (SANITIZE_UNDEFINED)
    # 检查编译器对候选标志的支持。
    # 'sanitizer_check_compiler_flags' (来自 sanitize-helpers) 会尝试编译一个
    # 小程序来确定当前编译器实际支持哪个 UBSan 标志,并将选定的标志存储在内部变量中。
    # 参数: 标志列表, Sanitizer名称 ("UndefinedBehaviorSanitizer"), 简称 ("UBSan")。
    sanitizer_check_compiler_flags("${FLAG_CANDIDATES}"
        "UndefinedBehaviorSanitizer" "UBSan")
endif ()
# -------------------------------------------------------------------
##  应用函数
# -------------------------------------------------------------------
# 定义一个名为 'add_sanitize_undefined' 的 CMake 函数。
# 它的作用是专门为指定的构建目标 (TARGET) 添加 UBSan 编译标志。
function (add_sanitize_undefined TARGET)
    # 如果 SANITIZE_UNDEFINED 选项未启用,则函数立即返回。
    if (NOT SANITIZE_UNDEFINED)
        return()
    endif ()
    # 调用辅助函数 'sanitizer_add_flags' (来自 sanitize-helpers)。
    # 这个函数将前面确定好的、有效的 UBSan 标志应用到指定的 TARGET 上。
    sanitizer_add_flags(${TARGET} "UndefinedBehaviorSanitizer" "UBSan")
endfunction ()

sanitize-helpers.cmake

#---------------------------------------------
# Helper function: 获取源文件的语言类型
#---------------------------------------------
# FILE: 需要检测的源文件路径
# RETURN_VAR: 返回语言类型的变量名
function (sanitizer_lang_of_source FILE RETURN_VAR)
    # 获取文件扩展名
    get_filename_component(LONGEST_EXT "${FILE}" EXT)
    # 如果文件没有扩展名(例如一些无扩展名的头文件),直接返回空
    if("${LONGEST_EXT}" STREQUAL "")
       set(${RETURN_VAR} "" PARENT_SCOPE)
       return()
    endif()
    # 处理可能有多个点的文件名,取最后一个点后的最短扩展名
    string(REGEX REPLACE "^.*(\\.[^.]+)$" "\\1" FILE_EXT ${LONGEST_EXT})
    # 转小写,统一处理
    string(TOLOWER "${FILE_EXT}" FILE_EXT)
    # 去掉开头的点
    string(SUBSTRING "${FILE_EXT}" 1 -1 FILE_EXT)
    # 获取全局启用的语言列表
    get_property(ENABLED_LANGUAGES GLOBAL PROPERTY ENABLED_LANGUAGES)
    # 遍历已启用的语言,查找文件扩展名匹配的语言
    foreach (LANG ${ENABLED_LANGUAGES})
        list(FIND CMAKE_${LANG}_SOURCE_FILE_EXTENSIONS "${FILE_EXT}" TEMP)
        if (NOT ${TEMP} EQUAL -1)
            set(${RETURN_VAR} "${LANG}" PARENT_SCOPE)
            return()
        endif()
    endforeach()
    # 如果没有找到匹配语言,则返回空
    set(${RETURN_VAR} "" PARENT_SCOPE)
endfunction ()
#---------------------------------------------
# Helper function: 获取目标使用的编译器
#---------------------------------------------
# TARGET: CMake 目标名称
# RETURN_VAR: 返回使用的编译器列表变量名
function (sanitizer_target_compilers TARGET RETURN_VAR)
    # 用于存储目标所使用的编译器
    set(BUFFER "")
    # 获取目标的源文件列表
    get_target_property(TSOURCES ${TARGET} SOURCES)
    # 遍历每个源文件
    foreach (FILE ${TSOURCES})
        # 判断是否是对象库的生成表达式,如果是则跳过
        string(REGEX MATCH "TARGET_OBJECTS:([^ >]+)" _file ${FILE})
        if ("${_file}" STREQUAL "")
            # 获取源文件语言
            sanitizer_lang_of_source(${FILE} LANG)
            if (LANG)
                # 获取该语言对应的编译器
                list(APPEND BUFFER ${CMAKE_${LANG}_COMPILER_ID})
            endif()
        endif()
    endforeach()
    # 去重,确保返回的编译器列表唯一
    list(REMOVE_DUPLICATES BUFFER)
    set(${RETURN_VAR} "${BUFFER}" PARENT_SCOPE)
endfunction ()
#---------------------------------------------
# Helper function: 检查单个编译器标志是否可用
#---------------------------------------------
# FLAG: 待检测的编译器标志
# LANG: 源文件语言(C, CXX, Fortran)
# VARIABLE: 输出变量名,检测结果(TRUE/FALSE)
function (sanitizer_check_compiler_flag FLAG LANG VARIABLE)
    if (${LANG} STREQUAL "C")
        include(CheckCCompilerFlag)
        check_c_compiler_flag("${FLAG}" ${VARIABLE})
    elseif (${LANG} STREQUAL "CXX")
        include(CheckCXXCompilerFlag)
        check_cxx_compiler_flag("${FLAG}" ${VARIABLE})
    elseif (${LANG} STREQUAL "Fortran")
        # Fortran 检查标志可能在旧版本 CMake 中不可用
        include(CheckFortranCompilerFlag OPTIONAL RESULT_VARIABLE INCLUDED)
        if (INCLUDED)
            check_fortran_compiler_flag("${FLAG}" ${VARIABLE})
        elseif (NOT CMAKE_REQUIRED_QUIET)
            message(STATUS "Performing Test ${VARIABLE}")
            message(STATUS "Performing Test ${VARIABLE} - Failed (Check not supported)")
        endif()
    endif()
endfunction ()
#---------------------------------------------
# Helper function: 检查一组编译器标志是否可用
#---------------------------------------------
# FLAG_CANDIDATES: 候选标志列表
# NAME: 标志名称(用于消息显示)
# PREFIX: 前缀变量名,用于保存检测结果
function (sanitizer_check_compiler_flags FLAG_CANDIDATES NAME PREFIX)
    set(CMAKE_REQUIRED_QUIET ${${PREFIX}_FIND_QUIETLY})
    get_property(ENABLED_LANGUAGES GLOBAL PROPERTY ENABLED_LANGUAGES)
    foreach (LANG ${ENABLED_LANGUAGES})
        # 根据语言获取对应编译器
        set(COMPILER ${CMAKE_${LANG}_COMPILER_ID})
        # 如果该编译器还没有检测过
        if (COMPILER AND NOT DEFINED ${PREFIX}_${COMPILER}_FLAGS)
            foreach (FLAG ${FLAG_CANDIDATES})
                if(NOT CMAKE_REQUIRED_QUIET)
                    message(STATUS "Try ${COMPILER} ${NAME} flag = [${FLAG}]")
                endif()
                set(CMAKE_REQUIRED_FLAGS "${FLAG}")
                unset(${PREFIX}_FLAG_DETECTED CACHE)
                # 检查标志是否支持
                sanitizer_check_compiler_flag("${FLAG}" ${LANG} ${PREFIX}_FLAG_DETECTED)
                if (${PREFIX}_FLAG_DETECTED)
                    # 如果是 GNU 编译器,并且启用了静态链接选项,则再检测静态库标志
                    if (SANITIZE_LINK_STATIC AND (${COMPILER} STREQUAL "GNU"))
                        string(TOLOWER ${PREFIX} PREFIX_lower)
                        sanitizer_check_compiler_flag("-static-lib${PREFIX_lower}" ${LANG} ${PREFIX}_STATIC_FLAG_DETECTED)
                        if (${PREFIX}_STATIC_FLAG_DETECTED)
                            set(FLAG "-static-lib${PREFIX_lower} ${FLAG}")
                        endif()
                    endif()
                    # 缓存检测到的标志
                    set(${PREFIX}_${COMPILER}_FLAGS "${FLAG}" CACHE STRING "${NAME} flags for ${COMPILER} compiler.")
                    mark_as_advanced(${PREFIX}_${COMPILER}_FLAGS)
                    break()
                endif()
            endforeach()
            # 如果没有找到可用标志,则置空并发出警告
            if (NOT ${PREFIX}_FLAG_DETECTED)
                set(${PREFIX}_${COMPILER}_FLAGS "" CACHE STRING "${NAME} flags for ${COMPILER} compiler.")
                mark_as_advanced(${PREFIX}_${COMPILER}_FLAGS)
                message(WARNING "${NAME} is not available for ${COMPILER} compiler. Targets using this compiler will be compiled without ${NAME}.")
            endif()
        endif()
    endforeach()
endfunction ()
#---------------------------------------------
# Helper function: 给目标添加指定的 sanitizer 编译和链接标志
#---------------------------------------------
# TARGET: 目标名称
# NAME: sanitizer 名称(例如 ASAN, TSAN 等)
# PREFIX: 前缀变量名,用于获取标志
function (sanitizer_add_flags TARGET NAME PREFIX)
    # 获取目标使用的编译器
    sanitizer_target_compilers(${TARGET} TARGET_COMPILER)
    list(LENGTH TARGET_COMPILER NUM_COMPILERS)
    # 如果没有可用标志,则直接返回
    if ("${${PREFIX}_${TARGET_COMPILER}_FLAGS}" STREQUAL "")
        return()
    endif()
    # 将检测到的标志拆分为列表,并添加到目标编译选项和链接选项中
    separate_arguments(flags_list UNIX_COMMAND "${${PREFIX}_${TARGET_COMPILER}_FLAGS} ${SanBlist_${TARGET_COMPILER}_FLAGS}")
    target_compile_options(${TARGET} PUBLIC ${flags_list})
    separate_arguments(flags_list UNIX_COMMAND "${${PREFIX}_${TARGET_COMPILER}_FLAGS}")
    target_link_options(${TARGET} PUBLIC ${flags_list})
endfunction ()

FindSanitizers.cmake

#------------------------------------------------------------
# 选项: 是否尝试静态链接 Sanitizer 库
# Off 表示默认不静态链接
#------------------------------------------------------------
option(SANITIZE_LINK_STATIC "Try to link static against sanitizers." Off)
#------------------------------------------------------------
# 标记 Sanitizers 模块已被加载
# 供其他模块检测
#------------------------------------------------------------
set(Sanitizers_FOUND TRUE)
#------------------------------------------------------------
# 处理 QUIET 模式,如果 Sanitizers_FIND_QUIETLY 已定义,则查找时不输出信息
#------------------------------------------------------------
set(FIND_QUIETLY_FLAG "")
if (DEFINED Sanitizers_FIND_QUIETLY)
    set(FIND_QUIETLY_FLAG "QUIET")
endif ()
#------------------------------------------------------------
# 查找各类 Sanitizer 包
# 使用 QUIET 参数避免未找到时报错
# ASan: Address Sanitizer
# TSan: Thread Sanitizer
# MSan: Memory Sanitizer
# UBSan: Undefined Behavior Sanitizer
#------------------------------------------------------------
find_package(ASan ${FIND_QUIETLY_FLAG})
find_package(TSan ${FIND_QUIETLY_FLAG})
find_package(MSan ${FIND_QUIETLY_FLAG})
find_package(UBSan ${FIND_QUIETLY_FLAG})
#------------------------------------------------------------
# 函数: 添加 sanitizer 黑名单文件
# FILE: 黑名单文件路径(可相对或绝对路径)
#------------------------------------------------------------
function(sanitizer_add_blacklist_file FILE)
    # 如果传入路径不是绝对路径,则补全为当前源码目录下的绝对路径
    if(NOT IS_ABSOLUTE ${FILE})
        set(FILE "${CMAKE_CURRENT_SOURCE_DIR}/${FILE}")
    endif()
    # 获取真实路径,解析符号链接
    get_filename_component(FILE "${FILE}" REALPATH)
    # 调用前面定义的函数检查编译器是否支持 -fsanitize-blacklist
    # 并生成对应的标志变量 SanBlist_<compiler>_FLAGS
    sanitizer_check_compiler_flags("-fsanitize-blacklist=${FILE}"
        "SanitizerBlacklist" "SanBlist")
endfunction()
#------------------------------------------------------------
# 函数: 为指定目标添加已启用的 Sanitizer
# ARGV: 所有传入的目标
#------------------------------------------------------------
function(add_sanitizers)
    # 如果没有启用任何 sanitizer,则直接返回
    if (NOT (SANITIZE_ADDRESS OR SANITIZE_MEMORY OR SANITIZE_THREAD OR
        SANITIZE_UNDEFINED))
        return()
    endif ()
    # 遍历所有传入的目标
    foreach (TARGET ${ARGV})
        # 获取目标类型
        get_target_property(TARGET_TYPE ${TARGET} TYPE)
        # 如果是接口库,不能直接编译,打印警告
        if (TARGET_TYPE STREQUAL "INTERFACE_LIBRARY")
            message(WARNING "Can't use any sanitizers for target ${TARGET}, "
                    "because it is an interface library and cannot be "
                    "compiled directly.")
            return()
        endif ()
        # 获取目标所使用的编译器
        sanitizer_target_compilers(${TARGET} TARGET_COMPILER)
        list(LENGTH TARGET_COMPILER NUM_COMPILERS)
        # 如果目标使用了多个不同编译器,则无法使用 sanitizer
        if (NUM_COMPILERS GREATER 1)
            message(WARNING "Can't use any sanitizers for target ${TARGET}, "
                    "because it will be compiled by incompatible compilers. "
                    "Target will be compiled without sanitizers.")
            return()
        # 如果目标没有已知编译器,则发出警告(但可能是对象库输入的情况)
        elseif (NUM_COMPILERS EQUAL 0)
            message(WARNING "Sanitizers for target ${TARGET} may not be"
                    " usable, because it uses no or an unknown compiler. "
                    "This is a false warning for targets using only "
                    "object lib(s) as input.")
        endif ()
        # 调用各个具体的 sanitizer 添加函数,为目标添加对应的编译器标志
        add_sanitize_address(${TARGET})    # 添加 ASan 标志
        add_sanitize_thread(${TARGET})     # 添加 TSan 标志
        add_sanitize_memory(${TARGET})     # 添加 MSan 标志
        add_sanitize_undefined(${TARGET})  # 添加 UBSan 标志
    endforeach ()
endfunction(add_sanitizers)

CMakeLists.txt

#------------------------------------------------------------
# 将项目自定义的 CMake 模块目录添加到模块搜索路径
# CMAKE_MODULE_PATH 是 CMake 查找 find_package() 或 include() 模块时的搜索路径列表
# "${CMAKE_CURRENT_SOURCE_DIR}/cmake" 表示当前 CMakeLists.txt 所在目录下的 cmake 子目录
# 这样写可以确保自定义模块优先于系统模块被查找
#------------------------------------------------------------
set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake" ${CMAKE_MODULE_PATH})
#------------------------------------------------------------
# 函数: 添加普通测试用例
# TESTNAME: 测试用例名称(可作为生成的可执行文件名)
# SOURCEFILES: 测试源文件列表
#------------------------------------------------------------
function(add_testcase TESTNAME SOURCEFILES)
    # 从 ARGV 中移除 TESTNAME,使剩下的参数就是源文件列表
    list(REMOVE_AT ARGV 0)
    # 为测试用例添加可执行文件
    add_executable(${TESTNAME} ${ARGV})
    # 将生成的可执行文件注册为 CTest 测试
    add_test(${TESTNAME} ${TESTNAME})
endfunction(add_testcase)
#------------------------------------------------------------
# 函数: 添加启用 ASan 的测试用例
# TESTNAME: 测试用例名称
# SOURCEFILES: 测试源文件列表
#------------------------------------------------------------
function(add_sanitized_testcase TESTNAME SOURCEFILES)
    # 先调用普通测试用例函数,生成可执行文件和注册测试
    add_testcase(${TESTNAME} ${SOURCEFILES})
    # 为目标添加已启用的 sanitizer 标志(如 ASan)
    add_sanitizers(${TESTNAME})
endfunction(add_sanitized_testcase)
#------------------------------------------------------------
# 开启 AddressSanitizer
# TRUE 表示启用 ASan
#------------------------------------------------------------
# set(SANITIZE_ADDRESS TRUE)
# set(SANITIZE_MEMORY TRUE)
set(SANITIZE_THREAD TRUE)
# set(SANITIZE_UNDEFINED TRUE)
#------------------------------------------------------------
# 查找 Sanitizer 模块(之前定义的 Sanitizers.cmake)
# 用于后续 add_sanitizers 调用
#------------------------------------------------------------
find_package(Sanitizers)
#------------------------------------------------------------
# 添加测试用例
# "use-after-free.cpp" 测试源文件,将会生成同名可执行文件
# 并注册为 CTest 测试
#------------------------------------------------------------
add_sanitized_testcase("use-after-free.cpp" use-after-free.cpp)
#------------------------------------------------------------
# 设置测试属性
# WILL_FAIL: 表示测试预期会失败(例如 use-after-free 会触发 ASan 报错)
# CTest 会将失败视为通过此属性标记
#------------------------------------------------------------
set_tests_properties(
    "use-after-free.cpp"
    PROPERTIES
    WILL_FAIL TRUE
)

use-after-free.cpp

#include <iostream>
int* makeDangling() {
    int* p = new int(42);
    delete p;
    return p;  // 返回了一个悬空指针
}
int main() {
    int* d = makeDangling();
    std::cout << *d << "\n";  // use-after-free
}

set(SANITIZE_ADDRESS TRUE)

▶ 运行输出示例

==1270==ERROR: AddressSanitizer: heap-use-after-free on address 0x502000000010 at pc 0x555555555328 bp 0x7fffffffd180 sp 0x7fffffffd170
READ of size 4 at 0x502000000010 thread T0
    #0 0x555555555327 in main /home/xiaqiu/test/CppCon/day459/code/use-after-free.cpp:9
    #1 0x7ffff735c1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #2 0x7ffff735c28a in __libc_start_main_impl ../csu/libc-start.c:360
    #3 0x5555555551a4 in _start (/home/xiaqiu/test/build/debug/CppCon/day459/code/use-after-free.cpp+0x11a4) (BuildId: 84f431458253cd6d4ca707054c7f8a8bbefe3d6f)

2⃣ UndefinedBehaviorSanitizer(UBSan)示例:整数溢出

问题代码(integer-overflow.cpp)

#include <iostream>
int main() {
    int x = 2147483647;
    x = x + 1;   // signed overflow → UB
    std::cout << x;
}

set(SANITIZE_UNDEFINED TRUE)

▶ UBSan 报错

/home/xiaqiu/test/CppCon/day459/code/use-after-free.cpp:4:7: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'

3⃣ ThreadSanitizer(TSan)示例:数据竞争(data race)

问题代码(data-race.cpp)

#include <thread>
#include <iostream>
int x = 0;
void race() {
    for(int i = 0; i < 1000000; ++i)
        x++;  // data race
}
int main() {
    std::thread t1(race);
    std::thread t2(race);
    t1.join();
    t2.join();
    std::cout << x;
}

set(SANITIZE_THREAD TRUE)

▶ TSan 报错

==================
WARNING: ThreadSanitizer: data race (pid=15795)
  Read of size 4 at 0x555555559154 by thread T2:
    #0 race() /home/xiaqiu/test/CppCon/day459/code/use-after-free.cpp:5 (use-after-free.cpp+0x1379) (BuildId: c11e5cde9510242ef92fbf8ce6b57880d63c8bcc)
    #1 void std::__invoke_impl<void, void (*)()>(std::__invoke_other, void (*&&)()) /usr/include/c++/13/bits/invoke.h:61 (use-after-free.cpp+0x2162) (BuildId: c11e5cde9510242ef92fbf8ce6b57880d63c8bcc)
    #2 std::__invoke_result<void (*)()>::type std::__invoke<void (*)()>(void (*&&)()) /usr/include/c++/13/bits/invoke.h:96 (use-after-free.cpp+0x20b7) (BuildId: c11e5cde9510242ef92fbf8ce6b57880d63c8bcc)
    #3 void std::thread::_Invoker<std::tuple<void (*)()> >::_M_invoke<0ul>(std::_Index_tuple<0ul>) /usr/include/c++/13/bits/std_thread.h:292 (use-after-free.cpp+0x200c) (BuildId: c11e5cde9510242ef92fbf8ce6b57880d63c8bcc)
    #4 std::thread::_Invoker<std::tuple<void (*)()> >::operator()() /usr/include/c++/13/bits/std_thread.h:299 (use-after-free.cpp+0x1fae) (BuildId: c11e5cde9510242ef92fbf8ce6b57880d63c8bcc)
    #5 std::thread::_State_impl<std::thread::_Invoker<std::tuple<void (*)()> > >::_M_run() /usr/include/c++/13/bits/std_thread.h:244 (use-after-free.cpp+0x1f60) (BuildId: c11e5cde9510242ef92fbf8ce6b57880d63c8bcc)
    #6 <null> <null> (libstdc++.so.6+0xecdb3) (BuildId: ca77dae775ec87540acd7218fa990c40d1c94ab1)
  Previous write of size 4 at 0x555555559154 by thread T1:
    #0 race() /home/xiaqiu/test/CppCon/day459/code/use-after-free.cpp:5 (use-after-free.cpp+0x1391) (BuildId: c11e5cde9510242ef92fbf8ce6b57880d63c8bcc)
    #1 void std::__invoke_impl<void, void (*)()>(std::__invoke_other, void (*&&)()) /usr/include/c++/13/bits/invoke.h:61 (use-after-free.cpp+0x2162) (BuildId: c11e5cde9510242ef92fbf8ce6b57880d63c8bcc)
    #2 std::__invoke_result<void (*)()>::type std::__invoke<void (*)()>(void (*&&)()) /usr/include/c++/13/bits/invoke.h:96 (use-after-free.cpp+0x20b7) (BuildId: c11e5cde9510242ef92fbf8ce6b57880d63c8bcc)
    #3 void std::thread::_Invoker<std::tuple<void (*)()> >::_M_invoke<0ul>(std::_Index_tuple<0ul>) /usr/include/c++/13/bits/std_thread.h:292 (use-after-free.cpp+0x200c) (BuildId: c11e5cde9510242ef92fbf8ce6b57880d63c8bcc)
    #4 std::thread::_Invoker<std::tuple<void (*)()> >::operator()() /usr/include/c++/13/bits/std_thread.h:299 (use-after-free.cpp+0x1fae) (BuildId: c11e5cde9510242ef92fbf8ce6b57880d63c8bcc)
    #5 std::thread::_State_impl<std::thread::_Invoker<std::tuple<void (*)()> > >::_M_run() /usr/include/c++/13/bits/std_thread.h:244 (use-after-free.cpp+0x1f60) (BuildId: c11e5cde9510242ef92fbf8ce6b57880d63c8bcc)
    #6 <null> <null> (libstdc++.so.6+0xecdb3) (BuildId: ca77dae775ec87540acd7218fa990c40d1c94ab1)
  Location is global 'x' of size 4 at 0x555555559154 (use-after-free.cpp+0x5154)
  Thread T2 (tid=15813, running) created by main thread at:
    #0 pthread_create ../../../../src/libsanitizer/tsan/tsan_interceptors_posix.cpp:1022 (libtsan.so.2+0x5ac1a) (BuildId: 38097064631f7912bd33117a9c83d08b42e15571)
    #1 std::thread::_M_start_thread(std::unique_ptr<std::thread::_State, std::default_delete<std::thread::_State> >, void (*)()) <null> (libstdc++.so.6+0xeceb0) (BuildId: ca77dae775ec87540acd7218fa990c40d1c94ab1)
    #2 main /home/xiaqiu/test/CppCon/day459/code/use-after-free.cpp:9 (use-after-free.cpp+0x1404) (BuildId: c11e5cde9510242ef92fbf8ce6b57880d63c8bcc)
  Thread T1 (tid=15812, running) created by main thread at:
    #0 pthread_create ../../../../src/libsanitizer/tsan/tsan_interceptors_posix.cpp:1022 (libtsan.so.2+0x5ac1a) (BuildId: 38097064631f7912bd33117a9c83d08b42e15571)
    #1 std::thread::_M_start_thread(std::unique_ptr<std::thread::_State, std::default_delete<std::thread::_State> >, void (*)()) <null> (libstdc++.so.6+0xeceb0) (BuildId: ca77dae775ec87540acd7218fa990c40d1c94ab1)
    #2 main /home/xiaqiu/test/CppCon/day459/code/use-after-free.cpp:8 (use-after-free.cpp+0x13ee) (BuildId: c11e5cde9510242ef92fbf8ce6b57880d63c8bcc)
SUMMARY: ThreadSanitizer: data race /home/xiaqiu/test/CppCon/day459/code/use-after-free.cpp:5 in race()
==================
==================
WARNING: ThreadSanitizer: data race (pid=15795)
  Write of size 4 at 0x555555559154 by thread T2:
    #0 race() /home/xiaqiu/test/CppCon/day459/code/use-after-free.cpp:5 (use-after-free.cpp+0x1391) (BuildId: c11e5cde9510242ef92fbf8ce6b57880d63c8bcc)
    #1 void std::__invoke_impl<void, void (*)()>(std::__invoke_other, void (*&&)()) /usr/include/c++/13/bits/invoke.h:61 (use-after-free.cpp+0x2162) (BuildId: c11e5cde9510242ef92fbf8ce6b57880d63c8bcc)
    #2 std::__invoke_result<void (*)()>::type std::__invoke<void (*)()>(void (*&&)()) /usr/include/c++/13/bits/invoke.h:96 (use-after-free.cpp+0x20b7) (BuildId: c11e5cde9510242ef92fbf8ce6b57880d63c8bcc)
    #3 void std::thread::_Invoker<std::tuple<void (*)()> >::_M_invoke<0ul>(std::_Index_tuple<0ul>) /usr/include/c++/13/bits/std_thread.h:292 (use-after-free.cpp+0x200c) (BuildId: c11e5cde9510242ef92fbf8ce6b57880d63c8bcc)
    #4 std::thread::_Invoker<std::tuple<void (*)()> >::operator()() /usr/include/c++/13/bits/std_thread.h:299 (use-after-free.cpp+0x1fae) (BuildId: c11e5cde9510242ef92fbf8ce6b57880d63c8bcc)
    #5 std::thread::_State_impl<std::thread::_Invoker<std::tuple<void (*)()> > >::_M_run() /usr/include/c++/13/bits/std_thread.h:244 (use-after-free.cpp+0x1f60) (BuildId: c11e5cde9510242ef92fbf8ce6b57880d63c8bcc)
    #6 <null> <null> (libstdc++.so.6+0xecdb3) (BuildId: ca77dae775ec87540acd7218fa990c40d1c94ab1)
  Previous write of size 4 at 0x555555559154 by thread T1:
    #0 race() /home/xiaqiu/test/CppCon/day459/code/use-after-free.cpp:5 (use-after-free.cpp+0x1391) (BuildId: c11e5cde9510242ef92fbf8ce6b57880d63c8bcc)
    #1 void std::__invoke_impl<void, void (*)()>(std::__invoke_other, void (*&&)()) /usr/include/c++/13/bits/invoke.h:61 (use-after-free.cpp+0x2162) (BuildId: c11e5cde9510242ef92fbf8ce6b57880d63c8bcc)
    #2 std::__invoke_result<void (*)()>::type std::__invoke<void (*)()>(void (*&&)()) /usr/include/c++/13/bits/invoke.h:96 (use-after-free.cpp+0x20b7) (BuildId: c11e5cde9510242ef92fbf8ce6b57880d63c8bcc)
    #3 void std::thread::_Invoker<std::tuple<void (*)()> >::_M_invoke<0ul>(std::_Index_tuple<0ul>) /usr/include/c++/13/bits/std_thread.h:292 (use-after-free.cpp+0x200c) (BuildId: c11e5cde9510242ef92fbf8ce6b57880d63c8bcc)
    #4 std::thread::_Invoker<std::tuple<void (*)()> >::operator()() /usr/include/c++/13/bits/std_thread.h:299 (use-after-free.cpp+0x1fae) (BuildId: c11e5cde9510242ef92fbf8ce6b57880d63c8bcc)
    #5 std::thread::_State_impl<std::thread::_Invoker<std::tuple<void (*)()> > >::_M_run() /usr/include/c++/13/bits/std_thread.h:244 (use-after-free.cpp+0x1f60) (BuildId: c11e5cde9510242ef92fbf8ce6b57880d63c8bcc)
    #6 <null> <null> (libstdc++.so.6+0xecdb3) (BuildId: ca77dae775ec87540acd7218fa990c40d1c94ab1)
  Location is global 'x' of size 4 at 0x555555559154 (use-after-free.cpp+0x5154)
  Thread T2 (tid=15813, running) created by main thread at:
    #0 pthread_create ../../../../src/libsanitizer/tsan/tsan_interceptors_posix.cpp:1022 (libtsan.so.2+0x5ac1a) (BuildId: 38097064631f7912bd33117a9c83d08b42e15571)
    #1 std::thread::_M_start_thread(std::unique_ptr<std::thread::_State, std::default_delete<std::thread::_State> >, void (*)()) <null> (libstdc++.so.6+0xeceb0) (BuildId: ca77dae775ec87540acd7218fa990c40d1c94ab1)
    #2 main /home/xiaqiu/test/CppCon/day459/code/use-after-free.cpp:9 (use-after-free.cpp+0x1404) (BuildId: c11e5cde9510242ef92fbf8ce6b57880d63c8bcc)
  Thread T1 (tid=15812, running) created by main thread at:
    #0 pthread_create ../../../../src/libsanitizer/tsan/tsan_interceptors_posix.cpp:1022 (libtsan.so.2+0x5ac1a) (BuildId: 38097064631f7912bd33117a9c83d08b42e15571)
    #1 std::thread::_M_start_thread(std::unique_ptr<std::thread::_State, std::default_delete<std::thread::_State> >, void (*)()) <null> (libstdc++.so.6+0xeceb0) (BuildId: ca77dae775ec87540acd7218fa990c40d1c94ab1)
    #2 main /home/xiaqiu/test/CppCon/day459/code/use-after-free.cpp:8 (use-after-free.cpp+0x13ee) (BuildId: c11e5cde9510242ef92fbf8ce6b57880d63c8bcc)
SUMMARY: ThreadSanitizer: data race /home/xiaqiu/test/CppCon/day459/code/use-after-free.cpp:5 in race()
==================
2000000ThreadSanitizer: reported 2 warnings
[1] + Done                       "/usr/bin/gdb" --interpreter=mi --tty=${DbgTerm} 0<"/tmp/Microsoft-MIEngine-In-1stnfanc.xai" 1>"/tmp/Microsoft-MIEngine-Out-hogdtz3c.zvk"

4⃣ MemorySanitizer(MSan)示例:未初始化内存使用

MSan 只能在 Clang + 全部库都必须带 MSan 时使用。

问题代码(uninitialized.cpp)

#include <iostream>
int main() {
    int x;
    std::cout << x << "\n"; // 未初始化
}

set(SANITIZE_UNDEFINED TRUE)

▶ MSan 报错

==INFO: MemorySanitizer: use-of-uninitialized-value

5⃣ LeakSanitizer(LSan)示例:内存泄漏

LSan 通常随 ASan 自动开启。

问题代码(leak.cpp)

int* leak() {
    int* p = new int(5);
    return p; // 未 delete
}
int main() {
    leak();
}

▶ 输出

Direct leak of 4 byte(s) in 1 object(s)

关于 构建系统的并行性和可复现性

1⃣ 在每次按键时运行 Sanitizers

  • 目标:希望在每次代码编辑或按键时运行 AddressSanitizer、ThreadSanitizer 等。
  • 问题:这些工具对 CPU 消耗很高,尤其在大型项目中,可能严重拖慢编译速度。
  • 思路:学习其他构建系统如何优化并行编译和资源利用。

2⃣ 构建系统的并行性(Parallelism)

并行级别

Level 1 - Gradle
  • Gradle 支持子项目级并行,即每个子模块(sub-project)可以并行构建。
  • 结构示例:
.
├── app
│   └── build.gradle
├── lib
│   └── build.gradle
└── settings.gradle
  • 限制:
    1. 并行粒度粗:每个子项目是最小单位。
    2. 子项目数量有限。
    3. 默认情况下,并行是关闭的(需要 --parallel 启用)。
Level 2 - Make / FASTBuild
  • 可以更细粒度地定义目标(targets)。
  • 可通过参数 -j--jobs 指定最大并行任务数:
    MaxParallelJobs = − j × CPU cores \text{MaxParallelJobs} = -j \times \text{CPU cores} MaxParallelJobs=j×CPU cores
  • 适合 CPU 密集型编译任务。
Level 3 - Bazel
  • 与前一级相同,但可以针对资源密集型目标(如大文件链接、代码生成)动态降低并行度。
  • 公式示意:
    EffectiveParallelism = min ⁡ ( MaxParallelJobs , CPU available TargetResourceUsage ) \text{EffectiveParallelism} = \min(\text{MaxParallelJobs}, \frac{\text{CPU available}}{\text{TargetResourceUsage}}) EffectiveParallelism=min(MaxParallelJobs,TargetResourceUsageCPU available)
Level 4 - Ninja
  • 可以定义多个独立 Job Pool,每个池子大小不同。
  • 例子:
link=1   # 链接操作单线程
cc=16    # 编译操作最多16线程
codegen=16

CMake 支持 Job Pools 示例

  1. 定义全局 Job Pools
set_property(GLOBAL PROPERTY JOB_POOLS
    compile=16
    link=1
    codegen=16
)
  1. 为目标分配 Job Pool
set_property(TARGET atarget PROPERTY JOB_POOL_COMPILE compile)
set_property(TARGET atarget PROPERTY JOB_POOL_LINK link)
  1. 为自定义命令分配 Job Pool
add_custom_target(protocgen
    COMMAND protoc --cpp_out=./out server.proto
    JOB_POOL codegen
    SOURCES t
)

总结:CMake 可以控制不同类型任务的并行度,例如编译、链接和代码生成任务独立分配 CPU 核心,类似 Ninja 的多池设计。

3⃣ 可复现性(Reproducibility)

  • 定义:给定相同输入和配置,构建目标应产生完全相同的输出
  • 公式表示:
    Output = f ( SourceFiles , Toolchains , Config ) \text{Output} = f(\text{SourceFiles}, \text{Toolchains}, \text{Config}) Output=f(SourceFiles,Toolchains,Config)

    f ( Inputs , Config ) = f ( Inputs , Config ) 在任意机器上 f(\text{Inputs}, \text{Config}) = f(\text{Inputs}, \text{Config}) \quad \text{在任意机器上} f(Inputs,Config)=f(Inputs,Config)在任意机器上
    则构建是可复现的。

可复现性等级

Level 1 - Make / Ninja / FASTBuild
  • 没有提供工具确保可复现性。
  • 开发者需要自行保证环境一致性(Hermeticity)。
Level 2 - Meson
  • 可以查询并配置构建工具。
  • 支持定义自定义工具链,确保可复现性。
  • 可用公式表示工具链输入:
    Output = f ( SourceFiles , ToolchainConfig ) \text{Output} = f(\text{SourceFiles}, \text{ToolchainConfig}) Output=f(SourceFiles,ToolchainConfig)
Level 3 - Gradle
  • 内置 robust 的工具链配置,通常不依赖系统全局工具。
  • 各个工作站上构建结果一致。
  • 可以通过配置工具链参数实现可复现:
    ToolchainConfig = compiler version , flags , library versions \text{ToolchainConfig} = {\text{compiler version}, \text{flags}, \text{library versions}} ToolchainConfig=compiler version,flags,library versions
Level 4 - Bazel
  • 工具链被视作普通输入。
  • 可以轻松定义自定义工具链,且保持构建可复现。
  • 输出完全确定:
    Output = f ( SourceFiles , ToolchainInputs , Config ) \text{Output} = f(\text{SourceFiles}, \text{ToolchainInputs}, \text{Config}) Output=f(SourceFiles,ToolchainInputs,Config)

总结理解

  1. 并行性
    • Gradle < Make/FASTBuild < Bazel < Ninja。
    • Job Pool 可以让 CMake/Ninja 精细控制不同任务的并行度。
    • 对 CPU 密集型工具(如 Sanitizers)很重要。
  2. 可复现性
    • Make/Ninja 基本没有保障。
    • Meson、Gradle 提供工具链配置。
    • Bazel 最强,可将工具链和构建配置完全视作输入,实现可复现构建。
  3. 数学模型
    • 并行度:
      EffectiveParallelism = min ⁡ ( MaxParallelJobs , AvailableCPU TargetCPUUsage ) \text{EffectiveParallelism} = \min(\text{MaxParallelJobs}, \frac{\text{AvailableCPU}}{\text{TargetCPUUsage}}) EffectiveParallelism=min(MaxParallelJobs,TargetCPUUsageAvailableCPU)
  • 可复现性:
    Output = f ( Inputs , Toolchains , Config ) \text{Output} = f(\text{Inputs}, \text{Toolchains}, \text{Config}) Output=f(Inputs,Toolchains,Config)
  • 输出应保持在任意机器上完全一致。

CMake 是否可实现高级构建特性

1⃣ CMake 是否可以实现这些功能?

  • 答案是 可以,但是需要解决几个特定问题

问题 1:文件顺序问题

  • 描述:编译源文件的顺序会影响结果,比如:
    Build ( a . c p p , b . c p p ) ≠ Build ( b . c p p , a . c p p ) \text{Build}(a.cpp, b.cpp) \neq \text{Build}(b.cpp, a.cpp) Build(a.cpp,b.cpp)=Build(b.cpp,a.cpp)
  • 解决方案:CMake 需要手动列出输入源文件,因此文件顺序是确定的,天然避免了这个问题。
set(APP_SOURCES
    a.cpp
    b.cpp
)
  • CMake 会按列表顺序处理。

问题 2:__DATE____TIME__

  • 问题:这些宏会在每次构建时产生不同的值,影响可复现性。
  • 解决方案
    1. MSVC
add_link_options("/Brepro")  # 启用可复现构建选项
  1. GCC(通过环境变量)
export SOURCE_DATE_EPOCH=1621012303
  1. GCC + Clang(重写宏):
add_definitions(-D__DATE__="May  14 2021")
add_definitions(-D__TIME__="17:11:43")
add_compile_options(-Wno-builtin-macro-redefined)

原理:把宏值固定,使每次编译生成的二进制一致。

问题 3:GCC 使用 -flto 时的随机性

  • 问题:GCC 的 LTO(Link-Time Optimization)会随机生成符号,导致不可复现。
  • 解决方案:为每个源文件设置固定随机种子(hash 值):
set(APP_SOURCES
    app.cpp
    lib.cpp
)
foreach(_file ${APP_SOURCES})
    file(SHA1 ${_file} sha1sum)
    string(SUBSTRING ${sha1sum} 0 8 sha1sum)
    set_property(SOURCE ${_file} APPEND_STRING PROPERTY 
        COMPILE_FLAGS "-frandom-seed=0x${sha1sum}")
endforeach()
  • 数学原理:随机种子由源文件内容唯一确定:
    Seed file = SHA1 ( FileContent ) [ 0 : 8 ] \text{Seed}_{\text{file}} = \text{SHA1}(\text{FileContent})[0:8] Seedfile=SHA1(FileContent)[0:8]

问题 4:__FILE__

  • 问题__FILE__ 宏包含完整路径,不同机器、不同路径下构建会不同。
  • 解决方案
    1. 保证源码和构建路径一致。
    2. 利用 Git + Ninja + 内容可寻址文件系统(Content Addressable Filesystem, CAF)
      优点:
  • 源码和构建树都在固定路径。
  • 可以追踪每次提交的变动(commit-id)。
  • 可以缓存编译结果,避免重复构建。

2⃣ Caching(缓存机制)

缓存等级

Level 1 - Gradle
  • 低粒度:小改动可能导致大目标重建。
  • 缓存粒度粗。
Level 2 - Make / Ninja
  • 基于文件系统的时间戳(mtime):
    Rebuild if:  ∃ f i ∈ Inputs s.t.  m t i m e ( f i ) > m t i m e ( Target ) \text{Rebuild if: } \exists f_i \in \text{Inputs} \text{ s.t. } mtime(f_i) > mtime(\text{Target}) Rebuild if: fiInputs s.t. mtime(fi)>mtime(Target)
  • 如果任一输入文件的修改时间晚于目标文件,则重新构建。
Level 3 - FASTBuild
  • 除了文件系统缓存,还支持 分布式缓存(Distributed Caching)。
Level 4 - Bazel
  • 基于文件内容的 哈希值(Digest)判断输入是否改变,而不是时间戳。
  • 同时有 内存缓存 支持大规模构建图。
    公式表示:
    Rebuild if:  ∃ f i ∈ Inputs s.t. Digest ( f i ) ≠ CachedDigest ( f i ) \text{Rebuild if: } \exists f_i \in \text{Inputs} \text{ s.t. } \text{Digest}(f_i) \neq \text{CachedDigest}(f_i) Rebuild if: fiInputs s.t. Digest(fi)=CachedDigest(fi)
    优势:即使文件时间戳相同,只要内容改变,也能重新构建。

CMake 的缓存策略

  • 默认:使用 Make 或 Ninja 可以得到文件系统缓存。
  • 增强:可以使用 tipi(Engflow + Tipi)来实现:
    • 远程执行(Remote Execution)
    • 远程缓存(Remote Caching)
  • 这样在 CMake 中也可以实现类似 Bazel 的内容可寻址缓存。

3⃣ 总结

  1. 可复现性问题
    • __DATE__, __TIME__, __FILE__, LTO 随机符号都是主要障碍。
    • 通过固定宏值、固定路径、SHA1 随机种子可解决。
  2. 缓存策略
    • Gradle < Make/Ninja < FASTBuild < Bazel
    • 文件系统 vs 内容哈希 vs 分布式缓存
    • CMake 可以通过 Ninja + tipi 实现高级缓存。
  3. 数学表示
  • 文件修改判断(时间戳):
    Rebuild = ⋁ f i ∈ Inputs [ m t i m e ( f i ) > m t i m e ( Target ) ] \text{Rebuild} = \bigvee_{f_i \in \text{Inputs}} [mtime(f_i) > mtime(\text{Target})] Rebuild=fiInputs[mtime(fi)>mtime(Target)]
  • 文件修改判断(内容哈希):
    Rebuild = ⋁ f i ∈ Inputs [ D i g e s t ( f i ) ≠ C a c h e d D i g e s t ( f i ) ] \text{Rebuild} = \bigvee_{f_i \in \text{Inputs}} [Digest(f_i) \neq CachedDigest(f_i)] Rebuild=fiInputs[Digest(fi)=CachedDigest(fi)]
  • LTO 随机符号固定种子:
    Seed file = SHA1 ( FileContent ) [ 0 : 8 ] \text{Seed}_{\text{file}} = \text{SHA1}(\text{FileContent})[0:8] Seedfile=SHA1(FileContent)[0:8]

CMake 缓存层级与依赖管理

1⃣ 缓存(Cache)策略

构建系统通常会引入多级缓存来优化构建速度,尤其在持续集成(CI)场景下。

L1 Cache

  • 作用:加快本地构建、安装目录恢复(install tree clean restore)。
  • 特点:高速、靠近 CPU,适合频繁的小规模操作。
  • 理念:保持工作区/中间构建结果在 L1 Cache 中,减少 I/O 开销。

L2 Cache

  • 作用:允许更多并行 CI 任务而占用更少核心资源。
  • 特点:速度比 L1 慢,但容量更大,适合共享缓存。
  • 理念:在分布式/并行构建中提供中间结果的缓存共享。

2⃣ 依赖管理(Dependency Management)

问题:如何处理第三方库和外部依赖。

  • Level 1 - Make / Ninja / FASTBuild
    • 没有依赖管理机制,开发者需手动处理。
  • Level 2 - Gradle
    • 主要管理 Java 的 .jar 包。
    • 对其他语言(如 C++)支持较差。
  • Level 3 - Meson / Bazel
    • 提供扩展机制,支持多语言依赖解析。
    • 有中央化依赖仓库,针对各自构建系统优化。

3⃣ C++ 包管理与 CMake

  • 问题:C++ 缺乏统一的包管理标准。
  • 解决方案:CMake 已成为事实标准,提供 FetchContent 模块。

FetchContent 用法示例

include(FetchContent)
FetchContent_Declare(
    Boost
    GIT_REPOSITORY https://github.com/boostorg/boost.git
    GIT_TAG        boost-1.80.0
)
FetchContent_MakeAvailable(Boost)
find_package(boost_filesystem CONFIG REQUIRED)
target_link_libraries(app Boost::filesystem)
  • 原理
    1. 从 Git 下载依赖。
    2. 构建并安装到项目中。
    3. 自动暴露 CMake targets,方便 target_link_libraries 使用。
  • 问题:FetchContent 下载与构建较慢(🕠)。

4⃣ 提升 FetchContent 性能:自定义依赖提供器

思路

  • 使用宏或函数 拦截 FetchContent 调用
  • 优化重复下载与构建过程。
  • 提供统一的依赖源管理接口。

示例宏 tipi_provide_dependency

macro(tipi_provide_dependency method package_name)
    set(oneValueArgs
        GIT_REPOSITORY
        GIT_TAG
    )
    cmake_parse_arguments(
        ARG "${options}" "${oneValueArgs}"
        "${multiValueArgs}" ${ARGN}
    )
    # 可以在此对 ${ARG_GIT_REPOSITORY} 做缓存或加速处理
    FetchContent_SetPopulated(${package_name})
endmacro()
  • 核心
    • 拦截 FetchContent_MakeAvailable 调用。
    • 可实现缓存、并行或加速下载。
    • 类似于 L1/L2 缓存思路:本地高效缓存 + 中间层共享缓存。

安装依赖提供器

cmake_language(
    SET_DEPENDENCY_PROVIDER tipi_provide_dependency
    SUPPORTED_METHODS FETCHCONTENT_MAKEAVAILABLE_SERIAL
)
list(APPEND CMAKE_PROJECT_TOP_LEVEL_INCLUDES dependency_provider.cmake)
  • 作用:将自定义依赖提供器注入顶层 CMake 项目,统一管理 FetchContent 调用。

5⃣ 实例:安装 Boost

include(FetchContent)
FetchContent_Declare(
    Boost
    GIT_REPOSITORY https://github.com/boostorg/boost.git
    GIT_TAG        boost-1.80.0
)
FetchContent_MakeAvailable(Boost)
  • 流程:
    1. 从 Git 克隆 Boost。
    2. 构建 Boost。
    3. 在项目中注册 Boost targets,可直接链接。

6⃣ 总结理解

  1. 缓存策略

缓存层 作用 特点
L1 快速恢复构建树 CPU 临近,高速小容量
L2 分布式/并行共享 容量大,可减少核心占用
  1. 依赖管理等级

Level 构建系统 特性
1 Make/Ninja/FASTBuild 无依赖管理,需要手动处理
2 Gradle Java .jar 支持较好,其他语言一般
3 Meson/Bazel 多语言支持,中央化仓库,扩展性好
  1. CMake 优势
  • 提供 FetchContent 作为事实标准。
  • 可通过自定义宏实现缓存优化、依赖加速。
  • 类似于构建缓存模型:
    EffectiveBuildTime = BuildTimeWithoutCache × ( 1 − CacheHitRate ) \text{EffectiveBuildTime} = \text{BuildTimeWithoutCache} \times (1 - \text{CacheHitRate}) EffectiveBuildTime=BuildTimeWithoutCache×(1CacheHitRate)
  • 通过 L1/L2 缓存和依赖提供器,可大幅缩短构建时间。

1⃣ CMake 的优势(Advantages)

  • 无锁定(No lock-in):纯 CMake,不依赖特定构建系统或厂商。
  • 按需从源码重建:必要时可从源码完整重建目标。
  • 高效且安全的构建缓存:利用本地或远程缓存加速重复构建。
  • 自动 CMake 包缓存:可以通过 tipi-build/cmake-tipi-provider 实现自动缓存。

2⃣ 构建可追溯性(Build Provenance & Traceability)

  • SBOMs(Software Bill of Materials)
    • 用于记录项目依赖及构建信息。
    • 可帮助实现软件供应链安全(Software Supply Chain Safety)。
    • 常用格式:SPDX。
    • 可验证 NTIA 合规性。

SBOMs 手动生成示例

include(sbom)
sbom_generate(
    OUTPUT ${CMAKE_INSTALL_PREFIX}/sbom-${GIT_VERSION_PATH}.spdx
    LICENSE MIT
    SUPPLIER tipi
    SUPPLIER_URL https://tipi.build
)
reuse_spdx()
add_executable(app app.cpp)
install(
    TARGETS app
    EXPORT "${targets_export_name}"
    RUNTIME DESTINATION "bin"
)

SBOM 自动生成示例

sbom_add(TARGET app)
sbom_finalize()
  • 说明
    1. sbom_add(TARGET app):为目标 app 生成依赖信息。
    2. sbom_finalize():汇总所有 SBOM 信息,生成最终 SPDX 文件。
  • 自动 SBOM 可以追踪依赖库及构建缓存信息,方便安全审计。

3⃣ 分布式 / 远程构建(Distributed / Remote Builds)

  • 概念:构建系统可以将目标构建任务缓存或在共享的分布式系统上执行,降低本地资源占用。
  • Level 1 - Make / Ninja / Meson
    • 无分布式缓存或远程执行,所有任务本地完成。
  • Level 2 - Gradle
    • 支持远程缓存,但不支持远程执行。
    • 低粒度目标限制了特性潜力。
  • Level 3 - FASTBuild
    • 支持分布式执行和缓存,但远程执行只针对已知的特定编译器。
  • Level 4 - Bazel
    • 支持任意工具(编译器、链接器、测试等)的分布式构建和缓存。

4⃣ CMake 支持分布式构建

  • 实现方法
    1. Engflow Remote Execution:采用类似 Bazel 的 RE 协议。
    2. tipi 单实例策略:提供高效缓存和远程执行支持。
  • 结论:CMake 可以实现 100% 的分布式构建能力。
  • 数学表示
    • 假设任务集合 T = t 1 , t 2 , . . . , t n T = {t_1, t_2, ..., t_n} T=t1,t2,...,tn
    • 本地执行时间:
      T local = ∑ i = 1 n BuildTime ( t i ) T_{\text{local}} = \sum_{i=1}^{n} \text{BuildTime}(t_i) Tlocal=i=1nBuildTime(ti)
  • 分布式执行时间(并行执行 + 缓存命中率 h h h):
    T distributed = ∑ i = 1 n ( 1 − h i ) BuildTime ( t i ) P i T_{\text{distributed}} = \sum_{i=1}^{n} (1-h_i) \frac{\text{BuildTime}(t_i)}{P_i} Tdistributed=i=1n(1hi)PiBuildTime(ti)
    其中:
  • h i h_i hi = 任务 t i t_i ti 的缓存命中率
  • P i P_i Pi = 分配给任务 t i t_i ti 的并行处理核心数或节点数
    说明:缓存命中率越高、分配节点越多,总构建时间越短。

5⃣ 总结理解

  1. CMake 优势
    • 无锁定,灵活可控。
    • 支持源码重建。
    • 高效安全的构建缓存。
    • 支持自动 SBOM 生成。
  2. SBOM 作用
    • 软件供应链安全。
    • SPDX 格式 + NTIA 验证。
    • 可自动生成和汇总项目及依赖的构建信息。
  3. 分布式/远程构建
    • 提升构建速度。
    • 支持缓存命中机制。
    • CMake + Engflow + tipi 可实现类似 Bazel 的高级特性。
  4. 公式理解
  • 分布式构建时间:
    T distributed = ∑ i = 1 n ( 1 − h i ) BuildTime ( t i ) P i T_{\text{distributed}} = \sum_{i=1}^{n} (1-h_i) \frac{\text{BuildTime}(t_i)}{P_i} Tdistributed=i=1n(1hi)PiBuildTime(ti)
  • 缓存命中可显著降低 T distributed T_{\text{distributed}} Tdistributed
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐