
用 LLVM/Clang LibTooling 编写 C++ 静态分析工具,核心是绕过编译器前端黑盒,直接在 AST 层做语义检查——不是靠正则或字符串匹配,而是真正理解代码结构、类型、作用域和控制流。
一、环境准备:不是装个 clang 就够了
LibTooling 是 LLVM 的 C++ 库接口,必须从源码构建(或使用预编译的 libclang-dev + libc++-dev + llvm-dev 包),仅装 clang 命令行工具无法链接 libTooling。推荐方式:
- Ubuntu/Debian:安装
llvm-dev、libclang-1x-dev(x ≥ 14)、libc++-dev,确保llvm-config和clang++版本一致 -
macOS(Homebrew):
brew install llvm,然后用$(brew --prefix llvm)/bin/clang++编译,链接时指定-lclangTooling -lclangFrontend -lclangSerialization -lclangDriver -lclangAST -lclangParse -lclangSema -lclangLex -lclangBasic -lLLVM - 关键验证:运行
llvm-config --libs tooling frontend ast parse sema看是否输出对应库名
二、最小可运行分析器:从 MatchFinder 入手
别一上来就写 ASTConsumer;用 clang::ast_matchers + MatchFinder 是最稳妥的起点——它把 AST 遍历封装成声明式规则,避免手动递归、生命周期管理出错。
- 定义 matcher:比如找所有调用
std::strcpy的地方:callExpr(callee(functionDecl(hasName("strcpy")))) - 注册回调:继承
MatchCallback,重写run(const MatchResult &),用Result.Nodes.getNodeAs提取节点("expr") - 注意上下文:
run()中拿到的是 const 节点指针,不能改 AST;如需修改(如自动修复),得用ASTRewriter+SourceManager定位位置并生成新文本
三、超越语法匹配:接入语义信息
单纯匹配函数名没意义。真正静态分析要结合类型、值流、控制流:
立即学习“C++免费学习笔记(深入)”;
- 获取参数类型:
call->getArg(0)->getType().getCanonicalType().getAsString(),再用isPointerType()或isConstQualified()判断 - 查变量定义位置:
varDecl(hasInitializer(expr()))+getNodeAs("v")->getInit()->getSourceRange() - 跨函数分析?不行——LibTooling 默认只处理单 TU(translation unit)。想做跨文件检查,必须用 Clangd + IndexStore 或自己实现
ASTUnit缓存 + 符号表重建,复杂度陡增 - 常见坑:
getQualifiedNameAsString()对模板实例化返回空,要用getQualifiedName().getAsString()+ 检查isAnonymousOrUnresolved()
四、集成进开发流程:不只是命令行玩具
让工具被团队用起来,关键在易用性和误报率控制:
- 支持 compile_commands.json:用
CompilationDatabase::loadFromDirectory()自动读取项目编译配置,不用手动传 -I/-D - 增量分析:LibTooling 本身不支持,但可借助
clang::tooling::ClangTool的run()接口配合文件时间戳跳过未改文件 - 输出格式对齐 IDE:打印
file:line:col: severity: message(如main.cpp:42:5: warning: unsafe strcpy usage),VS Code/CLion 可自动高亮 - 抑制误报:支持
// NOLINT或自定义注释(用SourceManager查当前行注释字符串),别硬编码跳过逻辑
基本上就这些。不复杂但容易忽略细节:版本对齐、AST 生命周期、跨 TU 限制、语义查询边界。写一个能发现 std::vector::at() 未检查异常的检查器,200 行以内就能跑通;想覆盖 RAII 泄漏或虚函数调用歧义,就得深入 Sema 和 CFG(Control Flow Graph)了。











