C++代码静态分析⼯具splint
1.引⾔
最近在项⽬中使⽤了静态程序分析⼯具,体会到它在项⽬实施中带给开发⼈员的⽅便。PC-Lint是⼀款针对C/C++语⾔、windows平台的静态分析⼯具,FlexeLint是针对其他平台的PC-Lint版本。由于PC-Lint/FlexeLint是商业的程序分析⼯具,不便于⼤家对其进⾏学习和使⽤,因⽽下⾯我将介绍⼀个针对C语⾔的开源程序静态分析⼯具——。
2.静态程序分析
先来说说什么是“静态程序分析(Static program analysis)”,静态程序分析是指使⽤⾃动化⼯具软件对程序源代码进⾏检查,以分析程序⾏为的技术,应⽤于程序的正确性检查、安全缺陷检测、程序优化等。它的特点就是不执⾏程序,相反,通过在真实或模拟环境中执⾏程序进⾏分析的⽅法称为“动态程序分析(Dynamic program analysis)”。
那在什么情况下需要进⾏静态程序分析呢?静态程序分析往往作为⼀个多⼈参与的项⽬中代码审查过程的⼀个阶段,因编写完⼀部分代码之后就可以进⾏静态分析,分析过程不需要执⾏整个程序,这有助于在项⽬早期发现以下问题:变量声明了但未使⽤、变量类型不匹配、变量在使⽤前未定义、不可达代码、死循环、数组越界、内存泄漏等。
静态分析⼯具在代码通过编译之后再对代码进⾏分析。我们会问:静态分析⼯具与编译器相⽐,所做的⼯作有什么不同?静态分析⼯具相⽐编译器,对代码进⾏了更加严格的检查,像数组越界访问、内存泄漏、使⽤不当的类型转换等问题,都可以通过静态分析⼯具检查出来,我们甚⾄可以在分析⼯具的分析标准⾥定义代码的编写规范,在检测到不符合编写规范的代码时抛出告警,这些功能都是编译器没有的。
既然静态分析⼯具发挥了不⼩的作⽤,何不在编译器⾥兼备静态分析的功能?对于这个问题,S. C. Johnson(他是最古⽼的静态分析⼯具Lint的作者)在其1978年发表的论⽂中给出了他的答案:“Lint与C编译器在功能上的分离既有历史原因,也有现实的意义。编译器负责把C 源程序快速、⾼效地转变为可执⾏⽂件,不对代码做类型检查(特别是对分别编译的程序),有益于做到快速与⾼效。⽽Lint没有“⾼效”的要求,可以花更多时间对代码进⾏更深⼊、仔细的检查。”
针对空指针提取、未定义变量使⽤、类型转换、内存管理、函数接⼝定义等,我们可以在静态分析⼯具⾥制定不同的检测标准,以下曲线图说明了在使⽤splint进⾏分析时,检测标准与splint运⾏的开销所对应的关系,从另⼀个⾓度看,也说明了静态分析⼯具与编译器的关系:
splint
掌握了“静态分析”等概念之后,我们再来看splint。
在Linux命令⾏下,splint的使⽤很简单,检测⽂件*.c,只要这样使⽤就可以了:
splint *.c
1.splint消息
我们通过以下例⼦来认识典型的splint告警信息:
1 //splint_msg.c
2 int func_splint_msg1(void)
3 {
4 int a;
5 return 0;
7 int func_splint_msg2(void)
8 {
9 int* a = (int*)malloc(sizeof(int));
10 a = NULL;
11 return 0;
12 }
运⾏splint splint_msg.c之后,我们来看输出的告警信息:
splint_msg.c: (in function func_splint_msg1)
splint_msg.c:4:6: Variable a declared but not used
A variable is declared but never used. Use /*@unused@*/ in front of
declaration to suppress message. (Use -varuse to inhibit warning)
splint_msg.c: (in function func_splint_msg2)
splint_msg.c:10:2: Fresh storage a (type int *) not released before assignment:
a = NULL
A memory leak has been detected. Storage allocated locally is not released
before the last reference to it is lost. (Use -mustfreefresh to inhibit
warning)
splint_msg.c:9:37: Fresh storage a created
Finished checking --- 2 code warnings
蓝⾊字体部分:给出告警所在函数名,在函数的第⼀个警告消息报告前打印;
红⾊字体部分:消息的正⽂,⽂件名、⾏号、列号显⽰在的警告的正⽂前;
⿊⾊字体部分:是有关该可疑错误的详细信息,包含⼀些怎样去掉这个消息的信息;
绿⾊字体部分:给出格外的位置信息,这⾥消息给出了是在哪⾥申请了这个可能泄露的内存。
2.检查控制
splint提供了三种⽅式可进⾏检查的控制,分别是.splintrc配置⽂件、flags标志和格式化注释。
flags:splint⽀持⼏百个标志⽤来控制检查和消息报告,使⽤时标志前加’+‘或’-’,'+'标志开启这个标志,'-'表⽰关闭此标志,下⾯例⼦展⽰了flags标志的⽤法:
splint -showcol a.c //在检测a.c时,告警消息中列数不被打印
splint -varuse a.c //在检测a.c时,告警消息中未使⽤变量告警不被打印
.splintrc配置⽂件:在使⽤源码安装splint之后,.splintrc⽂件将被安装在主⽬录下,.splintrc⽂件中对⼀些标志作了默认的设定,命令⾏中指定的flags标志会覆盖.splintrc⽂件中的标志。
格式化注释:格式化注释提供⼀个类型、变量或函数的格外的信息,可以控制标志设置,增加检查效果,所有格式化注释都以/*@开始,@*/结束,⽐如在函数参数前加/*@null@*/,表⽰该参数可能是NULL,做检测时,splint会加强对该参数的值的检测。
3.检测分析内容
1.解引⽤空指针(Null Dereferences)
在Unix操作系统中,解引⽤空指针将导致我们在程序运⾏时产⽣段错误(Segmentation fault),⼀个简单的解引⽤空指针例⼦如下:
1 //null_dereferences.c
2 int func_null_dereferences(void)
3 {
4 int* a = NULL;
5 return *a;
6 }
strcmp was not declared in执⾏splint null_dereference.c命令,将产⽣以下告警消息:
null_dereference.c: (in function func_null_dereferences)
null_dereference.c:5:10: Dereference of null pointer a: *a
A possibly null pointer is dereferenced. Value is either the result of a
function which may return null (in which case, code should check it is not
null), or a global, parameter or structure field declared with the null
qualifier. (Use -nullderef to inhibit warning)
null_dereference.c:4:11: Storage a becomes null
Finished checking --- 1 code warnin
2.类型(Types)
我们在编程中经常⽤到强制类型转换,将有符号值转换为⽆符号值、⼤范围类型值赋值给⼩范围类型,程序运⾏的结果会出⽆我们的预料。
1 //types.c
2 void splint_types(void)
4 short a = 0;
5 long b = 32768;
6 a = b;
7 return;
8 }
执⾏splint types.c命令,将产⽣以下告警消息:
types.c: (in function splint_types)
types.c:6:2: Assignment of long int to short int: a = b
To ignore type qualifiers in type comparisons use +ignorequals.
Finished checking --- 1 code warning
3.内存管理(Memory Management)
C语⾔程序中,将近半数的bug归功于内存管理问题,关乎内存的bug难以发现并且会给程序带来致命的破坏。由内存释放所产⽣的问题,我们可以将其分为两种:
当尚有其他指针引⽤的时候,释放⼀块空间
1 //memory_management1.c
2 void memory_management1(void)
3 {
4 int* a = (int*)malloc(sizeof(int));
5 int* b = a;
6 free(a);
7 *b = 0;
8 return;
9 }
在上⾯这个例⼦中,指针a与b指向同⼀块内存,但在内存释放之后仍对b指向的内容进⾏赋值操作,我们来看splint
memory_management1.c的结果:
memory_management1.c: (in function memory_management1)
memory_management1.c:7:3: Variable b used after being released
Memory is used after it has been released (either by passing as an only param
or assigning to an only global). (Use -usereleased to inhibit warning)
memory_management1.c:6:7: Storage b released
memory_management1.c:7:3: Dereference of possibly null pointer b: *b
A possibly null pointer is dereferenced. Value is either the result of a
function which may return null (in which case, code should check it is not
null), or a global, parameter or structure field declared with the null
qualifier. (Use -nullderef to inhibit warning)
memory_management1.c:5:11: Storage b may become null
Finished checking --- 2 code warnings
检查结果中包含了两个告警,第⼀个指出我们使⽤了b指针,⽽它所指向的内存已被释放;第⼆个是对解引⽤空指针的告警。
当最后⼀个指针引⽤丢失的时候,其指向的空间尚未释放
1 //memory_management2.c
2 void memory_management2(void)
3 {
4 int* a = (int*)malloc(sizeof(int));
5 a = NULL;
6 return;
7 }
这个例⼦中内存尚未释放,就将指向它的唯⼀指针赋值为NULL,我们来看splint memory_management2.c的检测结果:
memory_management2.c: (in function memory_management2)
memory_management2.c:5:2: Fresh storage a (type int *) not released before assignment:
a = NULL
A memory leak has been detected. Storage allocated locally is not released
before the last reference to it is lost. (Use -mustfreefresh to inhibit
warning)
memory_management2.c:4:37: Fresh storage a created
Finished checking --- 1 code warning
splint抛出⼀个告警:类型为int*的a在进⾏a = NULL赋值前没有释放新分配的空间。
4.缓存边界(Buffer Sizes)
splint会对数组边界、字符串边界作检测,使⽤时需要加上+bounds的标志,我们来看下⾯的例⼦:
1 //bounds1.c
2 void bounds1(void)
3 {
4 int a[10];
5 a[10] = 0;
6 return;
7 }
使⽤splint +bounds bounds1.c命令对其进⾏检测,结果如下:
bounds1.c: (in function bounds1)
bounds1.c:5:2: Likely out-of-bounds store: a[10]
Unable to resolve constraint:
requires 9 >= 10
needed to satisfy precondition:
requires maxSet(a @ bounds1.c:5:2) >= 10
A memory write may write to an address beyond the allocated buffer. (Use
-likelyboundswrite to inhibit warning)
Finished checking --- 1 code warning
告警消息提⽰数组越界,访问超出我们申请的buffer⼤⼩范围。再看⼀个例⼦:
1 //bounds2.c
2 void bounds2(char* str)
3 {
4 char* tmp = getenv("HOME");
5 if(tmp != NULL)
6 {
7 strcpy(str, tmp);
8 }
9 return;
10 }
不对这个例⼦进⾏详细检查,可能我们不能发现其中隐含的问题,执⾏splint +bounds bounds2.c之后,会抛出如下告警:
bounds2.c: (in function bounds2)
bounds2.c:7:3: Possible out-of-bounds store: strcpy(str, tmp)
Unable to resolve constraint:
requires maxSet(str @ bounds2.c:7:10) >= maxRead(getenv("HOME") @
bounds2.c:4:14)
needed to satisfy precondition:
requires maxSet(str @ bounds2.c:7:10) >= maxRead(tmp @ bounds2.c:7:15)
derived from strcpy precondition: requires maxSet(<parameter 1>) >=
maxRead(<parameter 2>)
A memory write may write to an address beyond the allocated buffer. (Use
-boundswrite to inhibit warning)
Finished checking --- 1 code warning
告警消息提⽰我们:在使⽤strcpy(str, tmp)进⾏字符串复制时,可能出现越界错误,因为str的⼤⼩可能不⾜以容纳环境变量“HOME”对应的字符串。绿⾊字体的内容指⽰了如何消除告警消息。
3.⼩结
这⾥仅给出了splint检查的4种检测:解引⽤空指针、类型、内存管理、缓存边界,除此之外,splint还对宏(Macros)、函数接⼝ (Function Interfaces)、控制流(Control Flow)等内容作检测,很多检测标志和格式化注释都未在本⽂中提到,更详细的内容请查看。
不管pc-lint、splint等静态程序分析⼯具的功能多么强⼤,它们对程序的检查也有疏漏的地⽅,⼯具的使⽤并不能提⾼我们的编程能⼒,我们更应该通过它们学习各种编码错误和代码隐患,凭积累的编码知识把程序隐患扼杀在摇篮⾥。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论