分享

How to use Clang Static Analyzer

 没原创_去搜索 2015-12-30
http://blog.csdn.net/sealjin/article/details/45221209


Introduction

Clang

宏观上讲,Clang是一个项目名称。微观上,类似于GCC,Clang是一个C语言、C++、Objective C语言的轻量级编译器,它是Clang项目的一部分。

相比较于GCC,Clang的编译速度更快,占用的内存更少。Clang的错误提示与警告信息也比GCC更加准确清晰。此外,Clang基于库的模块化设计,易于IDE的集成并且遵循LLVM BSD协议。

Clang Static Analyzer

Clang Static Analyzer是一个能查找C语言、C++、Objective-C(C语言家族)漏洞的源码分析工具。

目前,Clang Static Analyzer可以作为一个独立的工具,也可以在Xcode开发环境(Mac os)中运行。Clang Static Analyzer作为一个独立的工具可以从命令行(如ubuntu的终端)中启动,并且它运行在构建一个代码库过程中。

Clang Static Analyzer作为Clang项目的一部分,是一个百分之百开源的软件。就像Clang编译器一样,Clang Static Analyzer可以像一个C++库一样集成到其他应用程序中。

scan-build & scan-view

scan-build是一个命令行工具,它能够帮助使用者运行静态分析器(static analyzer)检查源代码,使其能正常的构建。静态分析器与代码的编译是互不影响并且同步执行的,即:当一个项目在构建中,源码会被静态分析器分析并查找源码的漏洞。如:

$ scan-build make

当构建完成时,结果将会以一个web网页的形式呈现给使用者。类似于scan-build,scan-view也是一个命令行工具。它能打开由scan-build生成的web网页,显示bug报告。


How to install

不同平台Clang的安装方法有所不同:Mac OS X环境下安装方法:MAC OS CLANG,本文介绍Ubuntu系统下的安装方法,除Mac OS X的其他平台都与ubuntu类似。

apt-get 安装

用apt-get直接安装,只需要在终端中输入:

$ sudo apt-get install clang

这种方法适用于对版本要求不高的用户,优点是简单快速。

源码安装

如果对Clang版本要求很高,就需要手动对Clang源代码编译安装,这也是官网建议的安装方法。这种方法优点是能得到最新版本,缺点是耗时麻烦。

  1. 安装必要工具:
    $ sudo apt-get install subversion
    $ sudo apt-get install g++
    
  2. 下载LLVM系统和Clang最新源码:
    • 用svn下载LLVM源码
      • 将目录切换到想要放LLVM源码的目录
      • 输入如下代码:
        $ sudo svn co http:///svn/llvm-project/llvm/trunk llvm
        
    • 下载Clang源码
      $ cd llvm/tools/
      $ sudo svn co http:///svn/llvm-project/cfe/trunk clang
      $ cd ../..
      
    • 下载额外Clang工具(可选)
      $ cd llvm/tools/clang/tools
      $ sudo svn co http:///svn/llvm-project/clang-tools-extra/trunk extra
      $ cd ../../../..
      
    • 下载Compiler-RT
      $ cd llvm/projects
      $ sudo svn co http:///svn/llvm-project/compiler-rt/trunk compiler-rt
      $ cd ../..
      
  1. 编译Clang
    • 创建一个build目录用于存放执行configure后生成的文件
      $ mkdir build
      $ cd build
      $ ../llvm/configure --prefix=/usr/clang --enable-optimized --enable-targets=host
      $ sudo make -j2
      
    • 运行configure的时候,可以添加一些选项,选项的详细说明可见:CLANG CONFIGURATION 这里,
      1. --prefix=/usr/clang 选项用于在生成Makefile文件后,执行make install时指定将Clang安装到/usr/clang目录中。
      2. --enable-optimized 选项用于打开优化。默认不打开,生成的是Debug版本,非常占用内存并且时间是optimized版本的10倍。这里没有必要生成Debug版本。
      3. --enable-target=host 选项用于选择目标平台。默认是生成所有平台,这里只需要适合本机的情况就可以了。
    • make的选项-j2指定2个线程同时执行。(如果双核就-j2,四核就-j4)
    • 编译会花费一段时间,编译之后就可以make install了:
      $ make install
      
      由于--prefix选项指定了安装路径,故Clang就被安装在/usr/clang目录下。
    • 让bash能搜索到可执行程序clang。在/usr/bin下创建软链接到目标目录,ln -s ../clang/bin/clang (也可以修改PATH环境变量,但在这里不推荐,因为这样不容易让scan-build工具找到clang)
  2. 调整scan-build和scan-view目录
    官网给的源码make install并不会安装scan-build和scan-view工具。编译完成后,编译出的scan-build和scan-view分别在$(SRCDIR)/tools/clang/tools/scan-build和$(SRCDIR)/tools/clang/tools/scan-view中($(SRCDIR)是下载的llvm路径)。为防止误删除这两个目录,通常情况下,会把这两个目录放到根目录下(比如放到/usr/share/clang下),如果当前目录在llvm目录下。
    $ sudo mkdir /usr/share/clang/scan-build -p
    $ sudo cp ./tools/clang/tools/scan-build/* /usr/share/clang/scan-build/
    $ sudo mkdir /usr/share/clang/scan-view -p
    $ sudo cp -r ./tools/clang/tools/scan-view/* /usr/share/clang/scan-view/
    $ cd /usr/bin
    $ sudo ln -s ../share/clang/scan-build/scan-build
    $ sudo ln -s ../share/clang/scan-view/scan-view
    
    至此,scan-build和scan-view工具就可以使用了,但是由于源码脚本并没有指定clang的路径,所以每次使用scan-build工具时必须写出clang的路径。这样非常麻烦,解决这个问题,可以修改scan-build脚本。
    sudo vi /usr/bin/scan-build
    # Find 'clang'
    if (!defined $AnalyzerDiscoveryMethod) {
    - -  $Clang = Cwd::realpath("$RealBin/bin/clang");
    +  $Clang = Cwd::realpath("/usr/bin/clang");
       if (!defined $Clang || ! -x $Clang) {
         $Clang = Cwd::realpath("$RealBin/clang");
       }
    
    即:将第一个$RealBin改为/usr。这样就向scan-build指定了Clang的位置。Clang Static Analyzer就完全安装完成了。

Standard Operation Procedure

Clang编译器

与GCC相类似,Clang也集成预编译器、汇编器和链接器于一体,使用的语法格式也为:

clang [options] <inputs>

其大多数编译选项也与GCC类似,如-E只执行预编译、-S生成汇编语言文件(以.s结尾)、-c选项只编译不链接等。详细的选项说明可以man clang或者clang -help查看。Clang编译器的用户手册详见:CLANG COMPILER USER‘S MANUAL
与GCC不同的一点是使用--analyze选项可以启动静态分析器,如

$ clang --analyze test.c

在在运行这条指令时,Clang Static Analyzer会被启动,分析test.c中的bug。

scan-build和scan-view

  • scan-build
    1. 开始
      scan-build本质上是一个唤起Clang Static Analyzer的Perl脚本。scan-build命令会介入到整个工程的构建过程中去分析这个工程。这就意味着,没有被gcc或是clang编译的文件是不会被分析的。
    2. 基本使用
      scan-build的基本使用方式很简单,只需要在命令行开头输入scan-build即可。
      $ scan-build make
      
      在make整个工程的同时,scan-build启动静态分析器分析正在构建的的工程代码。下面是scan-build命令的通用格式:
      $ scan-build [scan-build options] <command> [command options]  
      
      scan-build会逐个运行这些命令,其参数也是按顺序执行。例如,在make命令中传入一个 -j4参数,结果就是用4核并行编译:
      $ scan-build make -j4
      
      scan-build也可以用来分析具体的文件:
      $ scan-build gcc -c test1.c  test2.c
      
      这个命令实现对test1.c和test2.c文件的分析。
    3. 其他选项(scan-build options)
      scan-build支持很多选项,下面是一些常用选项:
      1. -o (output)html报告文件存放目录。可以按照需要创建一些子目录,来区分每个运行的分析器。如果没有指定这个参数,默认将报告文件保存在/tmp目录下。
      2. -h (help)显示scan-build的所有参数。
      3. -k (keep on going)增加一个连续运行的参数到具体的命令中。这个选项目前只支持make和xcodebuild。
      4. -v (verbose)冗余输出,用于输出更详细的分析结果。两个或三个-v选项可以增加冗余度,关于bug的分析报告也就更多。
      5. -V (view)运行指令后立即打开浏览器显示报告结果。
      6. -plist 输出的结果保存成.plist文件。
      7. -plist-html 输出的结果保存成html和.plist两种文件格式。
      8. --use-c++[=compiler_path] 使用默认的C++和Objective-C++编译器,或指定路径使用指定的编译器。
      9. --use-cc[=compiler_path] 使用默认的C或Objective-C编译器,或指定路径使用指定的编译器。
    4. 更多scan-build选项可以man scan-build或者scan-build -h查看。
    5. scan-build的输出结果
      scan-build的输出结果是一个HTML文件集合,每个html文件代表一个独立的缺陷报告。index.html文件是用来查询所有的缺陷。可以用浏览器打开index.html文件查看所有缺陷报告。html报告文件的存放路径是由-o选项指定的,默认是保存在/tmp目录下。scan-build运行后会打印出报告所在路径。如果想在命令执行完之后立即去查看报告,那么可以传入一个-V参数。
    6. 注意事项
      如果要分析的工程用configure脚本自动生成makefile文件,那么就需要通过scan-build来执行configure配置文件。
      $ scan-build ./configure
      $ scan-build make
      
      configure文件需要通过scan-build运行的原因是静态分析器是通过介入编译器来分析源码的。这种介入是通过暂时地设置环境变量CC成ccc-analyzer。ccc-analyzer作为一个伪编译器,转发命令行参数给gcc或是clang来执行静态分析。
  • scan-viewscan-view工具用来查看由scan-build生成的html报告文件。其具体的用法为:
    scan-view [options] <results directory>
    
    scan-view的选项可以通过scan-view -h或者man scan-view查看。<results directory>是由scan-build生成的存放html文件的路径。scan-build执行完成后会提示出如何用scan-view查看html报告。

checkers

Clang Static Analyzer就是利用不同的checker来检测源码不同类型的bug的。静态分析器会默认使用6类checkers(default checker):

  • Core Checkers:提供一些一般性的检查,比如是否被0除、是否使用空指针和使用未初始化参数等。
  • C++ Checkers:提供C++检查。
  • Dead Code Checkers:检查没有使用的代码。
  • OS X Checkers:检查Objective-C和Apple's SDKs的使用情况。
  • Security Checkers:检查不安全API的使用和基于CERT Secure Coding Standards的检查。
  • Unix Checkers:检查Unix和POSIX API的使用情况。

其中,每一类的checkers中包含有不同的checker负责检查不同细分类型的bug。比如core checkers类中的一个checker: core.CallAndMessage 该checker主要负责检查函数调用的逻辑错误或者信息表达的错误,比如未初始化的参数,空的函数指针等。例如:

// C
struct S {
  int x;
};

void f(struct S s);

void test() {
  struct S s;
  f(s); // warn: passed-by-value arg contain uninitialized data
}

可以使用选项--help-checkers来查看默认checkers列表。-enable-checker和-disable-checker用来使用和禁用checker,例如

$ scan-build --help-checkers
$ scan-build -enable-checker core.CallAndMessage gcc test.c
$ scan-build -disable-checker core.CallAndMessage gcc test.c

如果不禁止使用某个checker,scan-build会自动使用默认的checkers。详细的checkers功能介绍请看后面的章节“The functions of default checkers”或AVAILABLE CHECKERS


The flow chart of scan-build & ccc-analyzer

用来启动静态分析器的scan-build是一个perl脚本。ccc-analyzer与scan-build一样,也是一个perl脚本。ccc-analyzer脚本是一个中间文件,用户不直接接触,但是会被scan-build调用。

scan-build flow chart


以上为scan-build脚本执行的前半部分,用来处理scan-build option。

第二部分的scan-build主要处理command和command option。
scan-build总结:

  1. 不管是编译单独文件还是整个工程,scan-build 都不直接启动clang,而是启动ccc-analyzer或c++-analyzer
  2. [scan-build option]经过scan-build处理,以环境变量的形式传递给ccc-analyzer
  3. [command option]会全部传递给ccc-analyzer
  4. scan-build在make工程时,通过修改(并只会修改)makefile中CC或CXX变量,将变量中指定的编译器全部换成ccc-analyzer或c++-analyzer。即:makefile中CC或CXX变量中指定的编译器更本就是没有任何作用的!
  5. 所以当用scan-build管理工程时,必须用--use-cc或--use-c++指定编译器

ccc-analyzer flow chart


ccc-analyzer脚本流程总结:

  1. 环境变量CCC_CCC做为最终的编译器
  2. 如果环境变量CCC_CC没有指定,会用默认的gcc或g++编译
  3. system()函数是阻塞型函数。即先编译,后启动静态分析器

How to use scan-build to find bugs

分析一个使用未初始化的函数指针的代码

  1. vi test.c输入如下代码:
    int main(){
      void (*foo)(void);
      foo();
    }
    
  2. 保存退出后,通过scan-build编译test.c
    $ scan-build gcc test.c
    
  3. scan-build会产生如下结果:
    scan-build: Using '/usr/bin/clang' for static analysis
    test.c:3:2: warning: Called function pointer is an uninitalized pointer value
            foo();
            ^~~~~
    1 warning generated.
    scan-build: 1 bugs found.
    scan-build: Run 'scan-view /tmp/scan-build-2014-06-27-164418-6424-1' to examine bug reports.
    
  4. 说明分析出了一个bug。按照提示用scan-view查看这个bug报告:
    $ scan-view /tmp/scan-build-2014-06-27-164418-6424-1
    
    scan-view会自动打开浏览器,显示报告信息,如下:

分析MIPS架构linux分位中的private apps

  1. 下载并编译linux分位源码
    步骤参见wiki:mDNS的章节avahi与netatalk。建议编译通过后再执行scan-build指令,防止系统环境问题干扰scan-build。一些常见的编译错误以及解决方法在wiki中也有解释。
  2. 修改maple_linux/userspace中makefile,使其使用scan-build启动clang静态分析器分析生成private apps的源码
     private-apps:
    -       $(MAKE) -C private/apps
    +       #$(MAKE) -C private/apps
    +       scan-build --use-cc /opt/toolchains/uclibc-crosstools-gcc-4.2.3-4/usr/bin/mips-linux-gcc $(MAKE) -C private/apps > ../1.txt
    
    将scan-build运行的结果输出重定向到1.txt是防止make的打印信息太多而找不到scan-build的输出结果。保存退出后在maple_linux目录下再次运行./build.sh
  3. 编译完成后在maple_linux目录下打开1.txt,根据最后一句提示用scan-view启动浏览器查看bug报告。结果如下:

The functions of default checkers

Clang Static Analyzer中default checkers共有6类,分别为:Core Checkers, C++ Checkers, Dead Code Checkers, OS X Checkers, Security Checkers和Unix Checkers。以下为除OS X Checkers的其他5类中各个checker的功能描述。






注:更多关于各个功能所能修改的代码示例详见:AVAILABLE CHECKERS


A example of a custom checker

Clang Static Analyzer之所以能够查找源代码中的bug就是因为checker的存在,所以,checker可以说是Analyzer的灵魂。尽管Analyzer有数十个自带的default checkers和experimental (alpha) Checkers,并且还在不停添加,但是这些checker永远不能包含所有可能出现bug的情况。幸运的是,用户可以根据自身的需求添加适合自己代码的checker,这很大的提高了Analyzer的灵活性。以下内容就来讨论checker的原理,以及如何写一个自定义checker。

1.明确checker需要解决的问题

当为防止数据同时被多个线程修改的时候,锁机制会被经常使用。在C/C++中,程序员必须保证上锁与解锁成对出现,但当函数功能的增多或是出现很多分支的时候,很容易造成double lock, double unlock, unreleased lock这些情况。由于Analyzer可以追踪程序的状态(Program State),所以它很适合检查这种bug。另一方面,上锁和解锁通常不会相隔很大距离(通常在一个函数内),而目前为止,Analyzer还并不支持跨越多文件检查。在这个例子中,假设上锁的函数名叫'lock',解锁的函数名叫'unlock'。为了简化这个例子,这两个函数都不带参数,即它们都代表一个锁。以下是三个代码中必须遵守的原则:

  • 所有调用lock()的函数,在返回之前必须调用unlock()来解锁----否则产生unreleased lock错误
  • 不允许有函数在已经调用lock()函数之后,紧接着又调用lock()----否则产生double lock错误
  • 不允许有函数在已经调用unlock()之后,紧接着又调用unlock()----否则产生double unlock错误

2.checker的结构

所有的checker都要继承自Checker模板类,这个模板类的参数描述了会调用这个checker的事件类型。事件类型以及对应事件的触发举例:

  • [check::PreStmt<xxx>] - 在statement xxx发生之前调用这个checker
  • [check::PostStmt<xxx>] - 在statement xxx发生之后调用这个checker
  • [check::PreCall] - 在函数调用之前调用这个checker
  • [check::EndFunction] - 在函数结束时调用这个checker
  • [check::BranchCondition] - 在分支出现时调用这个checker
  • [check::DeadSymbols] - 当参数超出生命周期时调用这个checker

更多详细的事件及事件功能可以参考:CheckerDocumentation.cpp在这个例子中,需要check::PreCall事件,用来判断调用的是lock()还是unlock()函数;check::EndFunction事件,用来查看函数结束时有没有unreleased lock的情况。对于每一种事件类型,当事件触发时,Analyzer都会回调checker中的回调函数。CheckerDocumentation.cpp中声明了这些回调函数。参照这些声明格式,例子中类的框架模型为:

class LockUnlockChecker : public Checker<check::PreCall,check::EndFunction > {
  ...
  public:
  LockUnlockChecker(void) {
  ...
  }
  void checkPreCall(const CallEvent &call, CheckerContext &C) const {
  ...
  }
  void checkEndFunction(CheckerContext &) const {
  ...
  }
};

3.程序状态(Program State)

Clang Static Analyzer就像其他静态分析工具一样,并不会执行源代码,而是象征性的执行代码(symbolic excution),并且会执行代码中的每一个分支(Path Sensitive)。在“执行”过程中,Analyzer会实时的根据运行情况追踪和改变程序状态(Program State)。举一个简单的例子,有如下代码:

void example_function(int a) {
  if(a)
    lock();
  ...
  if(a)
    unlock();
}

从这个例子中,代码关于上锁与解锁的使用是正确的。不管变量a为何值,代码的运行都满足之前指出的三条原则。Analyzer是如何运行这段代码的呢?在函数的开始,Analyzer会决定程序状态:

  • Value of 'a': any possible value
  • State of lock: unlocked

注:程序状态其他值并没有被列出,因为它们与本例无关。当“执行”到第一个if语句时,Analyzer并不能确定执行哪个分支,因为a的值无法被确定。所以,程序的状态被分成两个(针对两个分支):

  • Value of 'a': non-zero; State of lock: unlocked
  • Value of 'a': zero; State of lock: unlocked

在a非零的分支中,当执行到lock()函数时,程序的状态被改为:

  • Value of 'a': non-zero; State of lock: locked
  • Value of 'a': zero; State of lock: unlocked

这个状态将会保持到第二个if语句。当执行到第二个if语句时,因为这两个状态都定义了变量'a',所以状态不会再一次一分为二。a为非零的状态将被选择执行。最后,状态将变成:

  • Value of 'a': non-zero; State of lock: unlocked
  • Value of 'a': zero; State of lock: unlocked

因为上面的例子并没有bug出现,所以Analyzer不会报告bug。从这个例子可以总结,当PreCall与EndFunction事件被触发,Checker中的回调函数将被调用,checker的回调函数要么修改程序状态,要么报告bug。

4.Locked/Unlocked程序状态

知道什么样的信息应该存在程序状态中,那如何定义适合本例的程序状态?类ProgramState中定义了程序状态。程序状态主要包含以下三类信息:

  • 变量和表达式可能的值(在类Environment中)
  • 内存位置的值(在类Store中)
  • 与checker相关的值(checker-specific)

只有最后一类状态信息是需要所关心的,用来存储Locked/Unlocked状态。可以使用宏定义的方法来添加程序状态。对于这个例子,可以使用宏REGISTER_TRAIT_WITH_PROGRAMSTATE。这个宏有两个参数:给这类信息取的名称和信息的数据类型。此例中,信息的名称可以叫'LockState',信息的存储类型用'bool'。即:

REGISTER_TRAIT_WITH_PROGRAMSTATE(LockState, bool)

除了宏REGISTER_TRAIT_WITH_PROGRAMSTATE,还有REGISTER_MAP_WITH_PROGRAMSTATEREGISTER_SET_WITH_PROGRAMSTATEREGISTER_LIST_WITH_PROGRAMSTATE宏也可以用来定义checker相关的信息。状态信息添加好后,就可以用函数get()set()分别来获得和设置状态信息。比如:

ProgramStateRef state;
...
bool currentlyLocked = state->get<LockState>();
...
state = state->set<LockState>(true);

5.代码实现

根据checker的结构和程序状态的定义,部分代码实现如下:

void checkPreCall(const CallEvent & call, CheckerContext &C) const {
  const IdentifierInfo * identInfo = call.getCalleeIdentifier();
  if(!identInfo) {
    return;
  }
  std::string funcName = std::string(identInfo->getName());
  ProgramStateRef state = C.getState();
  if(funcName.compare("lock") == 0) {
    bool currentlyLocked = state->get<LockState>();
      if(currentlyLocked) {
        ... (emit warning about double lock) ...
      }
    state = state->set<LockState>(true);
    C.addTransition(state);
  } else if(funcName.compare("unlock") == 0) {
    bool currentlyLocked = state->get<LockState>();
    if(!currentlyLocked) {
      ... (emit warning about double unlock) ...
    }
    state = state->set<LockState>(false);
    C.addTransition(state);
  }
}
void checkEndFunction(CheckerContext &C) const {
  ProgramStateRef state = C.getState();
  bool currentlyLocked = state->get<LockState>();
  if(currentlyLocked) {
    ... (emit warning about returning without unlocking) ...
  }
}

6.报告bug

报告bug设计两个类:BugTypeBugReport。BugType代表bug的类型,因为有三个bug(double lock, double unlock, unreleased lock),所以在checker中需要定义三个BugType。将这三个BugType定义在LockUnlockChecker类中,并在构造函数中初始化。BugReport类代表一个特定发生的bug。BugReport的三个参数为:

  • Bug的类型
  • 当bug发生是出现的描述提示字符串
  • bug出现的位置

当报告bug时,一般情况需要转变当前程序状态成为Sink Node。Sink Node可以阻止Analyzer沿着这条路径继续分析,从而防止额外的bug沿着这条路径出现。由CheckerContext类中的函数generateSink()产生Sink Node,并且返回的指针代表着Sink node的位置(即可以作为bug的位置参数)。

ExplodedNode * bugloc = C.generateSink();
if(bugloc) {
  BugReport * bug = new BugReport(*DoubleLockBugType,
    "Call to lock when already locked", bugloc);
  C.EmitReport(bug);
}

Building a Checker in 24 Hours是一个官方提供的另一个checker例子。clang提供的所有API函数见:Clang API


Register and test the custom checker

有两种方法注册自定义checker,一种是将自定义checker作为experimental checker编译到clang可执行程序中,另一种是将自定义checker单独编译成.so共享库文件,在使用时动态加载。第一种方式适合已经测试好的checker,而第二种方式更适合调试阶段的checker。

Register and test as a experimental checker

对于从官网下载源码编译的情况,所有的checker文件都放在clang/lib/StaticAnalyzer/Checkers目录下(apt-get的安装方法不能将自定义checker编译到clang可执行程序中)。以上面的例子为例,以下说明如何注册一个用户自己写的checker:LockUnlockChecker?

  1. 创建一个新的checker,LockUnlockChecker.cpp。将这个文件放在lib/StaticAnalyzer/Checkers目录下。
  2. 将如下代码放在LockUnlockChecker.cpp最后:
    void ento::registerLockUnlockChecker(CheckerManager &mgr) {
      mgr.registerChecker<LockUnlockChecker>();
    }
    
  3. 这个Checker还需要在clang/lib/StaticAnalyzer/Checkers/Checkers.td文件中定义。因为这个checker是刚创建的,所以必须属于‘alpha’(测试)版本。并且,根据这个checker的功能判断属于上文说明6类中的哪一类。在此例中,假设用LockUnlockChecker测试Unix系统的上锁情况。所以,可以把这个checker定义在'alpha.unix'包中。将如下代码放入Checkers.td文件中定义‘alpha.unix'包中checker的位置。
    let ParentPackage = UnixAlpha in {
    ...
    def LockUnlockChecker : Checker<"LockUnlock">,
      HelpText<"Checker for use of lock()/unlock()">,
      DescFile<"LockUnlockChecker.cpp">;
    ...
    } // end "alpha.unix"
    

注意:

  • 加在Checkers.td文件中的代码Checker<"LockUnlock">中,"LockUnlock"将与所在的包名组成新添加checker的名称。例如此例中,checker的名称为alpha.unix.LockUnlock。该名称也可以自定义成别的。
  • Checkers.td的def LockUnlockChecker意为定义ento::registerLockUnlockChecker(CheckerManager &)这个函数。所以,def后面的部分必须与LockUnlockChecker.cpp文件中函数ento::registerXXX(CheckerManager?&)保持一致。
  • LockUnlockChecker.cpp中mgr.registerChecker<LockUnlockChecker>的LockUnlockChecker为自定义的类名,所以要与上面代码class LockUnlockChecker保持一致。
  • HelpText<"">中为对alpha.unix.LockUnlock功能的说明。
  • DescFile<"">中为alpha.unix.LockUnlock的源文件名。
  1. 完成两个文件代码的添加后,按照上文安装方法中步骤重新编译安装。两个文件可见附件中的[LockUnlockChecker.cpp]和[Checkers.td]
  2. 测试
    1. 完成代码的注册并重新编译安装后,输入以下指令查看clang支持的checker中是否有刚添加进去的checker:
      $ clang -cc1 -analyzer-checker-help 
      
    2. 写一段测试代码,可以使用附件中的lock_test.c
    3. 运行注:新添加的checker不是default checker,所以在使用新添加的checker查bug时需要用scan-build的选项-enable-checker将新checker添加进来:
      $ scan-build -enable-checker alpha.unix.LockUnlock gcc -c lock_test.c
      

Register and test as a plugin

用apt-get安装和用源码编译安装的方法都支持链接共享库的方式加载自定义checker。

  1. 为了能让clang准确加载插件,插件必须暴露一些符号给clang。符号CLANG_ANALYZER_API_VERSION_STRING用来指明插件所用API的版本。在clang源文件中,API版本由预编译变量CLANG_ANALYZER_API_VERSION_STRING提供。所以:
    extern "C" const char clang_analyzerAPIVersionString[] = CLANG_ANALYZER_API_VERSION_STRING;
    
    extern "C" 确保符号名不会被破坏。第二个插件必须输出的符号是clang_registerCheckers(CheckerRegistry &registry)。这个函数在加载插件时将会被调用。
    extern "C" void clang_registerCheckers(CheckerRegistry &registry) {
      registry.addChecker<LockUnlockChecker>("example.LockUnlockChecker", "Checker for use of lock()/unlock()");
    }
    
    当注册插件时,CheckerRegistry类中的成员函数addChecker会被调用,它包含有三个参数:
    • checker中的类名
    • checker名
    • 对这个checker功能的描述
  2. 将源文件编译成库文件
    需要用到可执行程序llvm-config。如果用apt-get安装方法,一般在目录/usr/lib/llvm-X.X/bin/llvm-config下(X.X为llvm版本号)。源码安装一般在目录build/Release+Asserts/bin/llvm-config。以下以源码安装为例:
    g++ -shared -fPIC `~/llvm/build/Release+Asserts/bin/llvm-config --cxxflags` -I`~/llvm/build/Release+Asserts/bin/llvm-config --src-root`/tools/clang/include -I`~/llvm/build/Release+Asserts/bin/llvm-config --obj-root`/tools/clang/include -o LockUnlockChecker.so LockUnlockChecker.cpp
    
  3. 测试可用附件中代码lock_test.c测试。
    scan-build -load-plugin ./LockUnlockChecker.so -enable-checker example.LockUnlockChecker gcc -c lock_test.c 
    

Notice

Clang options

以上的命令中涉及到很多选项,但需要区别clangclang -cc1clang -cc1是专门给开发者使用的,包含clang项目的所有功能。而clang相当于一个driver(驱动),是专门给用户使用,他兼容GCC的使用方法,以方便熟悉GCC的用户使用。例如,与gcc类似,clang命令也可以驱动预处理器、汇编器、编译器、连接器甚至静态分析器。 当使用命令行命令clang时,clang会自动产生一系列适合当前系统的选项并传递给clang -cc1。所以clangclang -cc1选项不能混用。

Compatibility

由于Clang项目目前仍处于开发阶段,不同版本之间有不少区别。(亲测在3.2版本的代码不能直接在3.5版本中运行)

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多