Clang
静态分析器在总体LLVM
设计中,如果项目操作原始的(C/C++)
源码,就属于Clang
前端,因为根据LLVMIR
恢复源码层
信息是很难的.
基于Clang
的最有意思工具
之一是Clang
静态分析器,类似传统编译器的警告
,在更小范围中,它用一套检查器
来生成详细的漏洞报告
.
每个检查器
检测违反具体规则
.
如同经典的警告,静态分析器
帮助在开发周期的早期发现漏洞
,而不必等到运行时
.分析是在解析
后,编译前做的.
另一方面,它需要很多时间
处理大量代码
,因此未整合
到典型的编译流程
中.
如,静态分析器可能会单独花数个小时
处理整个LLVM
源码,并运行
所有检查器
.
Clang
静态分析器指数级
时间复杂度的本质
,解释了该工具
的最大局限:它一次只能分析单个编译单元
,不能模块间分析
,或处理整个程序
.
尽管如此,因为依赖符号执行引擎
,仍是很强大的工具.
为了举例说明
,符号执行引擎如何帮助
找出错综复杂的漏洞
,先展示一个大多数编译器
可容易检测到的简单漏洞
并输出警告
.如下:
#include
void main() {
int i;
printf ("%d", i);
}
此代码中,用了个未初化的变量
,会导致程序的输出
依赖不能控制和预测的参数
,如执行程序前的内存内容
,导致未定义行为
.
因此,简单自动检查
可避免调试
中的巨大麻烦
.
如果熟悉编译器解析技术
,注意,可运用前向数据流
分析实现该检查,利用联合交汇符号
传播每个变量
状态来分析是否初化
它.
前向数据流分析
,从函数
第一个基本块
开始,传播每个基本块的变量
的状态信息
,并向后继基本块
压此信息.交汇符号
决定如何合并
多个前面的基本块信息
.
联交汇符号
并为每个前面的基本块
,设置个联合的基本块
属性.
在此分析中,如果有未初化的定义
,应触发编译器警告
.为此,数据流框架
为程序中每个变量
赋值如下状态
:
1,⊥符号
,表示未知
状态
2,已初化
符号,知道已初化了变量
3,未初化
符号,确定未初化
变量
4,Т符号
,不确定是否初化变量
.
数据流分析可依靠多项式
时间复杂度算法
而变得非常快.
为了见识简单分析如何不准确
,看看:
#include
void my_function(int unknownvalue) {
int schroedinger_integer;
if (unknownvalue)//不确定.
schroedinger_integer = 5;
printf("hi");
if (!unknownvalue)
printf("%d", schroedinger_integer);
}
简单数据流
不能提供准确信息
时,符号执行引擎
就起作用了.它建造一个可达程序状态图
,并可推导
全部可能的代码执行路径
.
调试
程序时,只会练习
一个路径.用如valgrind
等强大的虚机
调试程序
找内存泄漏
时,也只是练习一个路径
.
相反,符号执行引擎
可练习
所有路径,而不实际运行
你的代码.这是非常强大
的特性,但是需要大的运行时
来执行.
正如经典数据流框架
,引擎
按它执行每个语句
的顺序
遍历程序,找到每个变量
并给它们初始状态
.当到达改变控制流
的构造
时,不同之处出现了:引擎
将路径一分为二,继续单独分析每个路径
.
该图
叫可达程序状态图
.
比较可达
程序状态图和相同代码
的控制流图
.
注意,首先,是CFG
可能分叉
以表达改变控制流
,但是它也合并节点
,以避免在可达程序状态图
中看到的组合爆炸
.
合并时,数据流分析
可用联或相交
决定来合并不同路径
信息.
经典数据流分析
必需合并数据
,这是符号执行引擎
没有的限制
.与用多个输入
测试程序得到一样,可得到更精确
结果,但以更多
运行时和内存消耗为代价.
探索如何运用Clang
静态分析器.
测试静态分析器
前,应记得,clang -cc1
命令行会直接引用
编译器,而用clang
命令会触发编译器驱动
.
驱动
负责精心调度
编译中涉及的所有
其它的LLVM
程序的执行,但是也负责提供
系统的详尽参数
.
有人喜欢
直接用编译器,但此时可能找不到系统头文件
,或不知道如何配置其它参数
,而只有Clang
驱动知道这些.
另一方面,编译器可能设置
独特的开发者选项
,以调试程序,查看内部.对比如何用两种
方法检查
源文件.
//Compiler
clang -cc1 -analyze -analyzer-checker=<package> <file>
//Driver
clang -analyze -Xanalyzer -analyzerchecker=<package> <file>
用
表示想要分析的源码文件
,而
标签,让你可选择一批具体头文件
.
使用驱动时,注意-analyze
参数会触发
静态分析器.然而,-Xanalyzer
参数会直接转发下个标志
给编译器,让你可设置
特定参数.
因为驱动
是代理
,在整个示例过程中,使用直接编译器.此外,在简单示例
中,直接使用编译器
应该满足需求了.
如果觉得需要按官方
驱动方式使用检查器
,记得用驱动
,并在每个传给编译器的标志
前,先输入-Xanalyzer
选项.
检查器
是静态分析器可在代码
上执行的单个分析单元
.静态分析器
允许选择适合需求
的检查器的任意子集
,或全部开启
它们.
想得到已安装
检查器列表,运行下面命令:
$clang -cc1-analyzer-checker-help
它打印已安装
检查器的长长的列表
,显示所有可从Clang
得到的开箱即用的分析
.现在看看-analyzer-checker-help
命令的输出:
OVERVIEW: Clang Static Analyzer Checkers List
USAGE: -analyzer-checker <CHECKER or PACKAGE,...>
CHECKERS:
alpha.core.BoolAssignment Warn about assigning non-{0,1} values
to Boolean variables
检查器名字,按
格式,为用户提供简单运行一组
指定的相关检查器
的方法.
下表中,列举了最重要的包
,及每个包
的检查器示例列表
.
包名 | 内容 | 例子 |
---|---|---|
阿尔法 | 正在开发的检查器 |
alpha.core.BoolAssignment,alpha.security.MallocOverflow,alpha.unix.cstring.NotNullTerminated |
核心 |
通用环境 | core.NullDereference, core.DivideZero, core.StackAddressEscape |
cplusplus |
用于C++ 内存分配的单个检查器 (其他检查器目前处于alpha 阶段) |
cplusplus.NewDelete |
调试 |
输出静态分析器,调试信息检查器 | debug.DumpCFG, debug.DumpDominators, debug.ViewExplodedGraph |
llvm |
检查代码是否按LLVM 编码标准的单个检查器 |
llvm.Conventions |
osx |
为MacOSX 开发的程序检查器 |
API,osx.cocoa.ClassRelease,osx.cocoa.NonNilReturnValue,osx.coreFoundation.CFError |
安全 | 安全漏洞 检查器 |
security.FloatLoopCounter,security.insecureAPI.UncheckedReturn,security.insecureAPI.gets,security.insecureAPI.strcpy |
UNIX |
为UNIX 开发的程序检查器 |
.API,unix.Malloc,unix.MallocSizeof,unix.MismatchedDeallocator |
首先,试试经典警告方法.为此,简单运行Clang
驱动,不让它编译,只检查语法
:
$ clang -fsyntax-only joe.c
syntax-only
选项,打印警告,检查
语法错误,但是它没有检测
到问题.现在,看看符号执行引擎
:
$ clang -cc1 -analyze -analyzer-checker=core joe.c
可选地,如果前面命令行
要求指定头文件位置
,就如下使用驱动
:
$ clang --analyze -Xanalyzer -analyzer-checker=core joe.c
...警告,调用未初化值....
当场发现!记住,analyzer-checker
选项期望检查器的全名
,或检查器的整个包名
.选择使用了core
检查器的整个包
,但是可只用具体
的检查函数调用
参数的core.CallAndMessage
检查器.
注意,所有静态分析器命令
都以clang -cc1-analyzer
开始;因此,如果想知道解析器
支持的所有命令
,可如下:
$ clang -cc1 -help | grep analyzer
HTML
中生成图形化报告静态分析器
还可导出一个图形化指出代码中存在危险行为
程序路径的HTML
文件.还可用-o
参数指定存储报告
的目录名
.如下:
$ clang -cc1 -analyze -analyzer-checker=core joe.c -o report
可选地,可如下调用
驱动:
$ clang --analyze -Xanalyzer -analyzer-checker=core joe.c -o report
根据该命令行
,解析器处理joe.c
,并生成一个HTML
报告文件,放在report
目录中.
如果想用静态分析器
检查大型项目
.
为此可用scan-build
.
scan-build
替换定义了C/C++
编译器命令的CC
或CXX
环境变量,这样就引入
了项目普通的build
过程.它在编译
前分析每个文件
,再编译它,使得build
过程或脚本
可如期继续工作.
最后,生成HTML
报告.命令行
是很简单的:
$ scan-build <your build command>
你可自由地在scan-build
后,运行任意build
命令,如make
.要想构建Joe
,如,不必Makefile
,可直接用如下编译命令
:
$ scan-build gcc -c joe.c -o joe.o
完成后,可运行scan-view
以查看漏洞报告
:
$ scan-view <output directory given by scan-build>
Apache
的漏洞此例中,检验
在大型项目中,检查漏洞是何等容易.为此,在http://httpd.apache.org/download.cgi
下载最新的ApacheHTTPServer
源码包.
在写作时,它的版本是2.4.9
.示例中,通过控制台下载它,并在当前目录
解压文件:
$ wget http://archive.apache.org/dist/httpd/httpd-2.4.9.tar.bz2
$ tar -xjvf httpd-2.4.9.tar.bz2
用scan-build
检查该源码基
.为此,需要重复生成build
脚本的步骤.注意,需要所有必需依赖库
,以编译Apache
项目.
确认已有了所有依赖库之后,执行如下命令序列:
$ mkdir obj
$ cd obj
$ scan-build ../httpd-2.4.9/configure -prefix=$(pwd)/../install
用prefix
参数指示该项目
新的安装路径
.不过,如果不打算实际安装Apache
,只要不运行make install
,就不需要提供额外参数
.
示例中,安装路径
定义为install
目录.注意,还在命令前面加上scan-build
,它会覆盖CC
和CXX
环境变量.
在configure
脚本,创建所有Makefile
之后,就是启动实际的build
过程时了.用scan-build
拦截make
命令,而不是单独执行它:
$ scan-build make
因为Apache
代码非常多,完成分析
花了几分钟,找到了82
个漏洞.
本例,静态分析器表明,有一个执行路径
最后以未给dc->nVerifyClient
赋值而结束.该路径部分调用了ssl_cmd_verify_parse()
函数,在相同编译模块内
,显示出解析器
检查复杂函数间
路径的能力
.
scan-build
发现,在孤立状态下,该模块
可能会执行有漏洞的路径
,但是不表明用户会用到有漏洞的输入
.
静态
分析器不能在整个项目
环境中分析
该模块,因为此分析
需要花费大量时间(记住指数复杂度).
该路径有11
步,而在Apache
中发现的最长
路径有42
步.它在modules/generators/mod_cgid.c
模块中,它违反了标准CAPI
调用:它用null
指针参数调用strlen()
函数.
因为它的良好设计
,可轻易用自定义
检查器扩展
静态分析器.
记住静态
分析器和检查器
一样好,如果想分析是否有代码
乱用某个API
,要学习如何把该领域相关
的知识嵌入
到Clang
静态分析器中.
Clang
静态分析器的源码在llvm/tools/clang
中.头文件在include/clang/StaticAnalyzer
中,源码在lib/StaticAnalyzer
中.
查看目录
内容,按三个不同的子目录
划分项目
:Checkers,Core
,和Frontend
.
Core
的任务是用一个访问者模式
,源码级
模拟执行程序
,并在每个程序点
(在重要语句
前后)调用注册的检查器
,以保证给定的不变量
.
如,如果检查器
要确保不会两次释放
同一个分配的内存区域
,它会观察malloc()
和free()
,当检测到重复释放
时会生成一个漏洞报告
.
符号引擎不能用运行时的精确程序值
模拟程序
.
符号引擎
的威力在对程序每个可能
结果的推导
,为此,它检查符号(SVals)
而不是具体的值
.
符号
可代表任意区间的整数
,浮点
或未知数.它越了解值
,就越强大.
有三个
理解项目
实现关键的重要数据结构
:ProgramState,ProgramPoint
,和ExplodedGraph
.第一个代表当前状态
的当前执行环境
.
第二个代表程序流
中的在语句前面或后面
的具体点
.
最后代表整个可达
程序状态的图.另外,该图节点
是由ProgramState
和ProgramPoint
的元组
表示,即,每个程序点
都有具体状态
和它关联.
ExplodedGraph
,或可达状态图
,是对经典CFG
的重要展开.注意,一个有两个连接
的而不是嵌套
的if
的小的CFG
,在可达状态图
的表示中,会爆炸
(组合扩展)成四个不同
的路径.
为了节省
空间,会折叠该图
,即,如果创建一个表示程序点及状态
和另一个节点
的相同节点
,就不会分配新节点
,而是重用
已有节点,但可能构建
圈.
为此,ExplodedNode
继承了LLVM
库的父类llvm::FoldingSetNode
.LLVM
库已引入各种常见类
,因为表示
程序时,在编译器的中端和后端
中,广泛使用折叠
.
静态分析器的总体设计
,可划分
为以下部分:
1,引擎,按仿真路径
并管理
其它组件;
2,管理ProgramState
对象的状态管理器
;
3,约束
管理器,负责推导给定程序路径
引起的ProgramState
的约束;
4,及管理程序存储模型
的存储管理器
.
解析器另一个重点是,沿每条路径
模拟执行时,如何建模
内存行为.对如C和C++
此语言,这很难,因为它们提供了多种包括别名等访问相同内存片段
方式.
解析器实现了由Xu
等人描述的区域内存模型
,它甚至可区分一个数组的每个元素的状态
.
Xu
等人提出了内存区域的层级结构
,其中,如,数组元素
是数组的子区域
,数组
是栈的子区域
.
C
中的每个左值
,或每个变量或引用
,有对应区域
建模了它们所在的内存片段
.
另一方面,通过绑定建模
每个内存区域
的内容.每个绑定
关联一个符号值
和内存区域
.
考虑你在开发特定的嵌入式软件
,它依靠两个基本调用的API
:turnReactorOn()
和SCRAM()
(关闭核反应堆),来控制
核反应堆.
核反应堆包含燃料和控制杆
,前者
核反应,后者包含能减缓核反应,使核反应堆保持发电厂规模,而不是变成原子弹
的中子吸收器
.
客户告知你,两次调用SCRAM()
可能导致卡住控制杆
,两次调用turnReactorOn()
会导致核反应
失去控制.
该API
有严格的使用规则
,任务是,在代码
成为产品
之前,审查大型代码基
,确保未违反这些规则:
1,不引入turnReactorOn()
时,不能>2
次调用SCRAM()
2,不引入SCRAM()
时,不能>2
次调用trunRactionOn()
如,考虑如下:
int SCRAM();
int turnRactionOn();
void test_loop(int wrongTemperature, int restart) {
turnRactionOn();
if (wrongTemperature) {
SCRAM();
}
if (restart) {
SCRAM();
}
turnReactorOn();
//让反应堆工作的代码
SCRAM();
}
如果wrongTemperature
和restart
都不是0
,这份代码就违反了API
,没有引入trunReactorOn()
,就导致两次调用SCRAM()
.
如果这两个
参数都是0
,它也违反了API
,这样,代码在没有引入SCRAM()
,就两次调用turnReactorOn()
.
或可肉眼
检查代码,这是非常枯燥且易错
的,或使用像Clang
等静态分析器
处理.问题是,它不理解核电厂API
.可实现特殊检查器来克服
它.
第一步,要为状态模型
创建概念,要在不同程序状态
间传播
的信息.在此,关注反应堆
是开启
的还是关闭
的.
还知不知道
是否开启,因此,状态模型
有三个可能值:未知,开启,和关闭
.
代码以Clang
代码树中找到的SimpleStreamChecker.cpp
简单检查器为基础.
在lib/StaticAnalyzer/Checkers
中,应该创建一个新的ReactorChecker.cpp
文件,并开始编写表示跟踪
时所关心的状态的类
:
#include "ClangSACheckers.h"
#include "clang/StaticAnalyzer/Core/BugReporter/BugType.h"
#include "clang/StaticAnalyzer/Core/Checker.h"
#include "clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h"
#include "clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h"
using namespace clang;
using namespace ento;
class ReactorState {
private:
enum Kind {On, Off} K;
public:
ReactorState(unsigned Ink) : K((Kind) InK) {}
bool isOn() const { return K == On; }
bool isOff() const { return K == Off; }
static unsigned getOn() { return (unsigned) On; }
static unsigned getOff() { return (unsigned) Off; }
bool operator == (const ReactorState &X) const {
return K == X.K;
}
void Profile(llvm::FoldingSetNodeID &ID) const {
ID.AddInteger(K);
}
};
类的数据部分
限制为Kind
的单例.注意ProgramState
类会管理编写的状态信息
.
ProgramState
的不变性ProgramState
生来就是不变
的.一旦建造
出来,就不再改变:它代表在给定执行路径
中,计算出的给定程序点
的状态.
不同于CFG
的数据流分析,此时,处理对不同的一对程序点和状态
,都有不同节点
的可达程序状态图
.这样,如果程序循环
,引擎
会创建全新的记录了此次
新迭代的关联信息
的路径.
相反,数据流分析中,循环会导致新的信息
会更新循环体的状态
,直到到达固定点
.
然而,如前,一旦符号引擎
到达表示有相同状态
的给定循环体
的相同程序点
的节点
,它会认为在该路径
中,没有新的待处理信息
,就重用该节点
而不是新建一个
.
另一方面,如果循环有个,不断地用新信息更新状态
的循环体
,很快会达到符号引擎
的极限:它会在模拟可配置的预定数目
迭代后放弃
该路径,可在启动
该工具时设置
它.
因为状态
一旦创建就不变,ReactorState
类不需要setter
,或修改其状态
的类成员函数
,但是确实需要构造器
.
这就是ReactorState(unsigned InK)
构造器的目的,它按输入接受代表
当前反应器状态的整数
.
最后,Profile
函数是FoldingSetNode
子类ExplodeNode
的结果.所有子类
必须提供此方法,以协助LLVM
折叠来追踪
节点状态,并判断两个节点
是否相同(这时会折叠
它们).
因此,Profile
函数会按K
数字给出状态.
可用以Add
开头的FoldingSetNodeID
的成员函数来通知来识别该对象的实例
(查看llvm/ADT/FoldingSet.h
)的独特位
.示例中,我用了AddInteger()
.
现在,该声明Checker
子类了:
class ReactorChecker : public Checker<check::PostCall> {
mutable IdentifierInfo *IIturnReactorOn, *IISCRAM;
OwningPtr<BugType> DoubleSCRAMBugType;
OwningPtr<BugType> DoubleONBugType;
void initIdentifierInfo(ASTContext &Ctx) const;
void reportDoubleSCRAM(const CallEvent &Call, CheckerContext &C) const;
void reportDoubleON(const CallEvent &Call, CheckerContext &C) const;
public:
ReactorChecker();
//处理`ReactorOn`和`SCRAM`
void checkPostCall(const CallEvent &Call, CheckerContext &C) const;
};
第一行表明,在用带1个模板参数
的Checker
的子类.对该类,可用多个
模板参数,表示你的检查器在访问
时所感兴趣的程序点
.
这些模板参数
从自定义的Checker(a)
类继承,a
是按参数指定的所有的类
的子类.即,这里,检查器会从基类
继承PostCall
.
如此继承用来实现访问模式
,它仅会调用
感兴趣的对象,因此,类必须实现checkPostCall
成员函数.
为访问广泛
多样的程序点类型
(查看CheckerDocumentation.cpp
),也许关心注册
你的检查器.这里关注,在调用
后立即访问程序点
,因为想在调用某个核电厂API
函数后,记录
状态的改变.
这些成员函数
使用了,遵守依赖无状态
的检查器的设计的const
关键字.然而,确实想缓存代表turnReactorOn()
和SCRAM()
符号的IdendifierInfo
对象的结果.
这样,使用用来绕过const
限制的mutable
关键字.
还想通知Clang
基础设施,正在处理新漏洞类型
.为此,必须保存
新的BugType
实例,每个要报告的新漏洞,都各保存一个:两次调用SCRAM()
时的漏洞,及两次调用turnReactorOn()
时的漏洞
.
应该在匿名
名字空间中,封装刚编写的ReactorState
和ReactorChecker
类.这样避免链接器
导出这两个数据结构
,从而只在本地
使用.
深入实现类前,必须调用解析器引擎结合
自定义状态用的一个宏
来展开ProgramState
实例:
REGISTER_MAP_WITH_PROGRAMSTATE(RS, int, ReactorState)
注意,该宏的末尾
没有分号.这用每个ProgramState
实例关联
一个新的map
.第一个参数可以是此后用它引用数据的任意名字
,第二个参数是map
键值的类型
,第三个参数是要存储的对象类型
(此处是ReactorState
类).
检查器常常用map
存储状态
,因为经常用特定资源
关联新的状态
,如,前面的检测器中,每个变量的状态
,初化
的或未初化
的.
此时,map
的键值
会是变量名
,存储的值会是建模了状态
未初化或初化的自定义的类
.对其他给程序状态注册
信息的方式,见CheckerContext.h
中的宏定义.
注意,不是必需要有一个map
,因为对每个程序点
仅总是存储
一个状态.因此,会总是用1键值
访问map
.
检查器类构造器
如下:
ReactorChecker::ReactorChecker() : IIturnReactorOn(0), IISCRAM(0) {
//初化`bug`类型.
DoubleSCRAMBugType.reset(new BugType("Double SCRAM", "Nuclear Reactor API Error"));
DoubleONBugType.reset(new BugType("Double ON", "Nuclear Reactor API Error"));
}
注意,从Clang3.5
开始,BugType
构造器调用需要变为如下,就是按第一个参数添加this
关键字.
BugType(this, "Double SCRAM", "Nuclear Reactor API Error")
BugType(this, "Double ON", "Nuclear Reactor API Error"),
构造器用OwningPtr
的reset()
成员函数,实例化
了一个新的BugType
对象,并给出了新的漏洞种类
的描述.
还初化了IdentifierInfo
指针.接着,定义助手
函数来缓存这些指针的结果
:
void ReactorChecker::initIdentifierInfo(ASTContext &Ctx) const {
if (IIturnReactorOn)
return;
IIturnReactorOn = &Ctx.Idents.get("turnReactorOn");
IISCRAM = &Ctx.Idents.get("SCRAM");
}
ASTContext
对象保存了包含用户程序
用到的类型和声明
的特殊AST
节点,可用它找到监听时感兴趣函数的准确标识
.
现在,实现checkPostCall
访问者模式函数.记住,它是个不应修改检查器状态
的const
函数:
void ReactorChecker::checkPostCall(const CallEvent &Call, CheckerContext &C) const {
initIdentifierInfo(C.getASTContext());
if (!Call.isGlobalCFunction())
return;
if (Call.getCalleeIdentifier() == IIturnReactorOn) {
ProgramStateRef State = C.getState();
const ReactorState *S = State->get<RS>(1);
if (S && S->isOn()) {
reportDoubleON(Call, C);
return;
}
State = State->set<RS>(1, ReactorState::getOn());
C.addTransition(State);
return;
}
if (Call.getCalleeIdentifier() == IISCRAM) {
ProgramStateRef State = C.getState();
const ReactorState *S = State->get<RS>(1);
if (S && S->isOff()) {
reportDoubleSCRAM(Call, C);
return;
}
State = State->set<RS>(1, ReactorState::getOff());
C.addTransition(State);
return;
}
}
第一个参数是CallEvent
类型,在该程序点
(查看CallEvent.h
)前,保留程序调用函数
的精确函数信息,因为注册了一个后调用访问器
.
第二个参数是CheckerContext
类型,是该程序点
的当前状态
的唯一信息源
,因为检查器必须是无状态
的.用它取ASTContext
,初化检查
监听的函数依赖的Identifier
对象.
查询CallEvent
对象,来检查它是否
调用了trunReactorOn()
函数.如果是,需要处理转移
到开启状态
的过程.
在转移
状态前,首先检查是否已开启状态
,否则,就有漏洞.
注意在State->get
语句中,RS
只是在注册程序状态
的新特征
时所给的名字,1
是总是用它访问map
位置的固定整数
.
虽然此时不需要map
,但是用map
,可轻松
扩展检查器,以监听更复杂的多个状态
.
按const
指针恢复
存储状态,因为在处理的到达该程序点
的信息
是不变
的.
首先,必须检查它是否为,表示不知道反应堆
是否开启的空引用
.
如果不是空的
,检查它是否开启
,且为正,然后放弃
进一步分析而报告一个漏洞.
对其它情况,用ProgramStateRef
设置成员函数来新建一个状态
,并把该新的状态
传递给,记录信息并在ExplodedGraph
中创建一条新边
的addTransition()
成员函数.
只有在实际改变状态
时,才会创建
边.用类似逻辑,处理SCRAM
.
报告漏洞
成员函数代码如下:
void ReactorChecker::reportDoubleON(const CallEvent &Call, CheckerContext &C) const {
ExplodedNode *ErrNode = C.generateSink();
if (!ErrNode)
return;
BugReport *R = new BugReport(*DoubleONBugType,
"Turned on the reactor two times", ErrNode);
R->addRange(Call.getSourceRange());
C.emitReport(R);
}
void ReactorChecker::reportDoubleSCRAM(const CallEvent &Call, CheckerContext &C) const {
ExplodedNode *ErrNode = C.generateSink();
if (!ErrNode)
return;
BugReport *R = new BugReport(*DoubleSCRAMBugType, "Called a SCRAM procedure twice", ErrNode);
R->addRange(Call.getSourceRange());
C.emitReport(R);
}
第一个
动作是生成一个sink
节点,在可达程序状态
中,表明在该路径
上遇见一个严重漏洞
,不想继续分析该路径
.
下面几行
创建一个,说找到一个DoubleOnBugType
类型漏洞的新的BugReport
对象,可任意写漏洞描述
,并提供刚刚构建
的错误节点
.
还用到了会高亮
出现漏洞代码的addRange()
成员函数,并显示
给用户.
为了让静态分析器工具识别新的检查器
,要在源码
中定义一个注册函数
,然后在TableGen
文件中添加检查器的描述
.注册函数
如下:
void ento::registerReactorChecker(CheckerManager &mgr) {
mgr.registerChecker<ReactorChecker>();
}
TableGen
文件有个检查器表
.相对Clang
源码目录,它在lib/StaticAnalyzer/Checkers/Checkers.td
.编辑
该文件前,需要选择放置
检查器的包.
把它放在alpha.powerplant
中.它还不存在,因此要创建
它.打开Checkers.td
,在所有已有包定义
后添加一个新的定义
:
def PowerPlantAlpha : Package<"powerplant">, InPackage<Alpha>;
下面,添加新写的检查器
:
let ParentPackage = PowerPlantAlpha in {
def ReactorChecker : Checker<"ReactorChecker">,
HelperText<"Check for misuses of the nuclear power plant API">,
DescFile<"ReactorChecker.cpp">;
} //结束`"alpha.powerplant"`
如果用CMake
构建Clang
,应该添加你的新源文件
到lib/StaticAnalyzer/Checkers/CMakeLists.txt
.
如果用GNU
自动工具配置
脚本以构建Clang
,就不需要修改其它文件,因为LLVM
的Makefile
会扫描Checkers
目录中的新源码文件
,并在静态分析器的检查器库
中链接
它们.
进入构建LLVM
和Clang
的目录,运行make
.现在构建
系统会检测到你的新代码
,构建
它,并让Clang
静态分析器链接
它.
构建
后,命令行clang -cc1-analyzer-checker-help
就应该按合法选项
列举出新检查器
.
下面给出了检查器
的测试案例
,managereactor.c
(和前面
的相同):
int SCRAM();
int turnReactorOn();
void test_loop(int wrongTemperature, int restart) {
turnReactorOn();
if (wrongTemperature) {
SCRAM();
}
if (restart) {
SCRAM();
}
turnReactorOn();
//让反应堆工作的代码
SCRAM();
}
要用新检查器
分析以上代码,如下:
$ clang -analyze -Xanalyzer -analyzer-check=alpha.powerplant mamagereactor.c
检查器会显示
它可发现为错误
的路径
并退出.如果请求HTML
报告,就会看到一个漏洞报告
.
Clang静态分析器
开发检查器手册
从理论层面详细解释了解析器核心
所实现的内存模型
.
文档.