宏观上讲,Clang是一个项目名称。微观上,类似于GCC,Clang是一个C语言、C++、Objective C语言的轻量级编译器,它是Clang项目的一部分。
相比较于GCC,Clang的编译速度更快,占用的内存更少。Clang的错误提示与警告信息也比GCC更加准确清晰。此外,Clang基于库的模块化设计,易于IDE的集成并且遵循LLVM BSD协议。
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是一个命令行工具,它能够帮助使用者运行静态分析器(static analyzer)检查源代码,使其能正常的构建。静态分析器与代码的编译是互不影响并且同步执行的,即:当一个项目在构建中,源码会被静态分析器分析并查找源码的漏洞。如:
$ scan-build make当构建完成时,结果将会以一个web网页的形式呈现给使用者。类似于scan-build,scan-view也是一个命令行工具。它能打开由scan-build生成的web网页,显示bug报告。
不同平台Clang的安装方法有所不同:Mac OS X环境下安装方法:MAC OS CLANG,本文介绍Ubuntu系统下的安装方法,除Mac OS X的其他平台都与ubuntu类似。
用apt-get直接安装,只需要在终端中输入:
$ sudo apt-get install clang
这种方法适用于对版本要求不高的用户,优点是简单快速。
如果对Clang版本要求很高,就需要手动对Clang源代码编译安装,这也是官网建议的安装方法。这种方法优点是能得到最新版本,缺点是耗时麻烦。
$ sudo apt-get install subversion $ sudo apt-get install g++
$ sudo svn co http://llvm.org/svn/llvm-project/llvm/trunk llvm
$ cd llvm/tools/ $ sudo svn co http://llvm.org/svn/llvm-project/cfe/trunk clang $ cd ../..
$ cd llvm/tools/clang/tools $ sudo svn co http://llvm.org/svn/llvm-project/clang-tools-extra/trunk extra $ cd ../../../..
$ cd llvm/projects $ sudo svn co http://llvm.org/svn/llvm-project/compiler-rt/trunk compiler-rt $ cd ../..
$ mkdir build $ cd build $ ../llvm/configure --prefix=/usr/clang --enable-optimized --enable-targets=host $ sudo make -j2
$ make install由于--prefix选项指定了安装路径,故Clang就被安装在/usr/clang目录下。
$ 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就完全安装完成了。
与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 make在make整个工程的同时,scan-build启动静态分析器分析正在构建的的工程代码。下面是scan-build命令的通用格式:
$ scan-build [scan-build options] <command> [command options]scan-build会逐个运行这些命令,其参数也是按顺序执行。例如,在make命令中传入一个 -j4参数,结果就是用4核并行编译:
$ scan-build make -j4scan-build也可以用来分析具体的文件:
$ scan-build gcc -c test1.c test2.c这个命令实现对test1.c和test2.c文件的分析。
$ scan-build ./configure $ scan-build makeconfigure文件需要通过scan-build运行的原因是静态分析器是通过介入编译器来分析源码的。这种介入是通过暂时地设置环境变量CC成ccc-analyzer。ccc-analyzer作为一个伪编译器,转发命令行参数给gcc或是clang来执行静态分析。
scan-view [options] <results directory>scan-view的选项可以通过scan-view -h或者man scan-view查看。<results directory>是由scan-build生成的存放html文件的路径。scan-build执行完成后会提示出如何用scan-view查看html报告。
Clang Static Analyzer就是利用不同的checker来检测源码不同类型的bug的。静态分析器会默认使用6类checkers(default checker):
其中,每一类的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
用来启动静态分析器的scan-build是一个perl脚本。ccc-analyzer与scan-build一样,也是一个perl脚本。ccc-analyzer脚本是一个中间文件,用户不直接接触,但是会被scan-build调用。
以上为scan-build脚本执行的前半部分,用来处理scan-build option。
第二部分的scan-build主要处理command和command option。
scan-build总结:
ccc-analyzer脚本流程总结:
int main(){ void (*foo)(void); foo(); }
$ scan-build gcc test.c
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.
$ scan-view /tmp/scan-build-2014-06-27-164418-6424-1scan-view会自动打开浏览器,显示报告信息,如下:
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
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
Clang Static Analyzer之所以能够查找源代码中的bug就是因为checker的存在,所以,checker可以说是Analyzer的灵魂。尽管Analyzer有数十个自带的default checkers和experimental (alpha) Checkers,并且还在不停添加,但是这些checker永远不能包含所有可能出现bug的情况。幸运的是,用户可以根据自身的需求添加适合自己代码的checker,这很大的提高了Analyzer的灵活性。以下内容就来讨论checker的原理,以及如何写一个自定义checker。
当为防止数据同时被多个线程修改的时候,锁机制会被经常使用。在C/C++中,程序员必须保证上锁与解锁成对出现,但当函数功能的增多或是出现很多分支的时候,很容易造成double lock, double unlock, unreleased lock这些情况。由于Analyzer可以追踪程序的状态(Program State),所以它很适合检查这种bug。另一方面,上锁和解锁通常不会相隔很大距离(通常在一个函数内),而目前为止,Analyzer还并不支持跨越多文件检查。在这个例子中,假设上锁的函数名叫‘lock‘,解锁的函数名叫‘unlock‘。为了简化这个例子,这两个函数都不带参数,即它们都代表一个锁。以下是三个代码中必须遵守的原则:
所有的checker都要继承自Checker模板类,这个模板类的参数描述了会调用这个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 { ... } };
Clang Static Analyzer就像其他静态分析工具一样,并不会执行源代码,而是象征性的执行代码(symbolic excution),并且会执行代码中的每一个分支(Path Sensitive)。在“执行”过程中,Analyzer会实时的根据运行情况追踪和改变程序状态(Program State)。举一个简单的例子,有如下代码:
void example_function(int a) { if(a) lock(); ... if(a) unlock(); }从这个例子中,代码关于上锁与解锁的使用是正确的。不管变量a为何值,代码的运行都满足之前指出的三条原则。Analyzer是如何运行这段代码的呢?在函数的开始,Analyzer会决定程序状态:
注:程序状态其他值并没有被列出,因为它们与本例无关。当“执行”到第一个if语句时,Analyzer并不能确定执行哪个分支,因为a的值无法被确定。所以,程序的状态被分成两个(针对两个分支):
在a非零的分支中,当执行到lock()函数时,程序的状态被改为:
这个状态将会保持到第二个if语句。当执行到第二个if语句时,因为这两个状态都定义了变量‘a‘,所以状态不会再一次一分为二。a为非零的状态将被选择执行。最后,状态将变成:
因为上面的例子并没有bug出现,所以Analyzer不会报告bug。从这个例子可以总结,当PreCall与EndFunction事件被触发,Checker中的回调函数将被调用,checker的回调函数要么修改程序状态,要么报告bug。
知道什么样的信息应该存在程序状态中,那如何定义适合本例的程序状态?类ProgramState中定义了程序状态。程序状态主要包含以下三类信息:
只有最后一类状态信息是需要所关心的,用来存储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);
根据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) ... } }
报告bug设计两个类:BugType和BugReport。BugType代表bug的类型,因为有三个bug(double lock, double unlock, unreleased lock),所以在checker中需要定义三个BugType。将这三个BugType定义在LockUnlockChecker类中,并在构造函数中初始化。BugReport类代表一个特定发生的bug。BugReport的三个参数为:
当报告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
有两种方法注册自定义checker,一种是将自定义checker作为experimental checker编译到clang可执行程序中,另一种是将自定义checker单独编译成.so共享库文件,在使用时动态加载。第一种方式适合已经测试好的checker,而第二种方式更适合调试阶段的checker。
对于从官网下载源码编译的情况,所有的checker文件都放在clang/lib/StaticAnalyzer/Checkers目录下(apt-get的安装方法不能将自定义checker编译到clang可执行程序中)。以上面的例子为例,以下说明如何注册一个用户自己写的checker:LockUnlockChecker?。
void ento::registerLockUnlockChecker(CheckerManager &mgr) { mgr.registerChecker<LockUnlockChecker>(); }
let ParentPackage = UnixAlpha in { ... def LockUnlockChecker : Checker<"LockUnlock">, HelpText<"Checker for use of lock()/unlock()">, DescFile<"LockUnlockChecker.cpp">; ... } // end "alpha.unix"
注意:
$ clang -cc1 -analyzer-checker-help
$ scan-build -enable-checker alpha.unix.LockUnlock gcc -c lock_test.c
用apt-get安装和用源码编译安装的方法都支持链接共享库的方式加载自定义checker。
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会被调用,它包含有三个参数:
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
scan-build -load-plugin ./LockUnlockChecker.so -enable-checker example.LockUnlockChecker gcc -c lock_test.c
以上的命令中涉及到很多选项,但需要区别clang与clang -cc1。clang -cc1是专门给开发者使用的,包含clang项目的所有功能。而clang相当于一个driver(驱动),是专门给用户使用,他兼容GCC的使用方法,以方便熟悉GCC的用户使用。例如,与gcc类似,clang命令也可以驱动预处理器、汇编器、编译器、连接器甚至静态分析器。 当使用命令行命令clang时,clang会自动产生一系列适合当前系统的选项并传递给clang -cc1。所以clang与clang -cc1选项不能混用。
由于Clang项目目前仍处于开发阶段,不同版本之间有不少区别。(亲测在3.2版本的代码不能直接在3.5版本中运行)
How to use Clang Static Analyzer
原文地址:http://blog.csdn.net/sealjin/article/details/45221209