CppCon 2023 学习: CMake Successor Build Systems
并行性Job Pool 可以让 CMake/Ninja 精细控制不同任务的并行度。对 CPU 密集型工具(如 Sanitizers)很重要。可复现性Make/Ninja 基本没有保障。Meson、Gradle 提供工具链配置。Bazel 最强,可将工具链和构建配置完全视作输入,实现可复现构建。数学模型minminOutputfInputsToolchainsConfigOutputfInput
什么是 “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 p∈valid 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| 0≤i<∣A∣
措施:
- 使用
std::span - 使用
at()或带边界检查的工具 - 避免原始指针算术
5. 初始化安全(Initialization Safety)
定义:
使用对象之前,它必须已经初始化,并且初始化完整、正确。
错误示例:
int x; // 未初始化
int y = x + 1; // UB
解决:
- 构造函数
= defaultstd::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 (T1↔T2)∧(write∨read-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-cast、cppcoreguidelines-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 overflow⟺a+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>INTmax−b⇒overflow 若 b<0:a<INTmin−b⇒overflow
另一种等价的位论断(常用在实现中):若 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). overflow⟺sign(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)
- UBSan:
编程实践(避免 UB)
- 使用带检测的内建函数(compiler intrinsics):
- GCC/Clang 提供
__builtin_add_overflow,__builtin_mul_overflow等:
- GCC/Clang 提供
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;
}
- 使用库:许多项目采用“Checked Integer”库(例如 Chromium 的
base::checked_cast/CheckedNumeric,或微软 / Boost 的类似工具),用来在算术链路中传播是否发生了溢出。 - 不靠编译器以外的假设:不要因为在某个编译器/架构上结果“看起来正确”就假设这在所有实现上都安全;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 的最佳实践(清单)
- 在
.clang-tidy中开启cppcoreguidelines-*(并对owning-memory、pro-bounds-*、pro-type-reinterpret-cast保持严格)。(clang.llvm.org) - 在 CI(Debug)matrix 中加入一项:ASan + UBSan(
-fsanitize=address,undefined,integer),另一项:TSan(若并发相关)。(clang.llvm.org) - 对关键路径和库采用更严格的审查(例如强制
__builtin_*_overflow或 CheckedInt)。 - 在 PR 流程中把 clang-tidy 输出作为必须修复的错误(或放入 review checklist)。
- 对第三方二进制或源码开启 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)
如果你愿意,我可以 立刻为你做三件可执行的事情(任选其一或多个),并在当前回复里把结果给出:
- 把上面的 CMake +
.clang-tidy+ sanitizer 建议,生成一个完整可拷贝的CMakeLists.txt(含 Debug/Release 区分)与.clang-tidy文件。 - 为 Memory Safety 与 Arithmetic Safety 各生成 6–8 个“常见坑对照示例”(错误代码 + 修复),便于团队培训或入职文档。
- 生成一份可用于 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:不可共存(
thread与address运行时实现冲突)。 - 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,b⊆S.
现实中常用的不兼容对(示例):
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 文件?
原因总结:
- 运行时/链接冲突:某些 sanitizer 需要不同的 runtime 实现或特殊的库(如 MSan)。这些实现互相冲突,不能链接在同一二进制中。
- 编译/链接选项不同:不同 sanitizer 可能需要不同的编译和链接选项(例如 MSan 需要 instrumented libc)。
- 调试与性能考虑:每次构建的运行时开销很大,测试应独立运行以节省调试复杂度与资源。
- 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 自定义环境)能得到更可读的报错与可回放的运行环境。
- MSan 也有一些选项,且 MSan 需要运行时链接到 instrumented runtime。
七、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 以避免不兼容问题。
八、实战注意事项与陷阱提醒(要点清单)
- 别把 ASan 和 TSan、MSan 混到一个构建里。 若需要同时运行多类检查,请分开构建与运行。
- MSan 的使用门槛最高:需要 instrumented C/C++ runtime(libc、libstdc++),否则会产生大量误报或漏报。生产环境若不熟悉,慎用。
- TSan 测试通常需要控制并发与稳定的环境(建议在 CI 中固定 CPU 核心数、禁用 hyperthreading 或在容器里做资源限制)。
- ASan/UBSan 可以合并(通常非常有用)。经常把
-fsanitize=address,undefined作为默认 debug sanitizer。 - 为 sanitizer 构建使用 Debug 配置(
-g -O1常见),因为过度优化可能影响可诊断性。 - 收集 sanitizer 输出并存档(特别是在 CI 中),便于事后分析与回放。
- 对第三方库/系统库的限制:某些第三方二进制无法被 instrument(导致 MSan 无法信赖);ASan 对大多数库可用,但 MSan 对 runtime 要求更高。
- 测试并发时请用
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是常见组合;TSan、MSan需要各自独立的构建与专门配置。 - 在 CMake/CI 中采用 独立 toolchain / 独立构建目录 的策略可以让你在同一仓库里并行安全地运行多种 sanitizer 检查。
- 记录并统一
ASAN_OPTIONS/UBSAN_OPTIONS/TSAN_OPTIONS等运行时参数以获得可复现、易分析的报告。
如果你希望,我现在可以:
- 生成 四个完整的 toolchain 文件(
sanitize-address.cmake、sanitize-thread.cmake、sanitize-memory.cmake、sanitize-ub.cmake),并给出一个 完整的顶层 CMakeLists.txt 示例,直接可用。 - 或者给出一个 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
- 限制:
- 并行粒度粗:每个子项目是最小单位。
- 子项目数量有限。
- 默认情况下,并行是关闭的(需要
--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 示例
- 定义全局 Job Pools:
set_property(GLOBAL PROPERTY JOB_POOLS
compile=16
link=1
codegen=16
)
- 为目标分配 Job Pool:
set_property(TARGET atarget PROPERTY JOB_POOL_COMPILE compile)
set_property(TARGET atarget PROPERTY JOB_POOL_LINK link)
- 为自定义命令分配 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)
总结理解
- 并行性:
- Gradle < Make/FASTBuild < Bazel < Ninja。
- Job Pool 可以让 CMake/Ninja 精细控制不同任务的并行度。
- 对 CPU 密集型工具(如 Sanitizers)很重要。
- 可复现性:
- Make/Ninja 基本没有保障。
- Meson、Gradle 提供工具链配置。
- Bazel 最强,可将工具链和构建配置完全视作输入,实现可复现构建。
- 数学模型:
- 并行度:
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__ 宏
- 问题:这些宏会在每次构建时产生不同的值,影响可复现性。
- 解决方案:
- MSVC:
add_link_options("/Brepro") # 启用可复现构建选项
- GCC(通过环境变量):
export SOURCE_DATE_EPOCH=1621012303
- 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__宏包含完整路径,不同机器、不同路径下构建会不同。 - 解决方案:
- 保证源码和构建路径一致。
- 利用 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: ∃fi∈Inputs 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: ∃fi∈Inputs s.t. Digest(fi)=CachedDigest(fi)
优势:即使文件时间戳相同,只要内容改变,也能重新构建。
CMake 的缓存策略
- 默认:使用 Make 或 Ninja 可以得到文件系统缓存。
- 增强:可以使用
tipi(Engflow + Tipi)来实现:- 远程执行(Remote Execution)
- 远程缓存(Remote Caching)
- 这样在 CMake 中也可以实现类似 Bazel 的内容可寻址缓存。
3⃣ 总结
- 可复现性问题:
__DATE__,__TIME__,__FILE__, LTO 随机符号都是主要障碍。- 通过固定宏值、固定路径、SHA1 随机种子可解决。
- 缓存策略:
- Gradle < Make/Ninja < FASTBuild < Bazel
- 文件系统 vs 内容哈希 vs 分布式缓存
- CMake 可以通过 Ninja + tipi 实现高级缓存。
- 数学表示:
- 文件修改判断(时间戳):
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=fi∈Inputs⋁[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=fi∈Inputs⋁[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++)支持较差。
- 主要管理 Java 的
- 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)
- 原理:
- 从 Git 下载依赖。
- 构建并安装到项目中。
- 自动暴露 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)
- 流程:
- 从 Git 克隆 Boost。
- 构建 Boost。
- 在项目中注册 Boost targets,可直接链接。
6⃣ 总结理解
- 缓存策略:
| 缓存层 | 作用 | 特点 |
|---|---|---|
| L1 | 快速恢复构建树 | CPU 临近,高速小容量 |
| L2 | 分布式/并行共享 | 容量大,可减少核心占用 |
- 依赖管理等级:
| Level | 构建系统 | 特性 |
|---|---|---|
| 1 | Make/Ninja/FASTBuild | 无依赖管理,需要手动处理 |
| 2 | Gradle | Java .jar 支持较好,其他语言一般 |
| 3 | Meson/Bazel | 多语言支持,中央化仓库,扩展性好 |
- CMake 优势:
- 提供
FetchContent作为事实标准。 - 可通过自定义宏实现缓存优化、依赖加速。
- 类似于构建缓存模型:
EffectiveBuildTime = BuildTimeWithoutCache × ( 1 − CacheHitRate ) \text{EffectiveBuildTime} = \text{BuildTimeWithoutCache} \times (1 - \text{CacheHitRate}) EffectiveBuildTime=BuildTimeWithoutCache×(1−CacheHitRate) - 通过 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()
- 说明:
sbom_add(TARGET app):为目标 app 生成依赖信息。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 支持分布式构建
- 实现方法:
- Engflow Remote Execution:采用类似 Bazel 的 RE 协议。
- 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=1∑nBuildTime(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=1∑n(1−hi)PiBuildTime(ti)
其中: - h i h_i hi = 任务 t i t_i ti 的缓存命中率
- P i P_i Pi = 分配给任务 t i t_i ti 的并行处理核心数或节点数
说明:缓存命中率越高、分配节点越多,总构建时间越短。
5⃣ 总结理解
- CMake 优势:
- 无锁定,灵活可控。
- 支持源码重建。
- 高效安全的构建缓存。
- 支持自动 SBOM 生成。
- SBOM 作用:
- 软件供应链安全。
- SPDX 格式 + NTIA 验证。
- 可自动生成和汇总项目及依赖的构建信息。
- 分布式/远程构建:
- 提升构建速度。
- 支持缓存命中机制。
- CMake + Engflow + tipi 可实现类似 Bazel 的高级特性。
- 公式理解:
- 分布式构建时间:
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=1∑n(1−hi)PiBuildTime(ti) - 缓存命中可显著降低 T distributed T_{\text{distributed}} Tdistributed。
更多推荐

所有评论(0)