第1章:静态分析工具定制与扩展——编写自定义检查器
大家好,我是你们这堂课的讲师。做了十几年嵌入式软件,我越来越觉得静态分析不是「跑个工具就完事」的活儿。你想想看,每个团队都有自己的编码规范、历史遗留问题、甚至行业特有的坑。通用工具能覆盖80%,剩下那20%的硬骨头,就得靠我们自己动手了。
这一章,我们就聊聊怎么给Cppcheck和Clang-tidy写插件,怎么把规则脚本化,以及怎么做领域特定的分析。说白了,就是让工具真正听懂你的项目语言。
为什么需要自定义检查器?
我遇到过不少团队,买了商业工具,结果误报率高达40%。为什么?因为工具不懂你的业务逻辑。比如一个嵌入式项目里,某个寄存器写操作必须关中断,这种规则通用工具根本不知道。
自定义检查器能解决三个核心问题:
- 精准捕获领域错误——比如我们团队规定「所有malloc必须配对free,且不能跨模块释放」
- 降低误报率——只检查你关心的模式,别让工程师在噪音里找信号
- 固化团队经验——把踩过的坑写成规则,新人一跑就自动避坑
核心观点:静态分析工具是骨架,自定义规则才是血肉。没有定制化的分析,你只是在用别人的尺子量自己的脚。
Cppcheck插件开发实战
Cppcheck的插件机制其实挺轻量的。它提供了一套Token接口,你只需要继承一个基类,重写几个检查函数就行。我个人习惯用Cppcheck 2.10以上版本,API更稳定。
来看一个实际例子。假设我们要检查「禁止在中断服务程序里调用printf」:
// my_checker.cpp
#include "check.h"
#include "token.h"
class CheckISRPrintf : public Check {
public:
CheckISRPrintf() : Check(myname()) {}
void runChecks(const Tokenizer *tokenizer,
const Settings *settings,
ErrorLogger *errorLogger) override {
// 遍历所有token
for (const Token *tok = tokenizer->tokens(); tok; tok = tok->next()) {
// 检测到ISR函数定义
if (tok->str() == "__irq" || tok->str() == "interrupt") {
// 检查函数体内是否有printf调用
const Token *body = tok->next()->link();
if (body && body->scope() == Scope::eScopeGlobal) {
for (const Token *t = body; t != tok->next(); t = t->next()) {
if (t->str() == "printf") {
reportError(t, Severity::error,
"isrPrintf", "ISR中禁止调用printf");
}
}
}
}
}
}
static std::string myname() {
return "CheckISRPrintf";
}
};
小技巧:写Cppcheck插件时,先用--dump选项导出Token树,看看你的目标代码长什么样。我曾经花了两小时调试一个规则,最后发现是Token遍历顺序搞反了。
Clang-tidy自定义检查——更现代的路子
Clang-tidy的检查器基于AST(抽象语法树),比Cppcheck的Token级别更强大。你可以精确匹配类型、表达式、甚至控制流。不过门槛也高一点,得懂一点LLVM的API。
我建议新手从clang-tidy-plugin模板开始。下面是一个检查「禁止使用全局变量」的示例:
// GlobalVarCheck.cpp
#include "../ClangTidy.h"
#include "../ClangTidyModule.h"
#include "../ClangTidyModuleRegistry.h"
namespace clang {
namespace tidy {
namespace custom {
class GlobalVarCheck : public ClangTidyCheck {
public:
GlobalVarCheck(StringRef Name, ClangTidyContext *Context)
: ClangTidyCheck(Name, Context) {}
void registerMatchers(MatchFinder *Finder) override {
// 匹配全局变量声明
Finder->addMatcher(
varDecl(hasGlobalStorage(), unless(isConstexpr())).bind("gvar"),
this);
}
void check(const MatchFinder::MatchResult &Result) override {
const auto *VD = Result.Nodes.getNodeAs<VarDecl>("gvar");
if (VD && !VD->isStaticLocal()) {
diag(VD->getLocation(), "禁止使用非constexpr全局变量: %0")
<< VD->getName();
}
}
};
// 注册插件
class CustomModule : public ClangTidyModule {
public:
void addCheckFactories(ClangTidyCheckFactories &CheckFactories) override {
CheckFactories.registerCheck<GlobalVarCheck>("custom-global-var");
}
};
} // namespace custom
} // namespace tidy
} // namespace clang
注意:Clang-tidy的AST匹配器语法很灵活,但也容易写出性能很差的规则。我曾经写过一个嵌套匹配器,结果分析一个10万行文件跑了3分钟。后来改成单层匹配,秒出结果。记住:匹配器越精确,性能越好。
规则脚本化——让非开发者也参与
不是所有人都能写C++插件。所以我推荐团队里搞一套规则脚本层。比如用Python写一个规则引擎,解析YAML配置文件,然后调用底层工具。
我见过一个做得不错的方案:
- 用YAML定义规则模板(模式、严重级别、建议修复)
- Python脚本读取YAML,生成Cppcheck的suppress列表或Clang-tidy的配置文件
- CI流水线里跑这个脚本,自动适配不同项目的规则集
举个例子,规则文件长这样:
# custom_rules.yaml
rules:
- id: "NO_MAGIC_NUMBER"
pattern: "int x = 42;" # 简单模式匹配
severity: "warning"
message: "避免魔数,请使用宏或const变量"
- id: "CHECK_MUTEX_ORDER"
pattern: "lock(A); lock(B);" # 检测锁顺序
severity: "error"
message: "锁顺序不一致可能导致死锁"
然后脚本解析这个YAML,生成对应的检查命令。这样测试人员、架构师都能参与规则维护,不用碰C++代码。
领域特定分析——从通用到专用
通用工具检查的是语法和常见错误。但嵌入式、金融、通信这些领域,有自己独特的「潜规则」。比如:
- 嵌入式领域:检查中断嵌套深度、堆栈使用量、volatile使用是否正确
- 安全领域:检查敏感数据是否被清零、指针是否被安全擦除
- 通信协议领域:检查报文长度校验、序列号连续性
我参与过一个汽车电子项目,要求所有函数调用必须记录调用链,用于功能安全追溯。通用工具做不到,我们就写了一个Clang-tidy插件,在每次函数调用时插入一个AST visitor,把调用关系导出成JSON。然后另一个脚本分析JSON,检查是否有未授权的跨域调用。
经验之谈:领域特定分析最值钱的地方,不是技术有多难,而是你懂这个领域的「坑」。我见过一个团队花三个月写了一个通用检查器,结果发现商业工具已经覆盖了。后来他们聚焦在「内存池泄漏检测」这个细分领域,两周就搞定了,效果还特别好。
知识体系总览
下面这张图,是我梳理的本章知识结构。你可以看到,从底层工具到上层业务,每一层都有定制化的空间。
避坑指南
最后,分享几个我踩过的坑:
- 不要一开始就写复杂规则。先写一个最简单的「打印所有函数名」,确认插件能跑通,再逐步加逻辑。我曾经直接写了一个200行的检查器,结果编译通过但运行时崩溃,调试了两天才发现是Token指针越界。
- 规则要可测试。给每个自定义检查器写单元测试。Cppcheck有现成的测试框架,Clang-tidy也有lit测试。没有测试的规则,就是定时炸弹。
- 性能监控不能忘。在CI里加上规则执行时间的告警。如果某个规则导致构建时间翻倍,团队会直接禁用整个工具。我见过一个团队因为一个慢规则,把整个静态分析给关了。
我的建议:从团队最痛的一个问题入手。比如你们经常因为「忘记释放内存」出bug,那就先写一个内存泄漏检查器。别想着一步到位,静态分析工具的定制化,是持续迭代的过程。
好了,这一章就到这里。记住:工具是死的,规则是活的。真正让静态分析发挥价值的,是你对项目的理解,以及把理解变成规则的能力。
公众号:蓝海资料掘金营,微信deep3321