Clang Static Analyzer 阅读实验
1. 编译LLVM和clang
首先下载 LLVM 和 clang 的源码。把它们放在某个文件夹下,并在此处打开终端,执行下面的命令(保证你安装了xz、cmake):
tar xvf llvm-3.9.0.src.tar.xz
mv llvm-3.9.0.src llvm
tar xvf cfe-3.9.0.src.tar.xz
mv cfe-3.9.0.src llvm/tools/clang
# we compile everything under build/
mkdir build
cd build
cmake ../llvm # you can add `-G ninja` option
make -j4 # use -jN to specify number of threads
这可能要花很长时间(使用默认配置(开启了DEBUG),在 Core i7、开8线程下,大约需要1小时。内存使用峰值会超过12G,如果你遇到内存不够的问题,参考文档和这个答案。可以考虑使用RELEASE配置,只需为cmake
添加-DCMAKE_BUILD_TYPE=Release
选项,时间可缩短到20分钟)。
你需要记录一下你的编译环境、所用时间、内存等信息,放在sa/compile.txt中,格式如下:
CPU:
内存大小:
操作系统:ubuntu/windows/mac/...
cmake目标:make/ninja/vs/...
cmake build type:Debug/Release
编译线程数:
编译耗时:
内存占用峰值:
遇到的问题及如何解决:
你可以先去喝几杯茶。
编译结束后,你会在bin
目录下看到所有的可执行文件。
假设之后你需要执行这里的可执行文件,你可以在调用时指定路径,或者将这个路径加入到PATH环境变量中。
2. 使用Clang Static Analyzer
在深入了解 clang 静态分析的实现机制之前,可以先使用它检测包含有 bug 的 C 程序,初步了解静态分析对程序员到底有何用处。
Clang静态分析可以通过多种渠道来被使用,这里可以使用命令scan-build
来调用 Clang 静态分析。
例如,如下是一个含有潜在导致悬空引用(全局变量p
引用了局部变量str的地址)的C程序,假设保存在test.c
中:
char *p;
void test()
{
char str[] = "hello";
p = str;
}
如果用一般的编译器去编译这个程序,可能都不会报任何warning。
接下来可以执行命令:
$ scan-build clang -cc1 test.c
其中clang -cc1
指令表示只调用clang的前端进行编译。
你可以看到它会检测出bug,并报了warning。如果为scan-build
添加-V
选项,你还可以看到经过良好排版的网页端结果。
你也可以在直接调用clang命令时指定要使用的某个checker,从而在clang执行期间会调用这个checker对代码进行检查。考虑下面这个程序testfile.c
。
#include <stdio.h>
FILE *open(char *file)
{
return fopen(file, "r");
}
void f1(FILE *f)
{
// do something...
fclose(f);
}
void f2(FILE *f)
{
// do something...
fclose(f);
}
int main()
{
FILE *f = open("foo");
f1(f);
f2(f);
return 0;
}
将这个文件保存于dblclose.c
,之后执行:
$ clang --analyze -Xanalyzer -analyzer-checker=alpha.unix.SimpleStream dblclose.c
即可对这个文件执行SimpleStreamChecker。这个checker的含义如它名字所言。
3. 学习现有的checker
Clang 中实现了很多独立的 checker 用来做静态检查。先前看到的test.c
中的bug,就是被其中的一个名为(core.StackAddressEscape)的检查器检测出来的。
你可以执行
clang -cc1 -analyze -analyzer-checker-help
来查看有哪些checker可用。
所有checker的代码都在llvm/tools/clang/lib/StaticAnalyzer/Checkers/
下。
你可以按照自己的方式阅读。
下面是一些指导和需要回答的问题:
3.1 对程序绘制AST、CFG和exploded graph
阅读这一小节,完成下面几项:
- 安装 graphviz
- 写一个含有循环、跳转逻辑的简单程序,保存为
sa/test.c
- 使用
clang -cc1 -ast-view test.c
绘制程序的AST,输出保存为sa/AST.svg
- 根据文档说明,绘制
sa/CFG.svg
,sa/ExplodedGraph.svg
- 简要说明
test.c
、AST.svg
、CFG.svg
和ExplodedGraph.svg
之间的联系与区别 - 特别说明:如果你采用了release配置,或者你无法正常产生svg,你可以选择使用dump选项,并将文字输出放在对应名字的txt中。其他格式的图片也可以接受,不需要为格式问题耗费时间。
3.2 阅读Checker Developer Manual的Static Analyzer Overview一节
回答下面的问题:
- Checker 对于程序的分析主要在 AST 上还是在 CFG 上进行?
- Checker 在分析程序时需要记录程序状态,这些状态一般保存在哪里?
- 简要解释分析器在分析下面程序片段时的过程,在过程中产生了哪些symbolic values? 它们的关系是什么?
一段程序:
int x = 3, y = 4;
int *p = &x;
int z = *(p + 1);
3.3 简要阅读LLVM Programmer's Manual和LLVM Coding Standards
这两个manual比较长,你不需要全部阅读,你只需要给出下面几个问题的答案:
- LLVM 大量使用了 C++11/14的智能指针,请简要描述几种智能指针的特点、使用场合,如有疑问也可以记录在报告中.
- LLVM 不使用 C++ 的运行时类型推断(RTTI),理由是什么?LLVM 提供了怎样的机制来代替它?
- 如果你想写一个函数,它的参数既可以是数组,也可以是std::vector,那么你可以声明该参数为什么类型?如果你希望同时接受 C 风格字符串和 std::string 呢?
- 你有时会在cpp文件中看到匿名命名空间的使用,这是出于什么考虑?
3.4 阅读clang/lib/StaticAnalyzer/Checkers/SimpleStreamChecker.cpp
回答下面问题:
- 这个 checker 对于什么对象保存了哪些状态?保存在哪里?
- 状态在哪些时候会发生变化?
- 在哪些地方有对状态的检查?
- 函数
SimpleStreamChecker::checkPointerEscape
的逻辑是怎样的?实现了什么功能?用在什么地方? - 根据以上认识,你认为这个简单的checker能够识别出怎样的bug?又有哪些局限性?请给出测试程序及相关的说明。
3.5 Checker的编译
阅读这一节,以及必要的相关源码,回答下面的问题:
- 增加一个checker需要增加哪些文件?需要对哪些文件进行修改?
- 阅读
clang/include/clang/StaticAnalyzer/Checkers/CMakeLists.txt
,解释其中的 clang_tablegen 函数的作用。 .td
文件在clang中出现多次,比如这里的clang/include/clang/StaticAnalyzer/Checkers/Checkers.td
。这类文件的作用是什么?它是怎样生成C++头文件或源文件的?这个机制有什么好处?
4 扩展要求
完成上述基础要求后,你可以考虑完成这部分扩展要求。由于这个扩展要求难度较高,各位请量力而行。
扩展部分提供两种选择,分析现有 checker 的不足或者编写自己的 checker. 如果觉得很难想到一个有价值的 checker, 建议选择第一个.
4.1 分析现有 checker 的缺陷
clang 静态分析提供了对程序中一些常见问题的检查. 比如 cplusplus.NewDelete、unix.Malloc 能对内存泄漏等 bug 进行一定的检查. 但它们并不能解决程序中出现的所有这类问题.
在官网 Open Project 里提到了一些 clang 静态分析不能解决的问题. 比如:
- 浮点值的处理问题:当前clang 静态分析器将所有的浮点值处理为
unknown
,故不能静态检测含浮点值的条件,从而审查代码。
void foo(double f) {
int *pi = 0;
if (f > 1.)
pi = new int; // bug report: potential leakage
if (f > 0. && pi) // must taken, no leakage
delete pi;
}
- 不能处理多次循环:当前分析器简单地将每个循环展开
N
(比较小的整数) 次void foo() { int *pi = new int; for (int i = 0; i < 3; i++) // if replace 3 with 100, no bug report if (i == 1000) delete pi; // bug report: potential leakage }
不能处理按位运算
int main (int argc, char **argv) { const char *space; int flags = argc; if (flags & (0x01 | 0x02)) space = "qwe"; if (flags & 0x01) return *space; // bug report: Dereference of undefined pointer value return 0; }
除了这些缺陷以外, clang静态分析器还有哪些缺陷?
- 以动态内存、或文件等资源有关的缺陷检查为例,对clang 静态分析器进行如下使用和分析工作:
- 是否能检查该类缺陷?
- 检查能力到什么程度(程序存在哪些特征时检查不出来)?
- 检查的实现机制是什么?列出相关的源码位置和主要处理流程
- (可选)从实现机制上分析,为什么检查不出来上述问题2的解答中所列的特征?
- (可选)如果想增强检查能力,可以怎么做?
- 可选的动态内存、或文件等资源有关的缺陷检查
- cplusplus.NewDelete
- unix.Malloc
- unix.API
- 悬空引用
- 文件未关闭
- ......
4.2 编写自己的 checker
在阅读clang代码大致了解 checker 的编写方法之后,你可以仔细阅读Checker Developer Manual,另外这份slides 也是非常好的材料。
作为快速指导,下面将整理一下你需要做的主要工作,和每个阶段工作可能需要参考的材料:
- 确定你想要检测的bug,或者完成的功能。你可以参考这个链接来寻找一些idea。如果你想要做一种bug checker,你应当先考虑清楚你需要记录哪些状态,状态在哪些时机改变,哪些时机需要对状态进行检查从而确定是否有问题。
- 开始编码。这里你需要实现你刚刚的想法。你应当已经知道checker应该如何保存状态、如何设置callback、如何获取程序符号,你想知道的其他细节需要在文档中找,另外你会发现你的很多做法会有其他checker可以参考。
- 注册checker并编译:根据这一节,核实你的checker可以通过编译,并能够被clang调用。
- 编写测试样例进行测试。你的测试样例要能体现出你的checker能完成以及不能完成的事情。注意检查会不会有false positive的情况。测试样例放在
sa/test/
目录下。 - 编写说明文档。文档中说明你完成的功能,遇到的困难等。
编写好后,你需要提交你的PBXXXXXXXXChecker.cpp,以及对你的checker功能的说明(在README(.md)
中简述)、测试样例。请保证你的llvm、clang版本为3.9.0,且可以编译通过,之后在实验评测时会将各位的checker统一注册编译。
4.3 需要提交的目录格式:
mp1/
... 以前的内容
sa/
├─ compile.txt 编译记录
├─ AST.svg, CFG.svg, ExplodedGraph.svg
├─ test.c 3.1中要求的程序和图
├─ answers.txt(或.md) 回答问题
│ 以上为基础要求部分
├─ PBXXXXXXXXChecker.cpp 编写的checker源码
├─ checker.(md|doc|tex|txt) 对checker的说明
├─ analysis.(md|doc|tex|txt) 对 clang 静态分析器的分析
└─ test/
├─ ... 你的测试样例