Infer 静态分析器入门实战:Hello World 系列示例与差分分析详解
本篇技术指南基于 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 靠捕获编译命令来决定分析哪些文件,既然没有新的编译命令,自然没有文件可分析。解决方案有三种:
-
两次 Infer 运行之间先 clean:
./gradlew clean这会迫使 Gradle 每次都全量重编译,Infer 随之重新捕获所有文件。
-
让捕获延续(
--continue):使用--continue(简写-c):infer run --continue -- ./gradlew build这会让 Infer 把新产生的编译命令追加到上一次捕获结果之上,然后对整个代码库重新发起一次分析。从源码看,这一选项对应 infer/src/infer.ml 中的
Config.continue_capture配置(见 infer/src/infer.ml),它直接决定捕获阶段是新建还是延续捕获结果。 -
反应式分析(
--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 workflow:完整的捕获-分析-报告流程与差分分析细节;
- analyzing-apps-or-projects:在真实项目与 CI 中的集成方式;
- man-infer-run:
infer run全部命令行选项参考; - all-issue-types:各类缺陷类型的完整清单与含义。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



