2312llvm,07clang静态分析器

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++编译器命令的CCCXX环境变量,这样就引入了项目普通的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,它会覆盖CCCXX环境变量.

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.第一个代表当前状态当前执行环境.
第二个代表程序流中的在语句前面或后面具体点.
最后代表整个可达程序状态的图.另外,该图节点是由ProgramStateProgramPoint元组表示,即,每个程序点都有具体状态和它关联.

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();
}

如果wrongTemperaturerestart都不是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()时的漏洞.

应该在匿名名字空间中,封装刚编写的ReactorStateReactorChecker类.这样避免链接器导出这两个数据结构,从而只在本地使用.

编写寄存器宏

深入实现类前,必须调用解析器引擎结合自定义状态用的一个宏来展开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"),

构造器用OwningPtrreset()成员函数,实例化了一个新的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(1)语句中,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,就不需要修改其它文件,因为LLVMMakefile会扫描Checkers目录中的新源码文件,并在静态分析器的检查器库链接它们.

编译和测试

进入构建LLVMClang的目录,运行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静态分析器
开发检查器手册
从理论层面详细解释了解析器核心所实现的内存模型.
文档.

你可能感兴趣的:(llvm,llvm)