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源代码编译安装,这也是官网建议的安装方法。这种方法优点是能得到最新版本,缺点是耗时麻烦。
- 安装必要工具:
$ sudo apt-get install subversion $ sudo apt-get install g++
- 下载LLVM系统和Clang最新源码:
- 用svn下载LLVM源码
- 将目录切换到想要放LLVM源码的目录
- 输入如下代码:
$ sudo svn co http://llvm.org/svn/llvm-project/llvm/trunk llvm
- 下载Clang源码
$ cd llvm/tools/ $ sudo svn co http://llvm.org/svn/llvm-project/cfe/trunk clang $ cd ../..
- 下载额外Clang工具(可选)
$ cd llvm/tools/clang/tools $ sudo svn co http://llvm.org/svn/llvm-project/clang-tools-extra/trunk extra $ cd ../../../..
- 下载Compiler-RT
$ cd llvm/projects $ sudo svn co http://llvm.org/svn/llvm-project/compiler-rt/trunk compiler-rt $ cd ../..
- 用svn下载LLVM源码
- 编译Clang
- 创建一个build目录用于存放执行configure后生成的文件
$ mkdir build $ cd build $ ../llvm/configure --prefix=/usr/clang --enable-optimized --enable-targets=host $ sudo make -j2
- 运行configure的时候,可以添加一些选项,选项的详细说明可见:CLANG CONFIGURATION 这里,
- --prefix=/usr/clang 选项用于在生成Makefile文件后,执行make install时指定将Clang安装到/usr/clang目录中。
- --enable-optimized 选项用于打开优化。默认不打开,生成的是Debug版本,非常占用内存并且时间是optimized版本的10倍。这里没有必要生成Debug版本。
- --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)
- 创建一个build目录用于存放执行configure后生成的文件
- 调整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
- 开始
scan-build本质上是一个唤起Clang Static Analyzer的Perl脚本。scan-build命令会介入到整个工程的构建过程中去分析这个工程。这就意味着,没有被gcc或是clang编译的文件是不会被分析的。
- 基本使用
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文件的分析。
- 其他选项(scan-build options)
scan-build支持很多选项,下面是一些常用选项:
- -o (output)html报告文件存放目录。可以按照需要创建一些子目录,来区分每个运行的分析器。如果没有指定这个参数,默认将报告文件保存在/tmp目录下。
- -h (help)显示scan-build的所有参数。
- -k (keep on going)增加一个连续运行的参数到具体的命令中。这个选项目前只支持make和xcodebuild。
- -v (verbose)冗余输出,用于输出更详细的分析结果。两个或三个-v选项可以增加冗余度,关于bug的分析报告也就更多。
- -V (view)运行指令后立即打开浏览器显示报告结果。
- -plist 输出的结果保存成.plist文件。
- -plist-html 输出的结果保存成html和.plist两种文件格式。
- --use-c++[=compiler_path] 使用默认的C++和Objective-C++编译器,或指定路径使用指定的编译器。
- --use-cc[=compiler_path] 使用默认的C或Objective-C编译器,或指定路径使用指定的编译器。
- 更多scan-build选项可以man scan-build或者scan-build -h查看。
- scan-build的输出结果
scan-build的输出结果是一个HTML文件集合,每个html文件代表一个独立的缺陷报告。index.html文件是用来查询所有的缺陷。可以用浏览器打开index.html文件查看所有缺陷报告。html报告文件的存放路径是由-o选项指定的,默认是保存在/tmp目录下。scan-build运行后会打印出报告所在路径。如果想在命令执行完之后立即去查看报告,那么可以传入一个-V参数。
- 注意事项
如果要分析的工程用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总结:
- 不管是编译单独文件还是整个工程,scan-build 都不直接启动clang,而是启动ccc-analyzer或c++-analyzer
- [scan-build option]经过scan-build处理,以环境变量的形式传递给ccc-analyzer
- [command option]会全部传递给ccc-analyzer
- scan-build在make工程时,通过修改(并只会修改)makefile中CC或CXX变量,将变量中指定的编译器全部换成ccc-analyzer或c++-analyzer。即:makefile中CC或CXX变量中指定的编译器更本就是没有任何作用的!
- 所以当用scan-build管理工程时,必须用--use-cc或--use-c++指定编译器
ccc-analyzer flow chart
ccc-analyzer脚本流程总结:
- 环境变量CCC_CCC做为最终的编译器
- 如果环境变量CCC_CC没有指定,会用默认的gcc或g++编译
- system()函数是阻塞型函数。即先编译,后启动静态分析器
How to use scan-build to find bugs
分析一个使用未初始化的函数指针的代码
- vi test.c输入如下代码:
int main(){ void (*foo)(void); foo(); }
- 保存退出后,通过scan-build编译test.c
$ scan-build gcc test.c
- 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.
- 说明分析出了一个bug。按照提示用scan-view查看这个bug报告:
$ scan-view /tmp/scan-build-2014-06-27-164418-6424-1
scan-view会自动打开浏览器,显示报告信息,如下:
分析MIPS架构linux分位中的private apps
- 下载并编译linux分位源码
步骤参见wiki:mDNS的章节avahi与netatalk。建议编译通过后再执行scan-build指令,防止系统环境问题干扰scan-build。一些常见的编译错误以及解决方法在wiki中也有解释。
- 修改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
- 编译完成后在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_PROGRAMSTATE、REGISTER_SET_WITH_PROGRAMSTATE和REGISTER_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设计两个类:BugType和BugReport。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?。
- 创建一个新的checker,LockUnlockChecker.cpp。将这个文件放在lib/StaticAnalyzer/Checkers目录下。
- 将如下代码放在LockUnlockChecker.cpp最后:
void ento::registerLockUnlockChecker(CheckerManager &mgr) { mgr.registerChecker<LockUnlockChecker>(); }
- 这个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的源文件名。
- 完成两个文件代码的添加后,按照上文安装方法中步骤重新编译安装。两个文件可见附件中的[LockUnlockChecker.cpp]和[Checkers.td]
- 测试
- 完成代码的注册并重新编译安装后,输入以下指令查看clang支持的checker中是否有刚添加进去的checker:
$ clang -cc1 -analyzer-checker-help
- 写一段测试代码,可以使用附件中的lock_test.c
- 运行注:新添加的checker不是default checker,所以在使用新添加的checker查bug时需要用scan-build的选项-enable-checker将新checker添加进来:
$ scan-build -enable-checker alpha.unix.LockUnlock gcc -c lock_test.c
- 完成代码的注册并重新编译安装后,输入以下指令查看clang支持的checker中是否有刚添加进去的checker:
Register and test as a plugin
用apt-get安装和用源码编译安装的方法都支持链接共享库的方式加载自定义checker。
- 为了能让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 ®istry)。这个函数在加载插件时将会被调用。
extern "C" void clang_registerCheckers(CheckerRegistry ®istry) { registry.addChecker<LockUnlockChecker>("example.LockUnlockChecker", "Checker for use of lock()/unlock()"); }
当注册插件时,CheckerRegistry类中的成员函数addChecker会被调用,它包含有三个参数:
- checker中的类名
- checker名
- 对这个checker功能的描述
- 将源文件编译成库文件
需要用到可执行程序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
- 测试可用附件中代码lock_test.c测试。
scan-build -load-plugin ./LockUnlockChecker.so -enable-checker example.LockUnlockChecker gcc -c lock_test.c
Notice
Clang options
以上的命令中涉及到很多选项,但需要区别clang与clang -cc1。clang -cc1是专门给开发者使用的,包含clang项目的所有功能。而clang相当于一个driver(驱动),是专门给用户使用,他兼容GCC的使用方法,以方便熟悉GCC的用户使用。例如,与gcc类似,clang命令也可以驱动预处理器、汇编器、编译器、连接器甚至静态分析器。 当使用命令行命令clang时,clang会自动产生一系列适合当前系统的选项并传递给clang
-cc1。所以clang与clang -cc1选项不能混用。
Compatibility
由于Clang项目目前仍处于开发阶段,不同版本之间有不少区别。(亲测在3.2版本的代码不能直接在3.5版本中运行)