抽象诠释是那种会让工程师关掉标签页的术语。它听起来像是需要花一学期格论才能理解的东西。大多数开发者都以为它活在研究论文里,而不是在拉取请求中。

这个假设很昂贵。抽象诠释只是一种在不执行代码的情况下证明代码性质的方法。建立在它之上的工具可以捕获类型检查器和代码检查工具错过的空指针解引用、内存泄漏和竞态条件。好消息是:你不需要理解伽罗瓦连接就能使用它。你需要一个能工作的CI配置和大约二十分钟。

抽象诠释实际做什么

其核心,抽象诠释是一种自动化的证明技术。它运行你的程序,但使用的是近似值而不是真实值。

考虑一个变量x。在正常执行中,x可能保存42。在抽象诠释中,x可能保存”正整数”。分析跟踪这些抽象值通过每一个可能的代码路径。如果它能证明没有任何路径会导致空指针解引用,你就是安全的。如果它发现某个路径上x在解引用位置可能为空,它就会报告一个潜在缺陷。

神奇之处在于这对循环和条件语句也有效。分析器在抽象状态上计算不动点,从而可以对无界迭代进行推理,而无需实际永远迭代下去。这就是抽象诠释与在循环上挣扎的更简单符号执行工具的区别。

Facebook的Infer是使用这项技术最易于上手的生产工具。它通过将代码编译为中间表示,并在每个函数上运行组合式抽象诠释来分析Java、C、C++和Objective-C。Infer按函数缓存结果,因此增量构建很快。这正是让它在CI中可行的秘诀。

为什么你的代码检查工具不够

代码检查工具看语法。类型检查器看类型。抽象诠释看跨路径的行为。

代码检查工具可以标记你忘了检查空值。类型检查器可以强制函数返回Optional<T>。但两者都无法可靠地捕获你在第47行解引用了一个指针,而在一系列复杂的分支之后,某条路径让它未初始化。抽象诠释跟踪该指针通过每个分支和合并点的可能状态。

权衡是噪音。抽象诠释会产生误报。它可能报告一个你的业务逻辑保证永远不会发生的空指针解引用。分析器不知道你的不变量。它只知道代码字面上允许什么。

Infer的默认检查器经过调整,可以将大多数代码库的误报率保持在10-15%左右。这比类型检查器高,但它发现的缺陷往往是那些从代码审查和测试中溜走的。

将Infer添加到CI管道

你不需要从源代码构建Infer。Facebook发布了Docker镜像。以下是一个可以工作的GitHub Actions工作流,用于分析Java项目:

# .github/workflows/infer.yml
name: Abstract Interpretation

on: [pull_request]

jobs:
  infer:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run Infer
        uses: docker://ghcr.io/facebook/infer:main
        with:
          args: >
            infer run
            --make-command "mvn compile"
            --
            mvn compile

      - name: Upload report
        uses: actions/upload-artifact@v4
        with:
          name: infer-report
          path: infer-out/report.json

对于Node.js或Python项目,替换构建命令。Infer不原生分析JavaScript或Python,但你可以在那些项目通常依赖的C/C++扩展上运行它。如果你处于纯托管语言环境中,你仍然可以从CodeQL或SonarQube等工具获得类似的路径敏感分析,尽管它们底层引擎不同。

对于C或C++项目,设置更简单:

# .github/workflows/infer-cpp.yml
name: Infer C++ Analysis

on: [pull_request]

jobs:
  infer:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build with Infer
        uses: docker://ghcr.io/facebook/infer:main
        with:
          args: >
            infer run
            --make-command "make"
            --
            make

infer run --make-command模式在正常构建过程中拦截编译器调用。Infer提取中间表示,分析它,并将结果写入infer-out/。你的实际构建产物不受影响。

读取输出和调整误报

Infer将发现结果输出到infer-out/report.json和可人工阅读的infer-out/report.txt。一个典型的发现如下所示:

src/parser.c:142: error: NULL_DEREFERENCE
  pointer `node` last assigned on line 138 could be null and is dereferenced at line 142, column 5

消息告诉你变量、赋值位置和解引用发生的位置。你可以在编辑器中追踪路径。

如果Infer太吵,你可以抑制特定检查器或注释代码以跳过分析:

// src/parser.c
// infer-ignore: the parent check guarantees node is non-null here
node->value = parsed;

或全局禁用特定检查器:

infer run --make-command "make" --no-bufferoverrun --

缓冲区溢出检查器在具有复杂指针运算的代码上特别容易产生误报。我通常在遗留C代码库上禁用它,保持空指针解引用和内存泄漏检查器活动。这两个发现的真正缺陷率证明了审查时间的价值。

构建时间权衡

抽象诠释不是免费的。在中等规模C++项目上的完整Infer运行可能比正常构建长2-4倍。增量分析有帮助:在后续运行中,Infer只重新分析已更改的函数及其依赖项。实际上,这意味着10分钟的构建在干净的CI运行中可能变成15-20分钟,但在增量运行中只需3-5分钟。

如果你的CI预算紧张,在拉取请求上运行Infer,而不是在每次推送到main时。或者每晚运行。它发现的缺陷通常值得延迟,但正确的频率取决于你的团队对CI时间的容忍度。

另一个选择是在推送前本地运行Infer。相同的Docker镜像适用于任何安装了Docker的机器:

docker run --rm -v $(pwd):/workspace -w /workspace \
  ghcr.io/facebook/infer:main \
  infer run --make-command "make" --

Infer无法捕获什么

Infer是组合式的。它隔离分析函数,并使用摘要来建模调用者和被调用者。这让它可扩展,但意味着需要分析完整调用图的跨函数、路径敏感缺陷可能会漏掉。

它也不会发现逻辑缺陷。如果你的代码安全地解引用了一个指针但使用了错误的值,Infer保持沉默。它是一个安全检查器,不是正确性神谕。

并发缺陷是有限的。Infer有一个竞态条件检查器,但它是实验性的,产生的误报足以让大多数团队关闭它。

下一步做什么

从小处开始。选择一个使用编译语言的项目,添加上面的GitHub Actions工作流。让它在接下来的几个拉取请求上运行。与团队一起审查发现结果,并为噪音构建抑制列表。

一周后,你会感觉到信号是否值得CI时间。根据我的经验,在现有C或Java代码库上的第一次运行总会发现至少一个代码审查遗漏的空指针解引用。这通常足以证明保留它是值得的。

如果你想深入了解,Infer文档涵盖了用OCaml编写自定义检查器。这就是博士学位派上用场的地方。对于其他一切,默认检查器和Docker镜像就足够了。

FAQ

抽象诠释简单来说是什么? 它是一种静态分析技术,近似你的程序如何行为,以证明诸如”此指针永远不为空”之类的属性,而无需实际执行代码。

Infer免费使用吗? 是的。Infer在MIT许可证下开源,由Meta维护。

Infer与SonarQube相比如何? SonarQube根据语言混合使用模式匹配、污染分析和一些更深入的分析。Infer专门建立在抽象诠释之上,在C、C++、Java和Objective-C方面以SonarQube通常不具备的方式进行路径敏感分析。

我可以在JavaScript或Python上运行Infer吗? 不能直接运行。Infer分析编译语言。对于JavaScript和Python,考虑CodeQL或具有严格规则的类型感知代码检查工具如ESLint或Pyright。

Infer会显著降低CI速度吗? 完整分析需要2-4倍构建时间。拉取请求上的增量分析快得多,通常只增加几分钟。