第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配置文件,然后调用底层工具。

我见过一个做得不错的方案:

  1. 用YAML定义规则模板(模式、严重级别、建议修复)
  2. Python脚本读取YAML,生成Cppcheck的suppress列表或Clang-tidy的配置文件
  3. 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,检查是否有未授权的跨域调用。

经验之谈:领域特定分析最值钱的地方,不是技术有多难,而是你懂这个领域的「坑」。我见过一个团队花三个月写了一个通用检查器,结果发现商业工具已经覆盖了。后来他们聚焦在「内存池泄漏检测」这个细分领域,两周就搞定了,效果还特别好。

知识体系总览

下面这张图,是我梳理的本章知识结构。你可以看到,从底层工具到上层业务,每一层都有定制化的空间。

静态分析工具定制与扩展知识体系 Cppcheck插件 Token级别检查 Clang-tidy自定义检查 AST级别精确匹配 规则脚本化 YAML/Python驱动 核心能力:精准捕获领域错误 · 降低误报率 · 固化团队经验 嵌入式领域 中断/堆栈/volatile 安全领域 敏感数据/指针安全 通信协议领域 报文校验/序列号 从通用工具到领域特定分析,每一层都有定制化的空间

避坑指南

最后,分享几个我踩过的坑:

  • 不要一开始就写复杂规则。先写一个最简单的「打印所有函数名」,确认插件能跑通,再逐步加逻辑。我曾经直接写了一个200行的检查器,结果编译通过但运行时崩溃,调试了两天才发现是Token指针越界。
  • 规则要可测试。给每个自定义检查器写单元测试。Cppcheck有现成的测试框架,Clang-tidy也有lit测试。没有测试的规则,就是定时炸弹。
  • 性能监控不能忘。在CI里加上规则执行时间的告警。如果某个规则导致构建时间翻倍,团队会直接禁用整个工具。我见过一个团队因为一个慢规则,把整个静态分析给关了。

我的建议:从团队最痛的一个问题入手。比如你们经常因为「忘记释放内存」出bug,那就先写一个内存泄漏检查器。别想着一步到位,静态分析工具的定制化,是持续迭代的过程。

好了,这一章就到这里。记住:工具是死的,规则是活的。真正让静态分析发挥价值的,是你对项目的理解,以及把理解变成规则的能力。


公众号:蓝海资料掘金营,微信deep3321