Infer 静态分析器入门实战:Hello World 系列示例与差分分析详解

Infer 静态分析器入门实战:Hello World 系列示例与差分分析详解

【免费下载链接】infer A static analyzer for Java, C, C++, and Objective-C 【免费下载链接】infer 项目地址: https://gitcode.com/gh_mirrors/infer/infer

本篇技术指南基于 Infer 仓库中的 Hello World 系列示例,完整演示如何用 infer run 在 Java、Objective-C、C、Android、iOS 与 Make 六种场景下跑通「捕获 → 分析 → 报告 → 修复 → 复检」的完整闭环,并深入讲解增量构建下 --continue 与 --reactive 两种差分分析方案的取舍。读完本文,你将掌握 Infer 的基本命令格式、各类缺陷报告的解读方法,以及如何在真实构建系统中高效复用分析结果。所有示例均可在仓库 examples 目录中找到,对应文档位于 website/versioned_docs/version-1.3.0/00-hello-world.md。

Infer 的工作方式:捕获与分析两步走

在动手前先理解 Infer 的核心模型:Infer 不是一个「指定文件直接分析」的传统工具,而是捕获(capture)编译命令、再对捕获到的文件进行分析(analyze)。infer run 命令会包装你原本的编译命令(如 javac、clang、gcc、make、gradlew、xcodebuild),拦截实际的编译调用,同时生成分析所需的中间表示。这正是后续「二次运行无结果」现象的根本原因,也是差分分析选项存在的前提(详见下文 Android 示例中的差分分析)。

从仓库源码看,这一模型体现在 infer/src/infer.ml 的主流程中:continue_capture 与 reactive_mode 等配置直接决定了捕获阶段是否延续上一次的捕获结果(见 infer/src/infer.ml 附近对 Config.continue_capture 的使用),我们会在对应小节展开。

Hello world Java

先看最经典的 Java 空指针场景。仓库中的 examples/Hello.java 与文档一致,核心代码如下:

// Hello.java
class Hello {
  int test() {
    String s = null;
    return s.length();
  }
}

在 examples/Hello.java 所在目录执行:

infer run -- javac Hello.java

注意 -- 之后是真正的编译命令,Infer 只是在其外层拦截。运行后应看到如下报告:

Hello.java:5: error: NULL_DEREFERENCE
  object s last assigned on line 4 could be null and is dereferenced at line 5

报告精确指出了缺陷类型(NULL_DEREFERENCE)、位置(文件:行号)以及原因链(s 在第 4 行被赋值为 null,并在第 5 行被解引用)。现在给代码加上空值保护:

  int test() {
    String s = null;
    return s == null ? 0 : s.length();
  }

再次运行 infer run -- javac Hello.java,这次 Infer 报告 No issues found——修复已被验证。这就是 Infer 入门的完整心智模型:让编译器去编译、让 Infer 去发现、修复后复检确认。

Hello world Objective-C

Objective-C 示例展示了 nil 对象访问实例变量(ivar)的场景。仓库源码 examples/Hello.m 如下:

// Hello.m
#import <Foundation/Foundation.h>

@interface Hello: NSObject
@property NSString* s;
@end

@implementation Hello
NSString* m() {
    Hello* hello = nil;
    return hello->_s;
}
@end

在 examples/Hello.m 所在目录执行:

infer run -- clang -c Hello.m

Infer 报告:

Hello.m:10 NULL_DEREFERENCE
  pointer hello last assigned on line 9 could be null and is dereferenced at line 10, column 12

这里的修复方式很能体现 Objective-C 的语义:不直接改判空逻辑,而是改用属性 getter(hello.s)替代直接访问实例变量(hello->_s):

NSString* m() {
    Hello* hello = nil;
    return hello.s;
}

再次运行,Infer 报告 No issues found。之所以两种写法结果不同,是因为直接 -> 解引用必然触碰内存,而消息发送(hello.s 本质是 [hello s])在 Objective-C 中对 nil 是安全的——Infer 忠实反映了这一语言语义。

Hello world C

C 语言示例同样聚焦空指针解引用,仓库源码 examples/hello.c 为:

// hello.c
#include <stdlib.h>

void test() {
  int *s = NULL;
  *s = 42;
}

执行:

infer run -- gcc -c hello.c

报告:

hello.c:5: error: NULL_DEREFERENCE
  pointer s last assigned on line 4 could be null and is dereferenced at line 5, column 10

修复:在解引用前判空:

void test() {
  int *s = NULL;
  if (s != NULL) {
    *s = 42;
  }
}

复检后同样得到 No issues found。

一个重要的等价性说明:gcc 与 clang

文档特别强调:分析 C 文件时,Infer 捕获的是 gcc 命令,但实际会改用 clang 来解析这些文件。因此你可能看到与 gcc 不同的编译器错误或警告。就 Infer 而言,下面两条命令是等价的:

infer run -- gcc -c hello.c
infer run -- clang -c hello.c

这一点在 examples/README.md 中也有印证:「Infer captures the gcc command and runs clang instead to parse C files」。它意味着:你的构建系统用什么编译器都可以,Infer 的分析统一走 clang 前端(Java 走 javac/类字节码路径),从而保证分析语义一致。

Hello world Android

Android 示例是一个真实的 Gradle 工程,位于仓库 examples/android_hello。在动手前需要满足环境前提:安装 Android SDK 22 及更新版本,并确保包含「Android SDK Build-tools」和「Android Support Repository」组件。然后在 examples/android_hello 目录下创建 local.properties 文件,内容为单行配置:

sdk.dir=<location of your Android SDK>

该示例使用 Gradle 构建,但你不需要预先安装 gradle——工程自带的 examples/android_hello/gradlew 包装脚本会自动下载 Gradle 和项目依赖。配置好后运行:

infer run -- ./gradlew build

Infer 会输出一列缺陷,涵盖两类典型问题:

app/src/main/java/infer/inferandroidexample/MainActivity.java:29: error: NULL_DEREFERENCE
  object s last assigned on line 28 could be null and is dereferenced at line 29
  27.           setContentView(R.layout.activity_main);
  28.           String s = getDay();
  29. >         int length = s.length();
  30.           writeToFile();
  31.       }
  32.


app/src/main/java/infer/inferandroidexample/MainActivity.java:46: error: RESOURCE_LEAK
  resource of type java.io.FileOutputStream acquired to fis by call to FileOutputStream(...) at line 43 is not released after line 46
  44.               fis.write(arr);
  45.               fis.close();
  46. >         } catch (IOException e) {
  47.               //Deal with exception
  48.           }
  49.

app/src/main/java/infer/other/MainActivity.java:23: error: NULL_DEREFERENCE
  object returned by source() could be null and is dereferenced at line 23
  21.     @Override
  22.     protected void onCreate(Bundle savedInstanceState) {
  23. >     source().toString();
  24.     }
  25.

对应源码可在 examples/android_hello/app/src/main/java/infer/inferandroidexample/MainActivity.java 与 examples/android_hello/app/src/main/java/infer/other/MainActivity.java 中逐行核对:

  • 第一个 NULL_DEREFERENCE:getDay() 在非周三分支返回 null,导致 s.length() 空指针;
  • RESOURCE_LEAK:FileOutputStream 的 close() 位于 try 块内,一旦写入抛出 IOException 就不会执行到 close(),资源在异常路径上泄漏;
  • 第二个 NULL_DEREFERENCE:source() 直接返回 null,source().toString() 必然解引用空值。

此外 examples/android_hello/app/src/main/java/infer/inferandroidexample/MainActivity.java 中还有一个「Infer 不应报告」的 inferShouldNotReport() 方法——它依赖的 Generated.java 被 Infer 跳过,因此即使 Generated.returnsNull() 返回 null,Infer 也不应误报,这展示了 Infer 对生成代码的过滤机制;examples/android_hello/app/src/main/java/infer/other/MainActivity.java 中 @SuppressLint("NULL_DEREFERENCE") 标注的方法同样不会被报告,这是 Android 注解抑制机制在 Infer 中的体现。

差分分析(Differential analysis)

这里有一个容易踩的坑:如果文件没变就再次运行 infer run -- ./gradlew build,你会发现这次什么都没分析。原因在于 Gradle 是增量构建的——所有东西都编译过了,没有文件被重新编译。而 Infer 靠捕获编译命令来决定分析哪些文件,既然没有新的编译命令,自然没有文件可分析。解决方案有三种:

  1. 两次 Infer 运行之间先 clean:

    ./gradlew clean
    

    这会迫使 Gradle 每次都全量重编译,Infer 随之重新捕获所有文件。

  2. 让捕获延续(--continue):使用 --continue(简写 -c):

    infer run --continue -- ./gradlew build
    

    这会让 Infer 把新产生的编译命令追加到上一次捕获结果之上,然后对整个代码库重新发起一次分析。从源码看,这一选项对应 infer/src/infer.ml 中的 Config.continue_capture 配置(见 infer/src/infer.ml),它直接决定捕获阶段是新建还是延续捕获结果。

  3. 反应式分析(--reactive):在代码改动后使用 --reactive(简写 -r):

    infer run --reactive -- ./gradlew build
    

    这会让 Infer 只分析这次代码改动产生的影响,不重新分析全部代码,只重分析被修改的文件以及依赖它们的文件。这种模式下分析速度可以显著提升。

每种方案的更深入细节,可参阅仓库中的 Infer workflow 文档。

Hello world iOS

iOS 示例是一个完整的 Xcode 工程,位于仓库 examples/ios_hello。在工程目录下直接运行:

infer run -- xcodebuild -target HelloWorldApp -configuration Debug -sdk iphonesimulator

Infer 输出一批覆盖多种缺陷类型的报告:

AppDelegate.m:20: error: MEMORY_LEAK
   memory dynamically allocated to shadowPath by call to CGPathCreateWithRect() at line 20, column 28 is not reachable after line 20, column 5

AppDelegate.m:25: error: RESOURCE_LEAK
   resource acquired to fp by call to fopen() at line 25, column 8 is not released after line 25, column 5

AppDelegate.m:29: warning: PARAMETER_NOT_NULL_CHECKED
   Parameter callback is not checked for null, there could be a null pointer dereference: pointer callback could be null and is dereferenced at line 29, column 5

AppDelegate.m:34: error: NULL_DEREFERENCE
   pointer str last assigned on line 33 could be null and is dereferenced at line 34, column 12

AppDelegate.m:39: error: PREMATURE_NIL_TERMINATION_ARGUMENT
   pointer str last assigned on line 38 could be nil which results in a call to arrayWithObjects: with 1 arguments instead of 3 (nil indicates that the last argument of this variadic method has been reached) at line 39, column 12

Hello.m:20: error: NULL_DEREFERENCE
   pointer hello last assigned on line 19 could be null and is dereferenced at line 20, column 12

Hello.m:25: warning: IVAR_NOT_NULL_CHECKED
   Instance variable hello -> _hello is not checked for null, there could be a null pointer dereference: pointer ret_hello last assigned on line 24 could be null and is dereferenced at line 25, column 12

Hello.m:30: warning: PARAMETER_NOT_NULL_CHECKED
   Parameter hello is not checked for null, there could be a null pointer dereference: pointer ret_hello last assigned on line 29 could be null and is dereferenced at line 30, column 12

对照源码可以看到这些缺陷都是刻意埋入的练习点:

  • examples/ios_hello/HelloWorldApp/AppDelegate.m 中的 memory_leak_bug(CGPathCreateWithRect 创建对象后未释放)、resource_leak_buf(fopen 后未 fclose)、parameter_not_null_checked_block_bug(nil block 回调)、npe_in_array_literal_bug 与 premature_nil_termination_argument_bug(nil 混入数组字面量与可变参数列表);
  • examples/ios_hello/HelloWorldApp/Hello.m 中的 null_dereference_bug、ivar_not_nullable_bug(PARAMETER_NOT_NULL_CHECKED)与 parameter_not_null_checked_bug。

特别值得留意的是 PREMATURE_NIL_TERMINATION_ARGUMENT:arrayWithObjects: 是变参方法,nil 既作为参数又充当参数列表的终止符,把 nil 放在中间会让实参数量从 3 悄悄变成 1——这类问题纯靠编译器很难发现,是静态分析擅长捕捉的经典陷阱。

与 Gradle 场景相同,上述命令第二次运行时同样不会产生任何分析结果(没有文件被重新编译)。两个解决办法:给 infer 命令加上 --reactive(-r)标志:

infer run --reactive -- xcodebuild -target HelloWorldApp -configuration Debug -sdk iphonesimulator

或者先让构建系统清理目录:

xcodebuild -target HelloWorldApp -configuration Debug -sdk iphonesimulator clean

Hello world Make

最后一个场景是不依赖 IDE 的纯 Make 工程,位于仓库 examples/c_hello。其 Makefile 非常简单——把目录下所有 .c 文件用 ${CC} -c 编译成 .o,同时提供 clean 目标。在工程目录运行:

infer run -- make

Infer 输出五种典型缺陷:

example.c:22: error: NULL_DEREFERENCE
   pointer max last assigned on line 21 could be null and is dereferenced at line 22, column 10

example.c:36: error: NULL_DEREFERENCE
   pointer joe last assigned on line 35 could be null and is dereferenced by call to get_age() at line 36, column 10

example.c:45: error: RESOURCE_LEAK
   resource acquired to fd by call to open() at line 41, column 12 is not released after line 45, column 5

example.c:51: error: MEMORY_LEAK
   memory dynamically allocated to p by call to malloc() at line 51, column 14 is not reachable after line 51, column 3

example.c:57: error: MEMORY_LEAK
   memory dynamically allocated to p by call to malloc() at line 56, column 14 is not reachable after line 57, column 3

对应源码 examples/c_hello/example.c 中,缺陷点清晰可查:

  • 第 27-30 行 simple_null_pointer:max = 0 后直接 max->age;
  • 第 39-42 行 null_pointer_interproc:Person_create 返回 null,跨函数传递给 get_age 后解引用——这是**过程间分析(interprocedural analysis)**的体现,Infer 会跨函数追踪 null 的流向;
  • 第 44-51 行 fileNotClosed:open() 返回的 fd 未关闭;
  • 第 53-56 行 simple_leak:malloc 后指针立即丢失;
  • 第 58-65 行 common_realloc_leak:realloc 失败时原指针 p 变为不可达。

同样地,第二次运行 infer run -- make 不会有任何结果。两种处理方式:加 --reactive(-r):

infer run --reactive -- make

或者在重新分析前执行:

make clean

小结与下一步

通过六个 Hello World 级别的真实示例,你已经走通了 Infer 的完整工作流:用 infer run -- <编译命令> 包装任意构建系统,解读 NULL_DEREFERENCE、RESOURCE_LEAK、MEMORY_LEAK 等报告,修复后复检确认,并理解了增量构建下「二次运行无结果」的原因及 --continue / --reactive 两种差分分析策略的取舍:--continue 累积捕获结果后全量重分析,--reactive 只分析改动影响面、速度更快。

接下来可以深入以下仓库文档继续进阶:

【免费下载链接】infer A static analyzer for Java, C, C++, and Objective-C 【免费下载链接】infer 项目地址: https://gitcode.com/gh_mirrors/infer/infer

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值