资讯动态

Perfetto 的 LLVM Sanitizers 冒烟测试:构建配置自检与内存安全验证

发布时间:2026/9/18 4:54:58 来源:尧图企业网站定制
Perfetto 的 LLVM Sanitizers 冒烟测试构建配置自检与内存安全验证【免费下载链接】perfettoProduction-grade client-side tracing, profiling, and analysis for complex software systems.项目地址: https://gitcode.com/GitHub_Trending/pe/perfetto导读本文聚焦 Perfetto 仓库中专门用于验证 LLVM 内存/未定义行为检测器是否真正生效的冒烟测试smoke tests模块。它的核心目标不是发现业务代码缺陷而是自检构建系统本身——确保is_asan、is_lsan、is_msan、is_tsan、is_ubsan这些 GN 构建开关真的把对应 Sanitizer 注入了编译与链接流程从而能抓住违规而不是无条件通过。读完本文你将掌握这五个 Sanitizer 开关的语义与平台约束、测试用例的断言机制EXPECT_DEATH与正则匹配、底层编译/链接标志的生成逻辑以及如何在本地构建并运行这套自检。为什么需要冒烟测试而非普通单测test/sanitizers/README.md 开宗明义地说明了该二进制存在的唯一目的验证各种 Sanitizer 构建配置is_asan、is_lsan、is_msan、is_tsan、is_ubsan确实生效并能发现违规而不是在构建配置被弄坏时无条件成功。这段话点出了一个容易被忽视的工程风险Sanitizer 属于编译器插桩能力它的有效性完全依赖构建参数是否被正确传递。一旦出现诸如 GN 配置拼写错误、-fsanitize标志丢失、链接时未带上对应 runtime 库、或者 ignorelist 误伤等问题程序依然能正常编译运行所有被测代码也依然能通过——因为根本没有插桩在检测。此时普通单元测试会给出全绿的假象而真实的越界、泄漏、未初始化读取依然在静默发生。冒烟测试的意义正在于用一组必然触发违规的微型用例反向验证插桩链路是否存活。如果 Sanitizer 正常工作用例应当被检测器拦截并崩溃如果配置坏了用例要么静默通过、要么因预期外的死亡方式而失败——无论哪种结果都暴露了构建配置的问题。五个 Sanitizer 开关与派生参数所有开关定义在 gn/standalone/sanitizers/vars.gni 的declare_args()中参数默认值检测目标is_asanfalseAddress Sanitizer内存错误典型如 use-after-free、越界读写is_lsanfalseLeak Sanitizer内存泄漏is_msanfalseMemory Sanitizer读取未初始化内存is_tsanfalseThread Sanitizer线程竞争等并发错误is_ubsanfalseUndefined Behaviour Sanitizer未定义行为is_fuzzerfalse以 fuzzing 模式编译配合 LibFuzzeruse_sanitizer_configs_without_instrumentationfalse仅设置相关宏、不注入编译标志用于 component build 中对部分组件单独开 Sanitizer在这些原始开关之上vars.gni 派生出using_sanitizer当任一 Sanitizer 或use_libfuzzer开启、且当前 toolchain 是 default toolchain 时为真表示该 toolchain 正在使用 Sanitizer。文件末尾还内置了三条硬性约束assert从构建层面杜绝了不支持的组合开启任何is_*san必须使用 clangis_clangtrue或系统编译器is_msan仅支持 Linuxis_tsan仅支持 Linux 与 macOSis_fuzzer必须搭配use_libfuzzer或显式指定link_fuzzer链接标志。这些约束也解释了 tools/setup_all_configs.py 中预置的配置矩阵linux_asan、linux_lsan、linux_msan、linux_tsan、linux_ubsan、linux_fuzzeris_fuzzertrueis_asantruemacOS 侧有mac_asan、mac_tsan、mac_ubsanAndroid 侧有android_asan、android_lsan——均以is_clangtrue与is_debugfalse为基础组合。测试用例逐项剖析冒烟测试的全部用例集中在 test/sanitizers/sanitizers_unittest.cc每个用例都用#if defined(...)按构建时注入的宏隔离编译实现开了哪个 Sanitizer 就测哪个。ASANuse-after-freeL29-L43#if defined(ADDRESS_SANITIZER) TEST(SanitizerTests, ASAN_UserAfterFree) { EXPECT_DEATH( { void* alloc malloc(16); volatile char* mem reinterpret_castvolatile char*(alloc); mem[0] 1; mem[15] 1; free(alloc); mem[0] 2; // 释放后写入 abort(); }, AddressSanitizer:.*heap-use-after-free); } #endif // ADDRESS_SANITIZER用例先在堆上分配 16 字节并写入首尾两个字节确保整块内存被触及、避免优化器裁剪free之后再次向已释放的内存写入触发典型的 heap-use-after-free。EXPECT_DEATH期望子进程以匹配正则AddressSanitizer:.*heap-use-after-free的方式死亡。这里的关键细节是结尾的abort()它是一个兜底保险。如果 ASan 插桩正常子进程会先被 Sanitizer 报错终止匹配到预期消息如果插桩失效构建配置坏了代码会继续执行到abort()子进程虽然也死亡但死亡原因不匹配正则测试照旧失败——从而避免误通过。MSAN未初始化内存读取L45-L57#if defined(MEMORY_SANITIZER) TEST(SanitizerTests, MSAN_UninitializedMemory) { EXPECT_DEATH( { std::unique_ptrint mem(new int[10]); volatile int* x reinterpret_castvolatile int*(mem.get()); if (x[rand() % 10] 42) // 读取未初始化值 printf(\n); abort(); }, MemorySanitizer:.*use-of-uninitialized-value); } #endifnew int[10]分配后未初始化x[rand() % 10]读取其值参与比较构成 use-of-uninitialized-value。rand()的随机性让编译器无法在编译期判定分支恒假从而保留这次未初始化读取。预期死亡消息为MemorySanitizer:.*use-of-uninitialized-value。MSan 在仓库中仅支持 Linux且要求运行时库把fstat/readlink等系统调用结果正确标记为已初始化这正是 ignorelist.txt 中针对第三方代码SQLite 的sqlite3.c、libprotobuf 的OpenDiskFile、libunwindstack 的MemoryFileAtOffset::Init做豁免的原因。LSAN两路泄漏检测L59-L86// b/141460117: Leak sanitizer tests dont work in debug builds. #if defined(LEAK_SANITIZER) defined(NDEBUG) TEST(SanitizerTests, LSAN_LeakMalloc) { EXPECT_DEATH( { void* alloc malloc(16); reinterpret_castvolatile char*(alloc)[0] 1; alloc malloc(16); // 第一块内存泄漏 reinterpret_castvolatile char*(alloc)[0] 2; free(alloc); exit(0); // LSan runs on the atexit handler. }, LeakSanitizer:.*detected memory leaks); } TEST(SanitizerTests, LSAN_LeakCppNew) { EXPECT_DEATH( { std::unique_ptrint alloc(new int(1)); *reinterpret_castvolatile int*(alloc.get()) 1; alloc.release(); // 主动放弃所有权泄漏 alloc.reset(new int(2)); *reinterpret_castvolatile int*(alloc.get()) 2; exit(0); // LSan runs on the atexit handler. }, LeakSanitizer:.*detected memory leaks); } #endif // LEAK_SANITIZER defined(NDEBUG)两个用例分别覆盖 C 风格malloc泄漏与 Cnew泄漏并特意用exit(0)触发进程正常退出路径——因为LSan 的泄漏报告挂在atexit处理器上只有走到退出流程才会执行扫描。源码注释还记录了一个已知工程约束b/141460117Leak Sanitizer 测试在 debug 构建下不工作因此整个分支被defined(NDEBUG)门控只会在 releaseis_debugfalse构建中编译运行。UBSAN除零与移位越界L88-L111#if defined(UNDEFINED_SANITIZER) TEST(SanitizerTests, UBSAN_DivisionByZero) { EXPECT_DEATH( { volatile float div 1; float res 3 / (div - 1); // 除数为 0 ASSERT_GT(res, -1.0f); // 仅为使用 res 防优化 abort(); }, error:.*division by zero); } TEST(SanitizerTests, UBSAN_ShiftExponent) { EXPECT_DEATH( { volatile uint32_t n 32; volatile uint32_t shift 31; uint64_t res n (shift 3); // 移位量 34 ≥ 32 ASSERT_NE(1u, res); // 仅为使用 res 防优化 abort(); }, error:.*shift exponent); } #endif // UNDEFINED_SANITIZERdiv - 1因volatile无法被常量折叠运行时得到 0 做除数shift 3 34超过uint32_t的位宽 32构成非法移位。两处都靠volatile与用一下结果的断言阻止编译器在编译期消除违规或推导结果。预期死亡消息匹配error:.*division by zero与error:.*shift exponent——这两条子检测对应编译标志里的-fsanitizefloat-divide-by-zero、-fsanitizeinteger-divide-by-zero与-fsanitizeshift-exponent。兜底用例未配置任何 SanitizerL113-L119当五个defined(...)宏全部不满足即构建时一个 Sanitizer 都没开时编译一个仅打印No sanitizers configured!的普通用例。它让该目标在无 Sanitizer 的常规构建中也能编译、链接并顺利通过避免冒烟测试自身成为构建系统的负向依赖。构建目标与测试依赖test/sanitizers/BUILD.gn 把测试封装为一个source_set(unittests)source_set(unittests) { testonly true deps [ ../../gn:default_deps, ../../gn:gtest_and_gmock, ] sources [ sanitizers_unittest.cc ] }testonly true声明该目标仅用于测试禁止被生产目标依赖../../gn:default_deps引入 Perfetto 统一的编译配置其中就包含 Sanitizer 相关的 cflags/defines 注入../../gn:gtest_and_gmock引入 test/gtest_and_gmock.h 所在的 gtest/gmock 测试框架测试源码顶部也直接#include test/gtest_and_gmock.h。由于开哪个 Sanitizer 就定义哪个宏是在公共配置层完成的见下文这个 source_set 本身无需任何条件分支源码里的#if defined(...)便足以在编译期筛选出对应用例。编译与链接标志的生成原理Sanitizer 的插桩标志在 gn/standalone/sanitizers/BUILD.gn 中统一生成其中sanitizers_cflags配置L43-L112把每个开关映射为具体-fsanitize子项与宏定义GN 开关编译标志注入宏is_asan-fsanitizeaddressADDRESS_SANITIZERis_lsan-fsanitizeleakLEAK_SANITIZERis_tsan-fsanitizethreadTHREAD_SANITIZER、DYNAMIC_ANNOTATIONS_EXTERNAL_IMPL1is_msan-fsanitizememory、-fsanitize-memory-track-origins2MEMORY_SANITIZERis_ubsanbounds、float-divide-by-zero、integer-divide-by-zero、null、object-size、return、returns-nonnull-attribute、shift-exponent、signed-integer-overflow、unreachable、vla-boundUNDEFINED_SANITIZER所有启用 Sanitizer 的编译单元还会统一加上-fno-omit-frame-pointer保证回溯栈完整与-fsanitize-ignorelistpath读取豁免名单。is_fuzzer额外注入-fsanitizefuzzer-no-link与FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION与 OSS-Fuzz 同名宏保持一致并在此前加-mllvm -asan-use-private-alias以配合 ASan。链接侧sanitizers_ldflags配置L120-L147有几个值得注意的工程细节LSan 的链接标志是-fsanitizeaddress。源码注释专门说明这不是复制粘贴错误LSan 的运行时库已并入 ASan 运行时因此必须编译用-fsanitizeleak、链接用-fsanitizeaddressUBSan 链接时使用-fsanitizeundefined -fsanitizevptr比编译期子项集合更宽macOS 上通过sanitizer_options_link_helper配置追加-Wl,-U,_sanitizer_options_link_helper允许链接期解析该符号。运行时库的选择在 gn/standalone/sanitizers/sanitizers.gni 中按平台决定Linux 使用静态归档libclang_rt.{asan,tsan,ubsan_standalone}-x86_64.asanitizer_lib_dir_is_statictrue此时目录参数无意义Android 使用clang_rt.{asan,tsan,ubsan_standalone}-arch-android动态库并由copy_sanitizer_lib目标把.so拷贝到root_out_dir/sanitizer_libs/供打包macOS 使用clang_rt.*_osx_dynamic。ignorelist 与 ccache 缓存失效的巧思ignorelist.txt 记录了三类已知误报的第三方代码豁免新版 glibc 下 MSan 的readlink/fstat拦截器不会正确标记输出缓冲区为已初始化导致消费方误报 use-of-uninitialized-value——SQLite 的 VFS 层unixFullPathname、verifyDbFile、fileHasMoved等因此整文件豁免整个sqlite3.camalgamationlibprotobuf 的DiskSourceTree::OpenDiskFile与 libunwindstack 的MemoryFileAtOffset::Init也因同样的fstat()问题被按函数豁免。BUILD.gn 里还有一个针对构建缓存的细节-fsanitize-ignorelist只把路径暴露在命令行上而 ccache 按参数与源文件哈希缓存不会感知该文件内容变化可能长期供应过期对象。解决方式是调用 gn/standalone/hash_file.py 计算 ignorelist 的内容哈希折叠进PERFETTO_SANITIZERS_IGNORELIST_HASH这个-D宏——文件一改动编译命令行随之变化ccache 缓存自然失效。同时ignorelist_copy目标把该文件拷贝进构建目录让任何编辑都能触发依赖重建。构建与运行方式仓库使用 GN Ninja 构建入口脚本为 tools/gn 与 tools/ninja。以 Linux ASan 为例的典型流程生成构建目录并开启 Sanitizer 配置例如tools/gn gen out/linux_asan --argsis_clangtrue is_debugfalse is_asantruetools/setup_all_configs.py亦提供了linux_asan、linux_lsan、linux_msan、linux_tsan、linux_ubsan、linux_fuzzer、mac_asan、mac_tsan、mac_ubsan、android_asan、android_lsan等一键配置名见 tools/setup_all_configs.py构建冒烟测试目标tools/ninja -C out/linux_asan test/sanitizers:unittests运行该测试二进制若构建插桩链路完好ASAN_UserAfterFree等用例将以匹配正则的方式按预期死亡gtest 将其判定为通过。需要注意的适用前提使用任何 Sanitizer 都必须用 clangis_clangtrueMSan 仅限 LinuxTSan 仅限 Linux/macOS这是构建系统assert强制保证的LSan 冒烟用例只在 releaseNDEBUG构建下编译b/141460117若同时未开启任何 Sanitizer目标会退化为打印No sanitizers configured!的占位用例。小结Perfetto 的这套 sanitizer 冒烟测试是一个精悍而完整的构建配置健康检查范例通过EXPECT_DEATH 正则断言让必然违规的微型程序反向验证 ASan/LSan/MSan/TSan/UBSan 插桩是否真实生效用abort()兜底杜绝无条件通过的假阴性用宏门控让用例随构建开关自动启停再配合 ignorelist 豁免与 ccache 内容哈希把检测器本身没被打开这类隐蔽问题在 CI 早期暴露出来。对于任何重度依赖 Sanitizer 的大型 C 项目这套设计都值得作为参照先证明检测器活着再谈用它抓 bug。相关文件索引test/sanitizers/README.md冒烟测试定位说明test/sanitizers/sanitizers_unittest.cc全部冒烟测试用例test/sanitizers/BUILD.gn测试目标与依赖gn/standalone/sanitizers/vars.gni开关参数与平台约束gn/standalone/sanitizers/BUILD.gn编译/链接标志生成与 ignorelist 机制gn/standalone/sanitizers/sanitizers.gni各平台 Sanitizer 运行时库解析gn/standalone/sanitizers/ignorelist.txt第三方代码误报豁免名单tools/setup_all_configs.py各平台/各 Sanitizer 的预置构建配置【免费下载链接】perfettoProduction-grade client-side tracing, profiling, and analysis for complex software systems.项目地址: https://gitcode.com/GitHub_Trending/pe/perfetto创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价