原文链接:Clang、Infer 和 OCLint ,我们应该使用谁来做静态分析?
原文链接:如何利用 Clang 为 App 提质?
提到静态分析工具,我们最先想到的,或者说最常用的就是 Xcode 自带的Analyze。
下面贴一个 Analyze 工具检查案例:
上面2张图中对应的案例是《Objective-C高级编程》中提到的,只是针对现在的编译环境稍有些差异,第一个图中我们通过静态分析可以看出,执行block时之前定义的数组array对象已经被释放,所以会导致空指针访问,程序崩溃。
图二中情况与图一不同,block在全局区声明,在test2函数执行完成后被释放,所以造成悬垂指针。
上面的例子我们可以看出,通过静态语法分析能够找出在代码层面就能发现的内存泄露问题,还可以通过上下文分析出是否存在无用变量等问题。但是系统自带的Analyze工具的功能还是有限,有时需要依靠更强大的第三方静态检查工具:OCLint、infer、Clang静态分析器等。所以引出了一个问题,静态分析工具应该怎么选?
首先说说静态分析结果复杂度指标和缺点:
结果复杂度常用的指标有3个,如果复杂度太高,就有必要优化和重构代码了:
1 圈复杂度,是遍历一个模块时的复杂度。这个复杂度由分支语句还有运算符以及决策点共同决定。这个圈复杂度的值可以根据静态分析器的圈复杂度规则来监控,一般超过11的就需要重构了。
2 NPath复杂度,是一个方法所有可能执行的路径数量。一般高于200就要考虑重构。
3 NCSS复杂度,是指不包含注释的源码行数。因为方法和类过大会导致代码冗余,维护苦难。一般方法超过百行,类超过千行,就需要进行拆分和重构了。
缺点有2个:
1 耗费时间长。即便很多静态分析器在是设计就已经做了很多速度的优化,但是由于静态分析本身就包含了编译最耗时的IO和语法分析,而且静态分析所做的工作肯定比编译要多,所以最好的情况也要比编译过程要慢。
2 不够灵活。静态分析器只能检查事前编写的、可查找的错误。对于定制类型的错误分析只能开发者根据自己的需求写一些插件并添加进去。
其次因为OCLint、infer、Clang这三款静态分析工具都是基于Clang库来开发的。那么到底什么是Clang呢?Clang做了哪些事情?提供了什么能力?再了解这三款静态分析工具之前,我们先对Clang做个初步了解。
1 什么是Clang?
我们之前0506章节的时候学习过iOS开发的编译和链接流程,上图的左侧黑色方块部分就是 Clang。它是编译的前端。任务是进行:语法分析,语义分析,生成中间代码(intermediate representation)。在这个过程中,会进行类型检查,如果发现错误或者警告会标注出来在哪一行。而之前介绍的LLVM是编译后端,这个阶段会进行机器无关的代码优化,生成机器语言,并且进行机器相关的代码优化。
这一部分更详细的解释和学习请参考:浅谈iOS编译过程
了解了这部分内容,我们来一起罗列一下 Clang 有哪些优势:
1 对于使用者来说,Clang 编译的速度非常快,对内存的使用率非常低,并且兼容 GCC。
2 对于代码诊断来说,Clang 也非常强大。比如在 Xcode 编译iOS项目的时候,都是使用 LLVM 的,而 Clang 又是 LLVM 的编译前端,具体如下:代码的亮度(Clang)、实时代码检查(Clang)、代码提示(Clang)、debug断点调试(LLDB)。
3 Clang 对 typedef 的保留和展开也处理得非常好。typedef 可以缩写很长的类型,保留 typedef 对于粗粒度诊断分析很有帮助。同时,需要了解细节时对 typedef 进行展开即可。
4 在宏处理上,很多宏都是深度嵌套的,Clang 会自动打印实力话信息和嵌套范围信息来帮助你进行宏的诊断和分析。
5 Clang 的架构是模块化的。除了代码静态分析外,利用其输出的接口还可以开发用于代码转义、代码生成代码重构的工具。方便与IDE进行继承,这一点刚刚第二条也提到过。
Clang是基于C++开发的。源码质量非常高,有很多可以学习的地方,我个人本身对于C++的了解非常有限,如果你有兴趣的话可以在线查看Clang的源码,进行学习。
2 Clang做了哪些事?
Clang提供了一个易用性很高的黑盒Driver,用于封装前端指令和工具链的指令。
了解它做了哪些事,我们可以从这里入手。
老师文中给出这样一段示例代码,来看看 Clang 如何一步步编译它:
int main() {
int a;
int b = 10;
a = b;
return a;
}
1 Lexical Analysis - 词法分析
使用 Clang 命令 clang -Xclang -dump-tokens main.m
将代码切分成 Token。
词法分析其实是编译器开始工作真正意义上的第一个步骤,其所做的工作主要为将输入的代码转换为一系列符合特定语言的词法单元,这些词法单元类型包括了关键字,操作符,变量等等。
int 'int' [StartOfLine] Loc=<main.m:8:1>
identifier 'main' [LeadingSpace] Loc=<main.m:8:5>
l_paren '(' Loc=<main.m:8:9>
r_paren ')' Loc=<main.m:8:10>
l_brace '{' [LeadingSpace] Loc=<main.m:8:12>
int 'int' [StartOfLine] [LeadingSpace] Loc=<main.m:9:5>
identifier 'a' [LeadingSpace] Loc=<main.m:9:9>
semi ';' Loc=<main.m:9:10>
int 'int' [StartOfLine] [LeadingSpace] Loc=<main.m:10:5>
identifier 'b' [LeadingSpace] Loc=<main.m:10:9>
equal '=' [LeadingSpace] Loc=<main.m:10:11>
numeric_constant '10' [LeadingSpace] Loc=<main.m:10:13>
semi ';' Loc=<main.m:10:15>
identifier 'a' [StartOfLine] [LeadingSpace] Loc=<main.m:11:5>
equal '=' [LeadingSpace] Loc=<main.m:11:7>
identifier 'b' [LeadingSpace] Loc=<main.m:11:9>
semi ';' Loc=<main.m:11:10>
return 'return' [StartOfLine] [LeadingSpace] Loc=<main.m:12:5>
identifier 'a' [LeadingSpace] Loc=<main.m:12:12>
semi ';' Loc=<main.m:12:13>
r_brace '}' [StartOfLine] Loc=<main.m:13:1>
eof '' Loc=<main.m:15:5>
通过指令可以显示每个 Token 的类型、值、位置。这个位置是宏展开之前的位置,这样后面如果发现报错,就可以正确的提示错误位置了。
2 Semantic Analysis - 语法分析
将输出的 Token 先按照语法组合成语义,生成类似 VarDecl 这样的节点,然后将这些节点按照层级关系构成抽象语法树(AST)并输出。
使用指令 clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
TranslationUnitDecl 0x7fcef002c8e8 <<invalid sloc>> <invalid sloc>
|-TypedefDecl 0x7fcef002d180 <<invalid sloc>> <invalid sloc> implicit __int128_t '__int128'
| `-BuiltinType 0x7fcef002ce80 '__int128'
|-TypedefDecl 0x7fcef002d1e8 <<invalid sloc>> <invalid sloc> implicit __uint128_t 'unsigned __int128'
| `-BuiltinType 0x7fcef002cea0 'unsigned __int128'
|-TypedefDecl 0x7fcef002d280 <<invalid sloc>> <invalid sloc> implicit SEL 'SEL *'
| `-PointerType 0x7fcef002d240 'SEL *'
| `-BuiltinType 0x7fcef002d0e0 'SEL'
|-TypedefDecl 0x7fcef002d358 <<invalid sloc>> <invalid sloc> implicit id 'id'
| `-ObjCObjectPointerType 0x7fcef002d300 'id'
| `-ObjCObjectType 0x7fcef002d2d0 'id'
|-TypedefDecl 0x7fcef002d438 <<invalid sloc>> <invalid sloc> implicit Class 'Class'
| `-ObjCObjectPointerType 0x7fcef002d3e0 'Class'
| `-ObjCObjectType 0x7fcef002d3b0 'Class'
|-ObjCInterfaceDecl 0x7fcef002d488 <<invalid sloc>> <invalid sloc> implicit Protocol
|-TypedefDecl 0x7fcef00361e8 <<invalid sloc>> <invalid sloc> implicit __NSConstantString 'struct __NSConstantString_tag'
| `-RecordType 0x7fcef0036000 'struct __NSConstantString_tag'
| `-Record 0x7fcef002d550 '__NSConstantString_tag'
|-TypedefDecl 0x7fcef0036280 <<invalid sloc>> <invalid sloc> implicit __builtin_ms_va_list 'char *'
| `-PointerType 0x7fcef0036240 'char *'
| `-BuiltinType 0x7fcef002c980 'char'
|-TypedefDecl 0x7fcef0036548 <<invalid sloc>> <invalid sloc> implicit __builtin_va_list 'struct __va_list_tag [1]'
| `-ConstantArrayType 0x7fcef00364f0 'struct __va_list_tag [1]' 1
| `-RecordType 0x7fcef0036370 'struct __va_list_tag'
| `-Record 0x7fcef00362d0 '__va_list_tag'
`-FunctionDecl 0x7fcef00365f0 <main.m:8:1, line:13:1> line:8:5 main 'int ()'
`-CompoundStmt 0x7fcef0036940 <col:12, line:13:1>
|-DeclStmt 0x7fcef0036760 <line:9:5, col:10>
| `-VarDecl 0x7fcef0036700 <col:5, col:9> col:9 used a 'int'
|-DeclStmt 0x7fcef0036810 <line:10:5, col:15>
| `-VarDecl 0x7fcef0036790 <col:5, col:13> col:9 used b 'int' cinit
| `-IntegerLiteral 0x7fcef00367f0 <col:13> 'int' 10
|-BinaryOperator 0x7fcef00368c0 <line:11:5, col:9> 'int' '='
| |-DeclRefExpr 0x7fcef0036828 <col:5> 'int' lvalue Var 0x7fcef0036700 'a' 'int'
| `-ImplicitCastExpr 0x7fcef00368a8 <col:9> 'int' <LValueToRValue>
| `-DeclRefExpr 0x7fcef0036868 <col:9> 'int' lvalue Var 0x7fcef0036790 'b' 'int'
`-ReturnStmt 0x7fcef0036928 <line:12:5, col:12>
`-ImplicitCastExpr 0x7fcef0036910 <col:12> 'int' <LValueToRValue>
`-DeclRefExpr 0x7fcef00368e8 <col:12> 'int' lvalue Var 0x7fcef0036700 'a' 'int'
其中 TranslationUnitDecl 是根节点,表示一个编译单元;Decl 表示一个声明;Expr 表示表达式;Literal 表示字面量,是一个特殊的 Expr;Stmt 表示陈述。
下面这段文中是我上一篇 0506章节-编译提速 中总结的,其中Clang作为编译前端主要负责的就是红框中的部分。
随后的工作还有:
- 使用命令
clang -S -emit-llvm main.m -o main.ll
生成LLVM中间代码LLVM IR。 - 使用命令
clang -O3 -S -emit-llvm main.m -o main.ll
优化IR等。
等等
3 Clang提供了什么能力?
了解这个部分,要先了解一个 Clang AST 接口。Clang提供的能力都是基于它的。
这个接口的功能非常强大,除了能够获取符号在源码中的位置,还可以获取方法的调用关系,类型定义和源码里的所有内容。
以这个接口为基础,利用 LibClang、Clang Plugin 和 LibTooling 这些封装好的工具我们就可以开发出满足自己需求的工具了。
1 LibClang
LibClang 提供了一个稳定的高级 C 接口,Xcode 使用的就是 LibClang。LibClang 可以访问 Clang 的上层高级抽象的能力,比如获取所有 Token、遍历语法树、代码补全等。由于 API 很稳定,Clang 版本更新对其影响不打。但是,LibClang 并不能完全访问到 Clang AST 信息。
2 Clang Plugins
Clang Plugins 可以让你在 AST 上做些操作,这些操作能够继承到编译中,成为编译的一部分。插件是在运行时由编译器加载的动态库,方便集成到构建系统中。之后会有一篇文章 “如何编写 Clang 插件?”详细说明 Clang Plugins 的用法。
3 LibTooling
LibTooling 是一个 C++ 接口,通过 LibTooling 能够编写独立运行的语法检查和代码重构工具。LibTooling 的优势如下:
1 所写的工具不依赖于构建系统,可以作为单独命令使用。
2 可以完全控制 Clang AST。
3 能够和 Clang Plugins 共用一份代码。
展开:
1 在 LibTooling 的基础之上有个开发人员工具合集 Clang tools。比如:语法检查工具 clang-check;自动修复编译错误工具 clang-fixit;自动代码各式工具 clang-format;新语言和新功能的迁移工具;重构工具等。
2 基于完全控制 Clang AST 和可独立运行的特点。可以做很多事情:
- 改变代码,比如把 OC代码转成 JavaScript 或者 Swift。
- 做检查,命名规范检查,类型检查,甚至按照自己定义的规则进行代码检查。
- 做分析,对源码做分析,甚至重写程序。
最后,如果你打算基于 LibTooling 来开发工具,你可以参考官方的教程 Tutorial for building tools using LibTooling and LibASTMatchers ,通过这个教程你可以了解一些基础使用方法。
4 静态分析工具 OCLint、infer、Clang 怎么选?
这部分内容主要是07章节的,原文中分别介绍了3种分析工具的简单使用,因为本身项目中也没有完全应用这3种静态分析工具,也没办法一一应用。所以只能写一下总结。
1 OCLint
是基于 Clang Tooling 开发的静态分析工具,主要用来发现编译器检查不到的潜在问题:空的判断语句、未使用的局部变量和参数等。在网页前端开发时也经常使用。
具体的介绍和使用请参考:iOS使用OCLint做静态代码分析
2 Clang 静态分析器
是 Clang 项目的一部分,构建在 Clang 和 LLVM 之上。分析引擎用的就是 Clang 的库。
你可以在 这里 下载它。常用的工具就是 scan-build 和 scan-view。
Clang 静态分析器是由分析引擎(analyzer core)和 checkers 组成的。也就是说你所需要的功能都可以通过自己编写 checker 来实现。 但是我并没有找到原文中所说的存放checker的路径 /lib/StaticAnalyzer/Checkers。定制 checker 本身也不容易,学习成本很高。而且每执行一条语句,分析引擎需要回去遍历所有 checker 中的回调函数,这样 checker 的数量越多,检查的整体速度也会变得很慢,这也是 checker 架构不完美的地方。
3 Infer
Infer 是 Facebook 开源的、使用 OCaml 语言编写的静态分析工具。可以检查空指针访问、资源泄漏以及内存泄漏。这也是原文中推荐选择的静态分析工具!
查看:facebook/infer
文档:Getting started with Infer
安装:How to install Infer from source
示例:
#import <Foundation/Foundation.h>
@interface Hello: NSObject
@property NSString* s;
@end
@implementation Hello
NSString* m() {
Hello* hello = nil;
return hello->_s;
}
@end
终端输入:
infer -- clang -c Hello.m
结果如下:
Capturing in make/cc mode...
Found 1 source file to analyze in /Users/ming/Downloads/jikeshijian/infer-out
Starting analysis...
legend:
"F" analyzing a file
"." analyzing a procedure
F.
*Found 5 issues*
hello.m:10: error: NULL_DEREFERENCE
pointer `hello` last assigned on line 9 could be null and is dereferenced at line 10, column 12.
8. NSString* m() {
9. Hello* hello = nil;
10. *>* return hello->_s;
11. }
hello.m:10: warning: DIRECT_ATOMIC_PROPERTY_ACCESS
Direct access to ivar `_s` of an atomic property at line 10, column 12. Accessing an ivar of an atomic property makes the property nonatomic.
8. NSString* m() {
9. Hello* hello = nil;
10. *>* return hello->_s;
11. }
hello.m:4: warning: ASSIGN_POINTER_WARNING
Property `s` is a pointer type marked with the `assign` attribute at line 4, column 1. Use a different attribute like `strong` or `weak`.
2.
3. @interface Hello: NSObject
4. *>*@property NSString* s;
5. @end
6.
hello.m:10: warning: DIRECT_ATOMIC_PROPERTY_ACCESS
Direct access to ivar `_s` of an atomic property at line 10, column 12. Accessing an ivar of an atomic property makes the property nonatomic.
8. NSString* m() {
9. Hello* hello = nil;
10. *>* return hello->_s;
11. }
hello.m:4: warning: ASSIGN_POINTER_WARNING
Property `s` is a pointer type marked with the `assign` attribute at line 4, column 1. Use a different attribute like `strong` or `weak`.
2.
3. @interface Hello: NSObject
4. *>*@property NSString* s;
5. @end
6.
*Summary of the reports*
DIRECT_ATOMIC_PROPERTY_ACCESS: 2
ASSIGN_POINTER_WARNING: 2
NULL_DEREF
根据终端的提示可以看出,在 hello.m 代码中一共有五个问题,分别是一个错误和四个警告。
根据错误提示 pointer hello last assigned on line 9 could be null and is dereferenced at line 10, column 12.
可知,hello 这个指针可能为空,需要去掉第10行12列的引用。
此外,根据错误提示Property s is a pointer type marked with the assign attribute at line 4, column 1. Use a different attribute like strong or weak.
可知,s 这个属性是指针类型被标记为 assign
类型,应该改为 strong
或者 weak
修饰。
最终我将之前的代码修改为:
#import <Foundation/Foundation.h>
@interface Hello: NSObject
@property (nonatomic, strong) NSString* s;
@end
@implementation Hello
NSString* m() {
return hello.s;
}
@end
最后再运行 infer -- clang -c Hello.m
发现没有问题了:
Capturing in make/cc mode...
Found 1 source file to analyze in /Users/ming/Downloads/jikeshijian/infer-out
Starting analysis...
legend:
"F" analyzing a file
"." analyzing a procedure
F.
*No issues found
总结 Infer 工作流程:
1 转化阶段,将源码转成 Infer 内部的中间语言。类 C 语言使用 Clang 进行编译,Java 语言使用 javac 进行编译,编译的同时转成中间语言,输出到 infer-out 目录。
2 分析阶段,分析 infer-out 目录下的文件。分析每个方法,如果出现错误的话会继续分析下一个方法,并标记出错的位置,最后将所有错误汇总输出。
默认情况每次运行 infer 都会删除之前的 infer-out 文件夹。你可以通过 incremental 参数设置增量模式。
3 结果,在 infer-out 目录下是 JSON 格式的,名叫 report.json。
总结:
关于07章节的内容,先介绍了一下静态分析结果中衡量代码好坏的三个复杂度指标,然后分别介绍了Clang 静态分析器、Infer 和 OCLint 这三款静态分析工具。
关于08章节的内容,原文从三个层面详细介绍了 Clang。了解了它在iOS程序编译前端中发挥的重要作用以及通过拓展 Clang AST 接口我们大概可以获得什么能力。最终的目标是达到严格控制 APP 质量的目的。
如果你公司的项目有这方面的需求,进一步的学习是不可避免的。加油!