当你在写一个Checker的时候通常你需要先决定你是否需要path-sensitivity来处理,或者就用语法检测来处理
path-sensitivity比语法检测慢得多得多,但是之所以慢不是因为添加了Checker的原因,而是因为会初始化很多path-sensitivity需要的数据结构,所以如果你调用了path-sensitivity那么你加多少个checker都没关系
虽然纯语法分析通常和编译一样快,甚至更快,因为代码生成不会发生。但是,语法级别的分析并不能为大多数检查收集足够的信息
要理解不同类型的分析如何相互比较,最简单的方法就是看程序是如何运行的,从各种分析的角度来表现
使用一个例子,来构建它的抽象语法树,控制流程图,path-sensitive分解图
void foo(int x) { int y,z; if(x ==0 ) { y = 5; } if(!x) { z= 6; } }
clang的AST结构是由编译器前端产生的,是Clang所用程序的中间表示形式,然后后续的二进制代码就由AST啦i生产,和GCC编译器的AST不同,Clang的AST不仅包含正确编译程序所需的最小信息,还包含有序程序源代码的完整信息:树中的每一个元素都会记住其源位置以及它是如何再源代码中写入的,甚至在预处理之前都有,这让AST本身可以作为源代码分析的最简单的框架使用
看一个简单的例子
void foo(int x){ int y,z; if(x ==0) y=5; if(!x) z =6; }
针对这个例子查看抽象语法树
clang -cc1 -ast - dump test .c
TranslationUnitDecl << invalid sloc >> <invalid sloc > ‘- FunctionDecl <test .c:1:1 , line :7:1 > line :1:6 foo ’void ( int)’ |- ParmVarDecl 0 x3625c60 <col :10 , col :14 > col :14 used x ’int ’ ‘- CompoundStmt <col :17 , line :7:1 > |- DeclStmt <line :2:3 , col :11 > | |- VarDecl 0 x3625de0 <col :3, col :7> col :7 used y ’int ’ | ‘- VarDecl 0 x3625e50 <col :3, col :10 > col :10 used z ’int ’ |- IfStmt <line :3:3 , line :4:9 > | |-<<<NULL >>> | |- BinaryOperator <line :3:7 , col :12 > ’int ’ ’== ’ | | |- ImplicitCastExpr <col :7> ’int ’ <LValueToRValue > | | | ‘- DeclRefExpr <col :7> ’int ’ lvalue ParmVar 0 x3625c60 ’x’ ’int ’ | | ‘- IntegerLiteral <col :12 > ’int ’ 0 | |- BinaryOperator <line :4:5 , col :9> ’int ’ ’=’ | | |- DeclRefExpr <col :5> ’int ’ lvalue Var 0 x3625de0 ’y’ ’int ’ | | ‘- IntegerLiteral <col :9> ’int ’ 5 | ‘-<<<NULL >>> ‘- IfStmt <line :5:3 , line :6:9 > |-<<<NULL >>> |- UnaryOperator <line :5:7 , col :8> ’int ’ prefix ’!’ | ‘- ImplicitCastExpr <col :8> ’int ’ <LValueToRValue > | ‘- DeclRefExpr <col :8> ’int ’ lvalue ParmVar 0 x3625c60 ’x’ ’int ’ |- BinaryOperator <line :6:5 , col :9> ’int ’ ’=’ | |- DeclRefExpr <col :5> ’int ’ lvalue Var 0 x3625e50 ’z’ ’int ’ | ‘- IntegerLiteral <col :9> ’int ’ 6
读AST类似于读原程序,通过注释描述你可以大概理解到源代码是怎么写的
|- ParmVarDecl 0 x3625c60 <col :10 , col :14 > col :14 used x ’int ’ //这一句就表明,参数定义的是int x
然而,在构造AST时,编译器并不会去理解和建模应用程序中到底发生了什么,它不通过分支语句来构造不同的执行路径,也不试图预测不同的分支如何交互
你可以用AST做的最好的事就是用它去发现不想要的代码,比如:如果你不想C语言风格代码出现在你的C++项目里面,你可以很轻松的创建一个AST基础的Checker去警告所有的C风格代码,复杂一点的例子就是每次检查函数的返回值有没有错
“security.InsecureAPI"系列的Checker可以作为一个很好的AST-based checkers例子,在默认的CSA里面可以查看
但是,如果你用AST-base Checker去试着发现除0的错误,即使很容易找到a = x/0这样的代码,但是”a = 0, y = x/a"这种的代码错误就要困难得多,对于这种就需要采用别的检查方式
clang的流程图表示所有在程序执行期间会执行到的代码流程
CFG(流程控制图)是由一个一个分离的函数构造出来的
CFG的每一个节点代表一个基本的语句块,这些语句块不包含任何分支语句,所以就会按照顺序执行下来
每个基本块以终止符语句结束,该终止符语句是分支语句的结束语句或者函数的返回语句
然后连接边会将基本快连接到可能达到的其它块,具体取决于终止符分支条件的运行时的值,也就是说通过一些逻辑结构来连接区块
CSA提供了一个方便的办法来生成CFG
clang -cc1 -analyze -analyzer-checker=debug.ViewCFG xxx.c
就会生成一个网页版的CFG,可以直接用浏览器查看 大概这样子
基于CFG的分析有助于创建安全检查,有必要考虑所有可能的程序路径。例如,如果要确保某个分支条件总是计算为false那么别无选择,只能获得条件表达式中引用的所有变量的定义,CFG将是进行此类分析的非常好用的工具
通过AST构造CFG比较容易,但是基于CFG的分析通常很难实现,因为CFG不能提供数据流的分析,只能看图来分析。而相反Clang的框架就提供了一些现成的基于CFG的解决方案来处理,例如:LivenessAnalysis
DeadCode.DeadStores就是一个很好的例子
有时会将基于CFG分析和path-sensitive分析结合,使用检测器的path-sensitive部分来寻找潜在的缺陷位置,然后采用基于CFG的启发式算法,通过检测缺陷的其他路径来提高检查为真的几率,官方的deadcode.UnreachableCode Checker就是一个很好的结合了path-sensitive和基于CFG的分析的例子
CSA的exploded graph是path-sensitive静态分析引擎的基础数据结构
Analyzer核心代码尝试去解释程序代码,并处理通过CFG的各种不同路径,即使他们通过相同的语句或者关键语块,也因此被称为分解图
分解图由分析器引擎浏览通过CFG的所有路径组成,并在每条语句的每条路径上携带有关程序状态的信息。
分解图中的节点,被称为分解节点,是由程序的状态和当前正在分析的程序点组成的对
可以用debug.ViewExplodedGraph来查看
clang -cc1 -analyze -analyzer-checker =debug.ViewExplodedGraph xxxx //来查看
分解图通常都比较大
简化版: