软件测试实验学习笔记系列2 -- lint,splint的使用

lint简史

1979年,贝尔实验室SteveJohnson1979PCC(PortableCCompiler)基础上开发的出了代码分析工具Lint,可以检查出很多的不符合规范的的错误(如将“==”写成了“=”)以及函数接口参数不一致性的问题等,完成代码健壮性的检查。Lint后来形成了一系列的工具,包括PC-Lint/FlexeLint(Gimpel),LintPlus(Cleanscape)以及Splint.

功能

通常C/C++编译器假设程序是正确的,Lint恰好相反,因此它有优于编译器执行的一般性的代码检查。Lint还可以在多个文件之间执行错误检查和代码分析。下面是一些Lint可以检查出来的部分的错误列表:

  • 可能的空指针

  • 在释放内存后使用了指向该内存的指针

  • 赋值次序问题

  • 拼写错误

  • 0

  • 失败的case语句(遗漏了break语句)

  • 不可移植的代码(依赖了特定的机器实现)

  • 宏参数没有使用圆括号

  • 符号的丢失

  • 异常的表达式

  • 变量没有初始化

  • 可疑的判断语句(例如,if(x=0))

  • printf/scanf的格式检查

现有的Lint程序主要有两个版本:①PC-Lint,GimpelSoftware提供的支持C/C++的商用程序。官方地址:http://www.gimpel.com/html/pcl.htm②Splint(原来的LCLint)是一个有GNU免费授权的Lint程序,只支持c,而不支持c++.官方地址:http://www.splint.org/

由于PC-lint是商业软件,虽说功能强大,但是容易获取。这里主要介绍GNUsplint工具。

开源的代码静态分析工具Splint

Splint是一个动态检查C语言程序安全弱点和编写错误的一个程序,会进行多种错误检查:未使用变量,类型不一致,使用未定义的变量,无法执行的代码,忽略返回值,执行路径没有返回,无限循环等错误。

官方网站:http://www.splint.org/

Splint的安装

1.Linux下的安装

1.1rpm安装:

rpm -ivh splint.xxx.rpm

rpm安装包是著名的Linux发行商Redhat推出的基于源代码的软件包方式。这种安装方式的缺点是如软件依赖项有很多并且你没有安装那些依赖项时,哈哈,恭喜你,你有事忙了,需要满互联网的找到那些依赖项并安装好;如果依赖项还有依赖项并且你又没有安装,我只能说,哥们你中彩了。

1.2Ubuntu或者Debian下安装

sudo apt-get install splint

这种安装最省事,唯一的缺点是,安装的软件的版本可能不是最新的,以及总是按照默认的配置来安装的软件的---不够灵活

1.3源代码安装(通用)

tar -zxvf splint-3.1.2.src.gz

cd splint-3.1.2

./configure

make

make install

2.window下安装

可以使用源代码安装的方式.最新的官网提供了window下的软件安装包(msi格式),地址是:https://github.com/maoserr/splint_win32/downloads

由于本人使用的是Ubuntu12.04LTS,splint安装使用的是apt-get的安装方式,splint的版本是3.1.2,以下的介绍都是以次为基础的。

splint的命令

 splint的命令语法和其他的GNU/Linux的语法一致,其语法形式如下: splint [-options]  *.c

 遇到一些特别的情况可以通过man splint或者浏览手册页来获取帮助。 

splint的应用

4.1 splint 消息

通过以下面的例子来认识典型的splint告警信息:

//splint_msg.c
int func_splint_msg1(void){
      int a;
      return0;
}
int func_splint_msg2(void){
    int* a = (int*)malloc(sizeof(int));
    a = NULL;
    return0;
}

运行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

蓝色字体部分:给出告警所在函数名,在函数的第一个警告消息报告前打印;

红色字体部分:消息的正文,文件名、行号、列号显示在的警告的正文前;

黑色字体部分:是有关该可疑错误的详细信息,包含一些怎样去掉这个消息的信息;

绿色字体部分:给出格外的位置信息,这里消息给出了是在哪里申请了这个可能泄露的内存。

4.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会加强对该参数的值的检测。

4.3 检测内容分析

空引用错误:/*@null@*/--splint支持的注释类型,表明其后跟随的值可能为null

实例程序,null.c:

/*Program:null.c -- a example code for null reference error*/
//'/*@null@*/' is a special annotation the splint tool supported
char firstChar1(/*@null@*/char *s){
	return *s;
}
char firstChar2(/*@null@*/char *s){
	if(s ==NULL) return '\0';
	return *s;
}

splint命令: splint null.c

结果输出:

Splint 3.1.2--- 03 May 2009

Spec filenot found: null.lcl


null.c:2:65:Comment starts inside comment

A commentopen sequence (/*) appears within a comment. This usually means an

earliercomment was not closed. (Use -nestcomment to inhibit warning)

null.c: (infunction firstChar1)

null.c:4:10:Dereference of possibly null pointer s: *s

A possiblynull pointer is dereferenced. Value is either the result of a

functionwhich may return null (in which case, code should check it is not

null), ora global, parameter or structure field declared with the null

qualifier.(Use -nullderef to inhibit warning)

null.c:3:33:Storage s may become null

Finishedchecking --- 2 code warnings

函数firstChar1和firstChar2都使用了null的说明,表示指针s可能是一个空指针,所以splint会对s的值使用情况进行检查,由于firstChar2中对s的值进行了判断,所以没有对firstChar2函数中s输出警告信息。

未定义变量错误:C中使用没有定义变量会出错,/*@in@*/说明的变量表示必须先进行定义./*@out*@/表明在执行过函数后,这个变量就进行了定义。--个人感觉/*@out*@/类似与C#中的out关键字。

实例1:usedef.c

/*Program:usedef.c -- use splint check the varible undefined error or warnings */
//out represent varible *x will be defined after execution of function
extern void setVal(/*@out@*/int *x);
//in represent varible *x has been defined before the execution.
extern int getVal(/*@in@*/ int *x);
extern int mysteryVal(int *x);
int dumbfunc(/*@out@*/int *x,int i){
	if(i>3) return *x;
	else if(i>1)
		return getVal(x);
	else if(i==0)
		return mysteryVal(x);
	else{
		setVal(x);
		return *x;
	}
}

splint命令:splint usedef.c

splint执行的结果:

Splint 3.1.2--- 03 May 2009

usedef.c:(in function dumbfunc)

usedef.c:8:17:Value *x used before definition

#在一些执行路径中一个右值的被使用的时候可能没被初始化,

An rvalueis used that may not be initialized to a value on some execution

path. (Use-usedef to inhibit warning)

usedef.c:10:17:Passed storage x not completely defined (*x is undefined):

getVal(x)

Storagederivable from a parameter, return value or global is not defined.

Use/*@out@*/ to denote passed or returned storage which need not bedefined.

(Use-compdef to inhibit warning)

usedef.c:12:21:Passed storage x not completely defined (*x is undefined):

mysteryVal(x)

Finishedchecking --- 3 code warnings

错误原因:由于程序中没有对X定义,所以报出未定义的错误.但是由于setVal()使用了/*@out*@/说明,所以在语句“setVal(x)”和“returnx”,没有报未定义错误。

类型错误:C语言中的变量类型比较多,彼此之间有些细微的差别,splint可以对变量的类型进行检查:

实例1.typeerr.c

/*Program: typeerr.c -- use splint to check type varible error */
int foo(int i,char *s,bool b1,bool b2){
	if(i=3) return b1;
	if(!i || s) return i;
	if(s) return 7;
	if(b1 == b2)
		return 3;
	return 2;
}

splint命令:splint typeerr.c

splint执行的结果:

Splint 3.1.2--- 03 May 2009

typeerr.c:(in function foo)

typeerr.c:3:5:Test expression for if is assignment expression: i = 3

Thecondition test is an assignment expression. Probably, you mean to use==

instead of=. If an assignment is intended, add an extra parentheses nesting

(e.g., if((a = b)) ...) to suppress this message. (Use -predassign to

inhibitwarning)

#错误类型:if语句中的条件表达式是一个赋值语句。

typeerr.c:3:5:Test expression for if not boolean, type int: i = 3

Testexpression type is not boolean or int. (Use -predboolint to inhibit

warning)

#错误类型:if语句中的条件表达式返回值不是bool类型而是int类型

typeerr.c:3:17:Return value type bool does not match declared type int: b1

Types areincompatible. (Use -type to inhibit warning)

#错误类型:!的操作数不是bool类型而是int类型的i

typeerr.c:4:6:Operand of ! is non-boolean (int): !i

Theoperand of a boolean operator is not a boolean. Use +ptrnegate toallow !

to be usedon pointers. (Use -boolops to inhibit warning)

#错误类型:||操作符的右操作数不是bool类型而是整型

typeerr.c:4:11:Right operand of || is non-boolean (char *): !i || s

##错误类型:不应该使用==对两个bool类型进行比较,而应该使用&&

typeerr.c:6:5:Use of == with boolean variables (risks inconsistency because of

multipletrue values): b1 == b2

Two boolvalues are compared directly using a C primitive. This may produce

unexpectedresults since all non-zero values are considered true, so

differenttrue values may not be equal. The file bool.h (included in

splint/lib)provides bool_equal for safe bool comparisons. (Use -boolcompare

to inhibitwarning)

Finishedchecking --- 6 code warnings

实例2.malloc1.c

/*Program: malloc1.c -- check varible type */
#include<stdlib.h>
#include<stdio.h>
int main(){
	char * some_mem;
	int size1 = 1048567;
	//size_t size1 = 1048567;
	some_mem = (char*) malloc(size1);
	printf("Malloced 1M Memory!\n");
	free(some_mem);
	exit(EXIT_SUCCESS);
}

splint命令:splint malloc1.c

使用是splint检查malloc1.c

Splint 3.1.2--- 03 May 2009


malloc1.c:(in function main)

malloc1.c:8:28:Function malloc expects arg 1 to be size_t gets int: size1

To allowarbitrary integral types to match any integral type, use

+matchanyintegral.

size1的定义修改为:

size_t size1= 1048567;

再次使用splint将行检查:splintmalloc1.c

Splint 3.1.2--- 03 May 2009

Finishedchecking --- no warnings 

内存检查:缓冲区溢出是一种非常危险的c语言错误,大部分安全漏洞都与它有关,splint可以对缓冲区的使用进行检查,报告溢出或越界错误。

实例:overflow.c

/*Program: overflow -- splint check overflow error */
int main(){
	int buf[10];
	buf[10] = 3;
	return 0;
}

splint命令:splint overflow.c +bounds +showconstraintlocation

splint执行的结果:-

Splint 3.1.2--- 03 May 2009

CommandLine: Setting +showconstraintlocation redundant with current value

overflow.c:(in function main)

overflow.c:4:2:Likely out-of-bounds store: buf[10]

Unableto resolve constraint:

requires9 >= 10

neededto satisfy precondition:

requiresmaxSet(buf @ overflow.c:4:2) >= 10

A memorywrite may write to an address beyond the allocated buffer. (Use

-likelyboundswriteto inhibit warning)

Finishedchecking --- 1 code warning

错误类型:数组buf的大小是10字节,最大也可使用的buf[9],但是程序中使用了buf[10],数组越界了,所以报错了。

实例程序2.bound.c

/*Program: bound.c -- use splint checking bound overflow error */
void updateEnv(char * str){
	char *tmp;
	tmp = getenv("MYENV");
	if(tmp != NULL) strcpy(str,tmp);
}

void updateEnvSafe(char * str, size_t strSize){
	char *tmp;
	tmp = getenv("MYENV");
	if(tmp != NULL){
		strncpy(str,tmp,strSize -1);
		str[strSize-1]='\0';
	}
}

splint命令:splint bound.c +bounds +showconstraintlocation

splint执行的结果:

Splint3.1.2 --- 03 May 2009

CommandLine: Unrecognized option: +

A flag isnot recognized or used in an incorrect way (Use -badflag to inhibit

warning)

Spec filenot found: showconstraintlocation.lcl

Cannot openfile: showconstraintlocation.c

bound.c: (infunction updateEnv)

bound.c:5:18:Possible out-of-bounds store: strcpy(str, tmp)

Unableto resolve constraint:

requiresmaxSet(str @ bound.c:5:25) >= maxRead(getenv("MYENV") @

bound.c:4:8)

neededto satisfy precondition:

requiresmaxSet(str @ bound.c:5:25) >= maxRead(tmp @ bound.c:5:29)

derivedfrom strcpy precondition: requires maxSet(<parameter 1>) >=

maxRead(<parameter2>)

A memorywrite may write to an address beyond the allocated buffer. (Use

-boundswriteto inhibit warning)

bound.c: (infunction updateEnvSafe)

bound.c:13:3:Possible out-of-bounds store: str[strSize - 1]

Unableto resolve constraint:

requiresmaxSet(str @ bound.c:13:3) >= strSize @ bound.c:13:7 + -1

neededto satisfy precondition:

requiresmaxSet(str @ bound.c:13:3) >= strSize @ bound.c:13:7 - 1


Finishedchecking --- 2 code warnings

错误类型:由于使用strcpy函数,没有指定复制字符串的长度,所以,可能导致缓冲区溢出UpdateEnvSafe中使用strncpy进行字符串复制,从而避免了缓冲区溢出的错误。

小结

         在命令行下使用的splint非常的强大,splint同样可以可以集成到IDE 中.具体的要IDE的其他工具的设置。splint同样也可以写到在makefile文件中,然后使用make命令来预先检查代码中常见的静态错误。

         有了上面的这些简单的实例的演示,我们可以感受到splint的强大之处,当然,这里的介绍仅仅是一个简单抛砖引玉。更多的有关splint的内容可以参考参考文献[4],更多关于splint的使用可以参考splint 的官方手册[4].

         尽管pc-lint、splint等静态程序分析工具的功能强大,它们对程序的检查也有疏漏的地方,工具的使用并不能提高个人的编程能力,我们更应该通过它们学习各种编码错误代码隐患,凭积累的编码知识把程序隐患扼杀在摇篮里。

         除了C有静态的代码工具以外,java中也有一款开源的功能强大的静态代码检查工具FindBugs。

参考文献

[1]软件测试实验指导教程/蔡建平, 清华大学出版社, 2009.11

[2] splint 官网 http://www.splint.org/

[3]软件测试方法和技术(第二版)/朱少民,清华大学出版社, 2010.7

[4] splint 的官方手册 http://www.splint.org/manual/manual.html

[5] splint 的windows安装包下载地址:https://github.com/maoserr/splint_win32/downloads

[6] Findbugs官方地址:http://sourceforge.net/projects/findbugs/

[7] 代码静态分析工具——splint的学习与使用:http://www.cnblogs.com/bangerlee/archive/2011/09/07/2166593.html

下一篇是关于单元测试和xUnit

你可能感兴趣的:(软件测试,splint,静态代码分析工具)