静态代码检查工具PC-Lint
摘要:C/C++语言的语法拥有其它语言所没有的灵活性,这种灵活性带来了代码效率的提升,但相应增加了代码中存在
隐患的可能性。静态代码检查工具PC-Lint则偏重于代码的逻辑分析,它能够发现代码中潜在的错误,比如数组访问越界、内存泄漏、使用未初始化变量等。
本文将介绍如何安装和配置PC-Lint代码检查工具以及如何将PC-Lint与常见的代码编辑软件集成。
关键词:代码检查 PC-Lint 规则 选项
目 录 摘 要 1 引 言 2 PC-Lint介绍 3 PC-Lint的代码检查功能 3.1 强类型检查 3.2 变量值跟踪 3.3 赋值顺序检查 3.4 弱定义检查 3.5 格式检查 3.6 缩进检查 3.7 const变量检查 3.8 volatile变量检查 4 PC-Lint软件使用方法 4.1 安装与配置 4.2 PC-Lint与常用开发工具的集成(Visual C++,Source Insight,UEdit) 5 总结 参考文献 附录一 PC-Lint 重要文件说明 附录二 错误信息禁止选项说明 附录三 PC-Lint检测中的常见错误
一 引言
C/C++语言的语法拥有其它语言所没有的灵活性,这种灵活性带来了代码效率的提升,但相应也使得代码编写具有很大的随意性,另外C/C++编译器不进行
强制类型检查,也不做任何边界检查,这就增加了代码中存在隐患的可能性。如果能够在代码提交测试之前发现这些潜在的错误,就能够极大地减轻测试人员的压
力,减少软件项目的除错成本,可是传统的C/C++编译器对此已经无能为力,这个任务只能由专用的代码检查工具完成。目前有很多C/C++静态代码检查工
具,其中Logiscope RuleChecker和PC-Lint是目前应用比较广泛的两个工具。这两个检查工具各有特色,Logiscope
RuleChecker倾向于代码编码规范的检查,比如代码缩进格式、case语句书写规范、函数声明和布尔表达式的编写规则等,而PC-Lint则偏重
于代码的逻辑分析,它能够发现代码中潜在的错误,比如数组访问越界、内存泄漏、使用未初始化变量等。本文将介绍如何安装和配置PC-Lint代码检查工具
以及将PC-Lint与常见的代码编辑软件,如Visual C++,Source
Insight集成的方法,同时还将简要介绍一些PC-Lint常用的代码检查选项。
二 PC-Lint介绍
PC-Lint是GIMPEL
SOFTWARE公司开发的C/C++软件代码静态分析工具,它的全称是PC-Lint/FlexeLint for
C/C++,PC-Lint能够在Windows、MS-DOS和OS/2平台上使用,以二进制可执行文件的形式发布,而FlexeLint
运行于其它平台,以源代码的形式发布。PC-lint在全球拥有广泛的客户群,许多大型的软件开发组织都把PC-Lint检查作为代码走查的第一道工序。
PC-Lint不仅能够对程序进行全局分析,识别没有被适当检验的数组下标,报告未被初始化的变量,警告使用空指针以及冗余的代码,还能够有效地帮你提出
许多程序在空间利用、运行效率上的改进点。 通过下面的例子就可以看出PC-Lint工具的强大功能: 1: 2:char *report( int m, int n, char *p ) 3:{ 4: int result; 5: char *temp; 6: long nm; 7: int i, k, kk; 8: char name[11] = "Joe Jakeson"; 9: 10: nm = n * m; 11: temp = p == "" ? "null" : p; 12: for( i = 0; i14: k++; 15: kk = i; 16: } 17: 18: if( k== 1 ) result = nm; 19: else if( kk > 0 ) result = 1; 20: else if( kk < 0 ) result = -1; 21: 22: if( m == result ) return( temp ); 23: else return( name ); 24:}
这是一段C代码,可以通过大多数常见的C语言编译器的检查,但是PC-Lint能够发现其中的错误和潜在的问题:第
8行向name数组赋值时丢掉了结尾的nul字符,第10行的乘法精度会失准,即使考虑到long比int的字长更长,由于符号位的原因仍然会造成精度失
准,第11行的比较有问题,第14行的变量k没有初始化,第15行的kk可能没有被初始化,第22行的result也有可能没有被初始化,第23行返回的
是一个局部对象的地址。
随着C++语言的出现,C/C++编译器有了更严格的语法检查,但是仍然不能避免出现有BUG的程序。C++的类型检查依然不如Pascal那么严格。对
于一个小程序,多数程序员都能够及时发现上面出现的错误,但是从一个拥有成千上万行代码的大型软件中找出这些瑕疵将是一项烦琐的工作,而且没有人可以保证
能找出所有的这类问题。如果使用PC-Lint,只需通过一次简单的编译就可以检查出这些错误,这将节省了大量的开发时间。从某种意义上说。PC-
Lint是一种更加严格的编译器,它除了可以检查出一般的语法错误外,还可以检查出那些虽然符合语法要求,但很可能是潜在的、不易发现的错误。
三 PC-Lint的代码检查功能
PC-Lint能够检查出很多语法错误和语法上正确的逻辑错误,PC-Lint为大部分错误消息都分配了一个错误号,编号小于1000的错误号是分配给C
语言的,编号大于1000的错误号则用来说明C++的错误消息。表 1 列出了PC-Lint告警消息的详细分类:
表 1 列出了PC-Lint告警消息分类
错误说明 |
C |
C++ |
告警级别 |
语法错误 |
1-199 |
1001-1199 |
1 |
内部错误 |
200-299 |
|
0 |
致命错误 |
300-399 |
|
0 |
告警 |
400-699 |
1400-1699 |
2 |
消息 |
700-800 |
1700-1899 |
3 |
可选信息 |
900-999 |
1900-1999 |
4 |
以C语言为例,其中的编号1-199指的是一般编译器也会产生的语法错误;编号200-299是PC-Lint程序
内部的错误,这类错误不会出现在代码中的;编号300-399指的是由于内存限制等导致的系统致命错误。编号400-999中出现的提示信息,是根据隐藏
代码问题的可能性进行分类的:其中编号400-699指的是被检查代码中很可能存在问题而产生的告警信息;编号700-899中出现的信息,产生错误的可
能性相比告警信息来说级别要低,但仍然可能是因为代码问题导致的问题。编号900-999是可选信息,他们不会被默认检查,除非你在选项中指定检查他们。 PC-Lint/FelexLint提供了和许多编译器类似的告警级别设置选项-wLevel,它的告警级别分为以下几个级别,缺省告警级别为3级: -w0 不产生信息(除了遇到致命的错误) -w1 只生成错误信息 -- 没有告警信息和其它提示信息 -w2 只有错误和告警信息 -w3 生成错误、告警和其它提示信息(这是默认设置) -w4 生成所有信息 PC-Lint/FelexLint还提供了用于处理函数库的头文件的告警级别设置选项-wlib(Level),这个选项不会影响处理C/C++源代码模块的告警级别。它有和-wLevel相同的告警级别,缺省告警级别为3级: -wlib(0) 不生成任何库信息 -wlib(1) 只生成错误信息(当处理库的源代码时) -wlib(2) 生成错误和告警信息 -wlib(3) 生成错误、告警和其它信息(这是默认设置) -wlib(4) 产生所有信息
PC-Lint的检查分很多种类,有强类型检查、变量值跟踪、语义信息、赋值顺序检查、弱定义检查、格式检查、缩进检查、const变量检查和
volatile变量检查等等。对每一种检查类型,PC-Lint都有很多详细的选项,用以控制PC-Lint的检查效果。PC-Lint的选项有300
多种,这些选项可以放在注释中(以注释的形式插入代码中),例如: /*lint option1 option2 ... optional commentary */ 选项可以有多行 //lint option1 option2 ... optional commentary 选项仅为一行(适用于C++) 选
项间要以空格分开,lint命令一定要小写,并且紧跟在/*或//后面,不能有空格。如果选项由类似于操作符和操作数的部分组成,例如
-esym(534, printf, scanf, operator new),其中最后一个选项是operator
new,那么在operator和new中间只能有一个空格。PC-Lint的选项还可以放在宏定义中,当宏被展开时选项才生效。例如: #define DIVZERO(x) /*lint -save -e54 */ ((x) /0) /*lint -restore */ 允许除数为0而不告警 下面将分别介绍PC-Lint常用的,也是比较重要的代码检查类型,并举例介绍了各个检查类型下可能出现的告警信息以及常用选项的用法:
3.1 强类型检查
强类型检查选项“-strong”和它的辅助(补充)选项“-index”可以对typedef定义的数据类型进行强类型检查,以保证只有相同类型之间的变量才能互相赋值,强类型检查选项strong的用法是: -strong( flags[, name] ... ) strong选项必须在typedef定义类型之前打开,否则PC-Lint就不能识别typedef定义的数据类型,类型检查就会失效。flags参数可以是A、J、X、B、b、l和f,相应的解释和弱化字符在表 2 中列出:
表 2 强类型检查strong选项和参数表
A |
对强类型变量赋值时进行类型检查,这些赋值语句包括:直接赋值、返回值、参数传递、初始化 。 A参数后面可以跟以下字符,用来弱化A的检查强度: i 忽略初始化 r 忽略Return语句 p 忽略参数传递 a 忽略赋值操作 c 忽略将常量赋值(包括整数常量、常量字符串等)给强类型的情况 z 忽略Zero赋值,Zero定义为任何非强制转换为强类型的0常量。例如:0L和(int)0都是Zero,
但是(HANDLE)0当HANDLE是一个强类型的时候就不是Zero。(HANDLE
*)0也不是例如使用-strong(Ai,BITS)设置,PC-Lint将会对从非BITS类型数据向BITS类型数据赋值的代码发出告警,但是忽略
变量初始化时的此类赋值。 |
X |
当把强类型的变量赋指给其他变量的时候进行类型检查。弱化参数i, r, p, a, c, z同样适用于X并起相同的作用。 |
J |
选项是当强类型与其它类型进行如下的二进制操作时进行检查,下面是J的参数: e 忽略==、!=和?:操作符 r 忽略>、>=、<和<= o 忽略+、-、*、/、%、|、&和^ c 忽略该强类型与常量进行以上操作时的检查 z 忽略该强类型与Zero进行以上操作时的检查
使用忽略意味着不会产生告警信息。举个例子,如果Meters是个强类型,那么它只在判断相等和其他关系操作时才会被正确地检查,其它情况则不检查,在这个例子中使用J选项是正确的。 |
B |
B选项有两个效果: 1.
出于强类型检查的目的,假设所有的Boolean操作返回一个和Type兼容的类型,所谓Boolean操作就是那些指示结果为true或false的操
作,包括前面提到的四种关系运算符和两种等于判断符,取反操作符!,二元操作符&&和||。 2. 在所有需要判断Bolean值的地方,如if语句和while语句,都要检查结果是否符合这个强类型,否则告警。 例如if(a)...当a为int时,将产生告警,因为int与Bolean类不兼容,所以必须改为if(a != 0)。 |
b |
仅仅假定每一个Bolean类操作符都将返回一个与Type类型兼容的返回值。与B选项相比,b选项的限制比较宽松。 |
l |
库标志,当强类型的值作为参数传递给库函数等情况下,不产生告警。 |
f |
与B或b连用,表示抑止对1bit长度的位域是Boolean类型的假定,如果不选该项表示1bit长度的位域被缺省假定为Boolean类型。 |
这些选项字符的顺序对功能没有影响。但是A和J选项的弱化字符必须紧跟在它们之后。B选项和b选项不能同时使用,f选项必须搭配B选项或b选项使用,如果
不指定这些选项,-strong的作用就是仅仅声明type为强类型而不作任何检查。下面用一段代码演示-strong选项的用法:
//lint -strong(Ab,Bool) <选项是以注释的形式插入代码中> typedef int Bool; Bool gt(int a, b) { if(a) return a > b; // OK else return 0; // Warning } 例
子代码中Bool被声明成强类型,如果没有指定b选项,第一个return语句中的比较操作就会被认为与函数类型不匹配。第二个return语句导致告警
是因为0不是各Bool类型,如果添加c选项,例如-strong(Acb,Bool),这个告警就会被抑制。再看一个例子: /*lint -strong( AJXl, STRING ) */ typedef char *STRING; STRING s; ... s = malloc(20); strcpy( s, "abc" );
由于malloc和strcpy是库函数,将malloc的返回值赋给强类型变量s或将强类型变量s传递给strcpy时会产生强类型冲突,不过l选项抑制了这个告警。 强类型也可用于位域,出于强类型检查的目的,先假定位域中最长的一个字段是优势Boolean类型,如果没有优势Boolean或位域中没有哪个字段比其它字段长,这个类型从位域被切开的位置开始成为“散”类型,例如: //lint -strong( AJXb, Bool ) //lint -strong( AJX, BitField ) typedef int Bool; typedef unsigned BitField; struct foo { unsigned a:1, b:2; BitField c:1, d:2, e:3; } x; void f() { x.a = (Bool) 1; // OK x.b = (Bool) 0; // strong type violation x.a = 0; // strong type violation x.b = 2; // OK x.c = x.a; // OK 118 x.e = 1; // strong type violation x.e = x.d; // OK }
上面例子中,成员a和c是强类型Bool,成员d和e是BitField类型,b不是强类型。为了避免将只有一位的
位域假设成Boolean类型,需要在声明Boolean的-strong中使用f选项,上面的例子就应该改成这
样:-strong(AJXbf,Bool)。
另一个强类型检查选项是index,index的用法是: -index( flags, ixtype, sitype [, sitype] ... ) 这
个选项是对strong选项的补充,它可以和strong选项一起使用。这个选项指定ixtype是一个排除索引类型,它可以和Strongly
Indexed类型sitype的数组(或指针)一起使用,ixtype和sitype被假设是使用typedef声明的类型名称。flags可以是c或
d,c允许将ixtype和常量作为索引使用,而d允许在不使用ixtype的情况下指定数组的长度(Dimensions)。下面是一个使用index
的例子: //lint -strong( AzJX, Count, Temperature ) //lint -index( d, Count, Temperature ) // Only Count can index a Temperature typedef float Temperature; typedef int Count; Temperature t[100]; // OK because of d flag Temperature *pt = t; // pointers are also checked // ... within a function Count i; t[0] = t[1]; // Warnings, no c flag for( i = 0; i < 100; i++ ) t[i] = 0.0; // OK, i is a Count 119 pt[1] = 2.0; // Warning i = pt - t; // OK, pt-t is a Count
上面的例子中,Temperature是被强索引类型,Count是强索引类型。如果没有使用d选项,数组的长度将被映射成固有的类型: Temperature t[ (Count) 100 ]; 但是,这是个小麻烦,像下面那样将数组长度定义成常量更好一些: #define MAX_T (Count) 100 Temperature t[MAX_T];
这样做还有一个好处就是同样的MAX_T还可以用在for语句中,用于限制for语句的范围。需要注意的是,指向强
被索引类型的指针(例如上面的pt)如果用在[]符号(数组符号)中也会被检查类型。其实,无论何时,只要将一个值加到一个指向强被索引类型的指针时,这
个值就会被检查以确认它是一个强索引类型。此外,强被索引指针如果减去一个值,其结果被认为是平常的强索引,所以下面的例子就不会产生告警: i = pt - t;
3.2 变量值跟踪
3.2.1 变量值初始化跟踪
早期的变量值跟踪技术主要是对变量值的初始化进行跟踪,和变量初始化相关的LINT消息主要是644,
645 ("变量可能没有初始化"), 771, 772 ("不可靠的初始化"), 530 ("未初始化的"), and 1401 - 1403
("成员 ... 未初始化")。以下面的代码为例: if( a ) b = 6; else c = b; // 530 message a = c; // 645 message
假设b和c在之前都没有初始化,PC-Lint就会报告b没有初始化(在给c赋值的时候)和c可能没有被初始化(在给a赋值的时候)的消息。而while和for循环语句和上面的if语句稍微有所不同,比较下面的代码: while ( n-- ) { b = 6; ... } c = b; //772 message
假设b在使用之前没有被初始化,这里会报告b可能没有初始化
的消息(当给c赋值时)。之所以会有这样的区别,是因为程序设计者可能知道这样的循环体总是会被至少执行一次。相反,前面的if语句,对于程序设计者来说
比较难以确定if语句是否总会被执行,因为如果是这样的话,这样的if语句就是多余的,应该被去掉。While语句和if比较相似,看下面的例子: switch ( k ) { case 1: b = 2; break; case 2: b = 3; /* Fall Through */ case 3: a = 4; break; default: error(); } c = b; //645 message
尽管b在两个不同的地方被赋值,但是仍然存在b没有被初始化的可能。因此,当b赋值给c的时候,就会产生可能没有初
始化的消息。为了解决这个问题,你可以在switch语句之前给b赋一个默认值。这样PC-Lint就不会产生告警消息,但是我们也失去了让PC-
Lint检查后续的代码修改引起的变量初始化问题的机会。更好的方法是修改没有给b赋值的case语句。 如果error()语句代表那些“不可能发生”的事情发生了,那么我们可以让PC-Lint知道这一段其实是不可能执行的,下面的代码表明了这一点: switch ( k ) { case 1: b = 2; break; case 2: case 3: b = 3; a = 4; break; default: error(); /*lint -unreachable */ } c = b; 注意:这里的-unreachable应该放在error()后面,break的前面。另外一个产生”没有初始化”告警的方式是传递一个指针给free(或者采用相似的方法)。比如: if( n ) free( p ); ... p->value = 3; 在访问p的时候会产生p可能没有被初始化的消息。对于goto语句,前向的goto可能产生没有初始化消息,而向后的goto 会被忽略掉这种检查。 if ( a ) goto label; b = 0; label: c = b; 当在一个大的项目中使用未初始化变量检查时,可能会产生一些错误的报告。这种报告的产生,很大一部分来自于不好的程序设计风格,或者包括下面的结构: if( x ) initialize y ... if( x ) use y 当出现这种情况时,可以采用给y赋初始值的方式,或者利用选项-esym(644,y)关掉变量y上面的初始化检查。
3.2.2 变量值跟踪
变量值跟踪技术从赋值语句、初始化和条件语句中收集信息,而函数的参数被默认为在正确的范围内,只有在从函数中可以收集到的信息与此不符的情况下才产生告警。与变量值跟踪相关的消息有: (1) 访问地址越界消息(消息415,661,796) (2) 被0除消息(54,414,795) (3) NULL指针的错误使用(413,613,794) (4) 非法指针的创建错误(416,662,797) (5) 冗余的布尔值测试(774)
看下面的例子: int a[10]; int f() { int k; k = 10; return a[k]; // Warning 415 } 这个语句会产生警告415(通过 '[' 访问越界的指针),因为PC-Lint保存了赋给k的值,然后在使用k的时候进行了判断。如果我们把上面的例子稍加修改: int a[10]; int f( int n ) { int k; if ( n ) k = 10; else k = 0; return a[k]; // Warning 661 } 这样就会产生告警 661 (可能访问越界指针)。 使用“可能”是因为不是所有的路径都会把10赋值给k。PC-Lint不仅收集赋值语句和初始化,还从条件语句中收集值的信息。比如下面的例子: int a[10]; int f( int k, int n ) { if ( k >= 10 ) a[0] = n; return a[k]; // Warning 661 -- k could be 10 } 这里仍然产生661告警,因为PC-Lint检测到,在使用k的时候,k的值>=10。另外,对于函数来说,它总是假设K是正确的,程序使用者知道他们要做些什么,所以下面的语句不会产生告警: int a[10]; int f( int k, int n ) { return a[k+n]; } // no warning 和检查变量没有初始化一样,还可以检查变量的值是否正确。比如,如果下面例子中的循环一次都没有运行,k可能会超出范围。这时候会产生消息796 (可预见的地址访问越界). int a[10]; int f(int n, int k) { int m = 2; if( k >= 10 ) m++; // Hmm -- So k could be 10, eh? while( n-- ) { m++; k = 0; } return a[k]; // Info 796 - - k could still be 10 } 下面的例子演示了可能使用NULL指针的问题: int *f( int *p ) { if ( p ) printf( "\n" ); // So -- p could be NULL printf( "%d", *p ); // Warning return p + 2; // Warning } 这
里会产生两个告警,因为可能使用了NULL指针,很明显,这两个语句应该在if语句的范围内。为了使你的程序更加健壮,你可能需要打开Pointer-
parameter-may-be-NULL这个开关(+fpn)。这个选项假设所有传递到函数中的指针都有可能是NULL的。数组边界值在高位被检测,
也就是说 int a[10]; ... a[10] = 0; 被检测了,而a[-1]却检测不到。PC-Lint中有两个消息是和指针的越界检查有关的,一个是越界指针的创建,另外一个是越界指针的访问,也就是通过越界指针获取值。在ANSI C([1]3.3.6)中,允许创建指向超过数组末尾一个单元的指针,比如: int a[10]; f( a + 10 ); // OK f( a + 11 ); // error 但是上面创建的两个指针,都是不能访问的,比如: int a[10], *p, *q; p = a + 10; // OK *p = 0; // Warning (access error) p[-1] = 0; // No Warning q = p + 1; // Warning (creation error) q[0] = 0; // Warning (access error) 布尔条件检查不象指针检查那么严格,但是它会对恒真的布尔条件产生告警,比如: if ( n > 0 ) n = 0; else if ( n <= 0 ) n = -1; // Info 774 上面的代码会产生告警(774),因为第二个条件检查是恒真的,可以忽略。这种冗余代码不会导致问题,但它的产生通常是因为逻辑错误或一种错误可能发生的征兆,需要详细的检查。
3.2.3 使用assert(断言)进行补救
在某些情况下,虽然根据代码我们可以知道确切的值,但是PC-Lint却无法获取所有情况下变量的值的范围,这时候会产生一些错误的告警信息,我们可以使用assert语句增加变量取值范围信息的方法,来抑制这些错误的告警信息的产生。下面举例来说明: char buf[4]; char *p; strcpy( buf, "a" ); p = buf + strlen( buf ); // p is 'possibly' (buf+3) p++; // p is 'possibly' (buf+4) *p = 'a'; // Warning 661 - possible out-of-bounds reference PC-Lint无法知道在所有情况下变量的值是多少。在上面的例子中,产生告警的语句其实并不会带来什么危害。我们可以直接使用 *p = 'a'; //lint !e661 来抑制告警。另外,我们还可以使用assert工具来修正这个问题: #include ... char buf[4]; char *p; strcpy( buf, "a" ); p = buf + strlen( buf ); assert( p < buf + 3 ); // p is 'possibly' (buf+2) p++; // p is 'possibly' (buf+3) *p = 'a'; // no problem 由于assert在NDEBUG被定义时是一个空操作,所以要保证Lint进行的时候这个宏没有被定义。
为了使assert()和你的编译器自带的assert.h一起产生上面的效果,你需要在编译选项文件中添加一个选项。例如,假设assert 是通过以下的编译器宏定义实现的: #define assert(p) ((p) ? (void)0 : __A(...)) 考虑到__A()会弹出一个消息并且不会返回,所以这个需要添加的选项就是: -function( exit, __A ) 这个选项将exit函数的一些非返回特征传递给__A函数。做为选择结果,编译器可能将assert实现成一个函数,例如: #define assert(k) _Assert(k,...) 为了让PC-lint知道_Assert是一个assert函数,你需要使用-function( __assert, _Assert )选项或-function( __assert(1), _Assert(1) )选项复制__assert()函数的语义 许多编译器的编译选项文件中已经存在这些选项了,如果没有的话,你可以复制一个assert.h文件到PC-lint目录下(这个目录由于使用了-i选项,文件搜索的顺序优先于编译器的头文件目录)。
3.2.4 函数内变量跟踪
PC-Lint的函数值跟踪功能会跟踪那些将要传递给函数(作为函数参数)变量值,当发生函数调用时,这些值被用来初始化函数参数。这种跟踪功能被用来测定返回值,记录额外的函数调用,当然还可以用来侦测错误。考察下面的例子代码: t1.cpp: 1 int f(int); 2 int g() 3 { return f(0); } 4 int f( int n ) 5 { return 10 / n; } 在这个例子中,f()被调用的时候使用0作为参数,这将导致原本没有问题的10/n语句产生被0除错误,使用命令lin -u t1.cpp可以得到以下输出: --- Module: t1.cpp During Specific Walk: File t1.cpp line 3: f(0) t1.cpp 5 Warning 414: Possible division by 0 [Reference:File t1.cpp: line 3] 你
第一个注意到的事情是短语“During Specific
Walk”,紧接着是函数调用发生的位置,函数名称以及参数,再下来就是错误信息。如果错误信息中缺少了错误再现时的错误行和用来标记错误位置的指示信
息,这是因为检查到错误的时候代码(被调用函数的代码)已经走过了。如果像下面一样调换一下两个函数的位置: t2.cpp: 1 int f( int n ) 2 { return 10 / n; } 3 int g() 4 { return f(0); } 这种情况下就不会出现被0除的告警,因为此时f(0)在第四行,函数f()的代码已经过了,在这种情况下就需要引入multi-pass选项。如果在刚才的例子中使用lin -u -passes(2) t2.cpp命令,那么输出就变成: --- Module: t2.cpp /// Start of Pass 2 /// --- Module: t2.cpp During Specific Walk: File t2.cpp line 4: f(0) t2.cpp 2 Warning 414: Possible division by 0 [Reference:File t2.cpp: line 4]
使用-passes(2)选项将会检查代码两遍,一些操作系统不支持在命令行中使用-passes(2),对于这样
的系统,可以使用-passes=2 或 -passes[2]代替。通过冗长的信息可以看出来,以pass
2开始表示第一次检查没有产生告警信息。这一次得到的错误信息和前一次不同,在某种情况下我们可以推断出指定函数调用的返回值,至少可以得到一些返回值的
属性。以下面的模块为例: t3.cpp: 1 int f( int n ) 2 { return n - 1; } 3 int g( int n ) 4 { return n / f(1); } 使用命令 lin -u -passes(2) t3.cpp,可以得到以下输出信息: --- Module: t3.cpp /// Start of Pass 2 /// --- Module: t3.cpp
{ return n / f(1); } t3.cpp 4 Warning 414: Possible division by 0 [Reference:File t3.cpp: lines 2, 4]
第一遍检查我们知道调用函数f()传递的参数是1,第二遍检查先处理了函数f(),我们推断出这个参数将导致返回结
果是0,当第二遍检查开始处理函数g()的时候,产生了被0除错误。应该注意到这个信息并不是在短语“During Specific
Walk”之前出现的,这是因为错误是在对函数g()进行正常的处理过程中检测到的,此时并没有使用为函数g()的参数指定的值。指定的函数调用能够产生
附加的函数调用,如果我们pass足够多的检测次数,这个过程可能会重复发生,参考下面的代码: t4.cpp: 1 int f(int); 2 int g( int n ) 3 { return f(2); } 4 int f( int n ) 5 { return n / f(n - 1); } 第五行的分母f(n-1)并不会引起怀疑,直到我们意识到f(2)调用将导致f(1)调用,最终会调用f(0),迫使最终的返回值是0。使用下面的命令行: lin -u -passes(3) t4.cpp, 输出结果如下: --- Module: t4.cpp { return f(2); } t4.cpp 3 Info 715: Symbol 'n' (line 2) not referenced /// Start of Pass 2 /// --- Module: t4.cpp /// Start of Pass 3 /// --- Module: t4.cpp During Specific Walk: File t4.cpp line 3: f(2) File t4.cpp line 5: f(1) t4.cpp 5 Warning 414: Possible division by 0 [Reference:File t4.cpp: lines 3, 5] 到这里已经处理了三遍才检测到可能的被0除错误,想了解为什么需要处理三遍可以看看这个选项-specific_wlimit(n)。需要注意的是,指定的调用序列,f(2),f(2),是作为告警信息的序言出现的。
3.3 赋值顺序检查
当一个表达式的值依赖于赋值的顺序的时候,会产生告警564。这是C/C++语言中非常普遍的一个问题,但是很少有编译器会分析这种情况。比如 n++ + n 这个语句是有歧义的,当左边的+操作先执行的话,它的值会比右边的先执行的值大一,更普遍的例子是这样的: a[i] = i++; f( i++, n + i ); 第
一个例子,看起来好像自加操作应该在数组索引计算以后执行,但是如果右边的赋值操作是在左边赋值操作之前执行的话,那么自加一操作就会在数组索引计算之前
执行。虽然,赋值操作看起来应该指明一种操作顺序,但实际上是没有的。第二个例子是有歧义的,是因为函数的参数值的计算顺序也是没有保证的。能保证赋值顺
序的操作符是布尔与(&&)或(||)和条件赋值(? :)以及逗号(,),因此: if( (n = f()) && n > 10 ) ... 这条语句是正确的,而: if( (n = f()) & n > 10 ) ... 将产生一条告警。
3.4 弱定义检查
这里的弱定义包含是以下内容:宏定义、typedef名字、声明、结构、联合和枚举类型。因为这些东西可能在模块中被过多定义且不被使用,PC-Lint
有很多消息用来检查这些问题。PC-Lint的消息749-769 和1749-1769都是保留用来作为弱定义提示的。 (1) 当一个文件#include的头文件中没有任何引用被该文件使用,PC-Lint会发出766告警。 (2) 为了避免一个头文件变得过于大而臃肿,防止其中存在冗余的声明,当一个头文件中的对象声明没有被外部模块引用到时,PC-Lint会发出759告警。 (3) 当变量或者函数只在模块内部使用的时候,PC-Lint会产生765告警,来提示该变量或者函数应该被声明为static。 如果你想用PC-Lint检查以前没有检查过的代码,你可能更想将这些告警信息关闭,当然,如果你只想查看头文件的异常,可以试试这个命令: lint -w1 +e749 +e?75? +e?76? ...
3.5 格式检查
PC-Lint会检查printf和scanf(及其家族)中的格式冲突,例如: printf( "%+c", ... ) 将
产生566告警,因为加号只在数字转换时有用,有超过一百个这样的组合会产生告警,编译器通常不标记这些矛盾,其他的告警还有对坏的格式的抱怨,它们是
557和567。我们遵循ANSI C建立的规则,可能更重要的是我们还对大小不正确的格式进行标记(包括告警558, 559, 560 和
561)。比如 %d 格式,允许使用int和unsigned
int,但是不支持double和long(如果long比int长),同样,scanf需要参数指向的对象大小正确。如果只是参数的类型(不是大小)与
格式不一致,那将产生626和627告警。-printf 和
-scanf选项允许用户指定与printf或scanf函数族类似的函数,-printf_code 和
-scanf_code也可以被用来描述非标准的 % 码。
3.6 缩进检查
根据代码中的缩进问题,PC-Lint也会产生相应的告警,因为缩进的问题有很大一部分是由于代码结构不良或者大括号的遗漏造成的。比如下面的例子: if( ... ) if( ... ) statement else statement 很
明显这里的else是和第一个if语句对应的,而在这里编译器则把它和第二个if对应起来。PC-Lint会对这种情况产生告警。和这样的缩进检查相关的
告警主要有三个725(no positive indentation)、525(negatively indented
from)、539(Did not expect positive indentation from
Location)要进行缩进检查,我们首先要设置文件中的tab键所对应的空格数,默认的是占用8个空格,这个参数可以用-t#选项进行修改。比如
-t4表示tab键占用4个空格长度。另外,缩进检查还和代码的编码格式策略相关,需要进行必要的调整。
3.7 const变量检查
对于const变量的检查,PC-Lint是完全支持的。使用const变量,对于提高代码的质量非常有好处,看一下下面的例子: char *strcpy( char *, const char * ); const char c = 'a'; const char *p = &c; void main() { char buf[100]; c = 'b'; *p = 'c'; strcpy( p, buf ); ... 这
里的c和*P指向的内容都是静态变量,不可修改。上面的代码明显违反了这个规定,会产生Error(11),另外,把P作为第一个参数传入strcpy
中,会产生告警605(Increase in pointer
capability),而把buf作为第二个参数传入strcpy函数中,会产生告警603(Symbol 'Symbol' (Location)
not initialized),因为buf没有初始化,而作为静态变量的第二个参数,是不能在strcpy函数中再被初始化的。
3.8 volatile变量检查
对于volatile变量的检查,在PC-Lint中有这样的规定,如果一个表达式中同时使用了两次相同的volatile变量,那么就会给出564告警,因为这时候会产生赋值顺序的问题。 volatile char *p; volatile char f(); n = (f() << 8) | f(); /* Warning 564 */ n = (*p << 8) | *p; /* Warning 564 */
4.2.3 PC-Lint与source insight集成
PC-Lint与source
insight的集成也是通过添加定制命令实现的,从“Options”菜单中选择“Custom
Commands”命令项。点击“Add…”按钮,如图4.21所示,在弹出的“Custom Commands”窗口中完成以下输入: 在Name栏中输入“PC-lint Check Current File”,原则上这个名称可以随便起,只要你能搞清楚它的含义就可以了; 在Run栏中输入“C:\PcLint\lint-nt -u -iC:\PcLint\Lint std_f env-si %f”其中C:\PcLint是你PC-LINT的安装目录,std_f表示为Source Insight定制的配置文件std_f.lnt; 在Output栏中选择“Iconic Window”、“Capture Output”选项; 在Control栏中选择“Save Files First”; 在Source Links in Output栏中选择“Parse Links in Output”、“File,then Line”; 在Pattern栏中输入“^\([^ ]*\) \([0-9]+\)”;
图4.21 在Source Insight中添加定制命令
命令添加完成后就可以点击“Run”按钮就可以对当前文件执行PC-Lint检查。为了方便使用,还可以点击“Menu...”按钮将这个定制命令添加到Source Insight的菜单中。
4.2.4 PC-Lint与UltraEdit集成
在UltraEdit中集成PC-Lint的方法和Source Insight类似,也是添加一个定制命令菜单,具体实现方法是先单击UltraEdit的“高级”菜单中的“工具配置”命令,如图4.22所示,在打开的配置窗口中依次输入以下内容: 在“菜单项目名”栏输入“PC-lint Check Current File”; 在
“命令行”栏输入以下命令:C:\PCLint\lint-nt –u -iC:\PCLint std env-si %f
其中,C:\PCLint是PC-Lint的安装目录,使用std.lnt中的配置,由于UltraEdit和Source Insightde
的检查环境类似,所以借用env-si中的环境配置; 在“工作目录”栏输入以下路径:E:\code,这是代码所在目录; 选中“先保存所有文件”选项; 在“命令输出”栏中,选中“输出到列表”和“捕捉输出”两个选项; 点“插入”将命令行插入UltraEdit的菜单中;
图4.22 在UltraEdit中添加定制命令
此时在UltraEdit的“高级”菜单中会增加一个“PC-lint Check Current File”菜单,点击该菜单即可对当前文件执行PC-lint检查。
五 总结
软件除错是软件项目开发成本和延误的主要因素,PC-lint能够帮你在程序动态测试之前发现编码错误,降低软件消除错误的成本。使用PC-Lint在代
码走读和单元测试之前进行检查,可以提前发现程序隐藏错误,提高代码质量,节省测试时间。另外,使用PC-lint的编码规则检查,可以有效地规范软件人
员的编码行为。如果能够在软件开发过程中有效地使用PC-lint代码检查工具,将大大地提高代码质量,降低软件成本。
参考文献
[1] Gimpel Software. Reference Manual for PC-lint/FlexeLint. July,2001 [2] PC-Lint选项详解
附录一 PC-Lint 重要文件说明
Msg.txt :解释告警的内容。 options.lnt :反映全局编译信息显示情况的选项文件,通常需要添加自定选项以使代码检查更为严格。 env-xx.lnt :讲述如何将PC-lint与对应的编辑环境结合起来,xx是si表示是为Source Insight配置的检查环境,xx是vc6则表示是为Visual C++ 6.0准备的检查环境。 co-xxx.lnt :选定的编译器与库选项。 std.lnt :标准配置文件,包含内存模型等全局性东西。 lib-xxx.lnt :库类型的列表,包括标准C/C++库,MFC库,OWL库等等。 au-xxx.LNT :C++编程提出过重要建议的作者,选择某作者后,他提出的编程建议方面的选项将被打开。
附录 二 错误信息禁止选项说明
命令格式 说明 代码中的举例 -e# 隐藏某类错误 /*lint -e725 */ -e(#) 隐藏下一表达式中的某类错误 /*lint –e(534) */ printf(“it’s all”); !e# 隐藏本行中的错误 /*lint !e534*/ printf(“it’s all”); -esym(#, Symbol) 隐藏有关某符号的错误 /*lint –esym(534, printf)*/ printf(“it’s all”); -elib(#) 隐藏头文件中的某类错误 /*lint –elib(129) */ #include “r01.h” -efunc(#, ) 隐藏某个函数中的特定错误 /*lint –efunc(534, mchRelAll)*/ unsigned int mchRelAll(mchHoData *pHoData) { printf(“it’s all”); }
附录 三 PC-Lint检测中的常见错误
错误编码 错误说明 举例 40 变量未声明 506 固定的Boolean值 char c=3; if(c<300){} 525 缩排格式错误 527 无法执行到的语句 if(a > B) return TRUE; else return FALSE; return FALSE; 529 变量未引用 检查变量未引用的原因 530 使用未初始化的变量 534 忽略函数返回值 539 缩排格式错误 545 对数组变量使用& char arr[100], *p; p=&arr; 603 指针未初始化 void print_str(const char *p); … char *sz; print_str(sz); 605 指针能力增强 void write_str(char *lpsz); … write_str(“string”); 613 可能使用了空指针 616 在switch语句中未使用break; 650 比较数值时,常量的范围超过了 if( ch == 0xFF ) ... 变量范围 713 把有符号型数值赋给了无符号型 数值 715 变量未引用 725 Indentation错误 734 在赋值时发生变量越界 int a, b, c; … c=a*b; 737 无符号型变/常量和有变量型 变/常量存在于同一个表达式中。 744 在switch语句中没有default 752 本地声明的函数未被使用 762 函数重复声明 774 Boolean表达式始终返回真/假 char c; if(c < 300)
|