pc_lint的用法转

PC-Lint是一款C/C++软件代码静态分析工具,不仅可以检查一般的语法错误,还可以检查潜在的错误,比如数组访问越界、内存泄漏、使用未初始化变量、使用空指针等。在单元测试前使用PC-Lint来检查代码,可以提前发现程序中的潜在的错误,提高代码
的质量。
本文主要从两个方面介绍PC-lint,第一部分是在与不同的工具安装集成,第二部分是PC-lint的使用方法。

1. 安装PC-lint及如何集成到VC6.0和Source Insight
1.1 安装PC-lint
(1)下载PC-lint(PC-Lint 8.00w)。
(2)将下载的压缩包解压至到D盘,并对文件夹重命名为PC-Lint,这样路径为D:\PC-Lint。
备注:这也叫安?呵呵,似乎算免安装更合适吧。
1.2 将PC-lint集成到VC6.0
1.2.1 对单个C/C++进行静态代码分析
(1)将D:\PC-Lint\lnt 下的3个文件lib-w32.lnt,env-vc6.lnt,co-msc60.lnt复制到D:\PC-Lint\下。
(2)打开co-msc60.lnt,将该文件倒数第二行"lib-ole.lnt"的内容改为"D:\PC-Lint\lnt\lib-ole.lnt",也就是在前面加上绝对路径,以免在后面的步骤中无法找到该文件。
(3)在D:\PC-Lint\下创建std.lnt和options.lnt两个文件,其中std.lnt的内容如下:

注:-i后面的路径名为VC 6.0的安装路径和及其头文件路径;options.lnt可以暂时为空。
(4)在VC6.0的菜单栏中,Tools--->Customize...-->tools 新建一个名为pclint的项,在下面填入
"Command"项填入: D:\PC-Lint\lint-nt.exe
"Argument"项填入: -u D:\PC-Lint\std.lnt  D:\PC-Lint\env-vc6.lnt "$(FilePath)"

然后在Use Output Window 打上勾即可。
(5)在VC6.0的菜单栏Tools下多了一个pclint选项,打开一个VC项目后,就可以使用该选项对单个C/C++文件进行静态代码分析了。

1.2.2 对一个VC6.0项目进行静态代码分析
下面的步骤是在1.2.1的前三步的基础上进行的。
(1)先到http://www.weihenstephan.de/~syring/win32/UnxUtils.zip下载UnxUtils.zip。需要利用unix中的find等命令来查找当前目录下的C和C++文件,然后再将它们送给lint程序处理。
(2)解压UnxUtils.zip到D盘,这样路径为D:\UnxUtils。
(3)在在VC6.0的菜单栏Tools下多了一个pclint_prj选项,打开一个VC项目后,就可以使用该选项对单个C/C++文件进行静态代码分析了。
"Command"项填入: D:\UnxUtils\usr\local\wbin\find.exe
"Argument"项填入: $(FileDir) -name *.c -o -name *.cpp | D:\UnxUtils\usr\local\wbin\xargs D:\PC-Lint\lint-nt -i"D:\UnxUtils\usr\local" -u D:\PC-Lint\std.lnt D:\PC-Lint\env-vc6.lnt

然后在Use Output Window 打上勾即可。
(4)在VC6.0的菜单栏Tools下多了一个pclint_prj选项,打开一个VC项目后,就可以使用该选项对VC项目进行静态代码分析了。
注意:"Argument"项填的内容一定要注意参数中的路径,如果你不使用上述路径,可以用新路径将参数中的路径替换,以免重新写参数而导致出错。

1.3 将PC-lint集成到Source Insight 3.5中
1.3.1 对单个C/C++进行静态代码分析
(1)打开SourceInsight, 选择Options-->Custom Commands-->Add, 输入pclint
(2)在Run中填写: D:\PC-Lint\lint-nt -u D:\PC-Lint\std.lnt D:\PC-Lint\env-vc6.lnt %f
(3)Dir不用填写,将Iconic Window, Capture Output, Parse Links in OutPut,三项勾选上,并将File,then Line的单项选择也选上。
(4)然后点右侧的Menu...,在弹出的界面中在下拉框Menu中选择View,然后在下面的Menu Cotents中选择<end of menu>, 右侧点Insert即可。
(5)可以在Source Insight 3.5菜单View下看到刚才新建的项pclint,打开项目的任意一个待分析的源文件,运行pclint即可进行静态代码分析了。

1.3.2 对一个项目进行静态代码分析
下面的步骤是在1.2.2的基础上进行的。
(1)打开SourceInsight, 选择Options-->Custom Commands-->Add, 输入pclint_prj
(2)在Run中填写:
D:\UnxUtils\usr\local\wbin\find.exe %d -name *.c -o -name *.cpp | D:\UnxUtils\usr\local\wbin\xargs D:\PC-Lint\lint-nt -i"D:\UnxUtils\usr\local" -u D:\PC-Lint\std.lnt D:\PC-Lint\env-vc6.lnt
(3)Dir不用填写,将Iconic Window, Capture Output, Parse Links in OutPut,三项勾选上,并将File,then Line的单项选择也选上。
(4)然后点右侧的Menu...,在弹出的界面中在下拉框Menu中选择View,然后在下面的Menu Cotents中选择<end of menu>, 右侧点Insert即可。
(5)可以在Source Insight 3.5菜单View下看到刚才新建的项pclint_prj,打开项目,运行pclint_prj即可对项目进行静态代码分析了。

2.PC-lint的用法

2.1 pc-lint目录下几个重要的文件及程序
lint-nt.exe:PC-lint的可执行程序。
config.exe: PC-lint的配置文件程序。
pc-lint.pdf:PC-lint的PDF格式的在线手册,本文的大部分内容是从中得来的。
msg.txt:     对于错误消息编号的详细解释。
Lnt\:       这个目录下有些东西还是值得认识一下。
co-....lnt: 指定的编译器的可选编译文件。
co.lnt:     通用的可选编译文件。
sl-....c     非ANSI编译器的标准库文件模块
sl.c:        非ANSI编译器的通用标准库文件模块
env-....lnt:不同平台下的可选文件,包括MS Visual Studio和其他各种编辑工具。
lib-....lnt:可选文件, 特定的"有挑战性"的库文件。
au-....lnt: 可选文件, 作者们推荐的检测条件。

2.2 PC-lint的介绍
2.2.1 错误信息编号
对于大部分的错误消息,PC-lint都提供了一个关联的错误编号。小于1000的错误编号是分配给C语言的,1000以上的错误编号则是分配给C++语言的。1000呢?呵呵,被保留了。先看一个表格。
                                 C              C++           告警级别
语法错误(Syntax Errors)      1   - 199      1001 - 1199           1
内部错误(Internal Errors)    200 - 299                            0
致命错误(Fatal Errors)       300 - 399                            0
告警(Warnings)               400 - 699      1400 - 1699           2
提示(Informational)          700 - 899      1700 - 1899           3
可选信息(Elective Notes)     900 - 999      1900 - 1999           4

对于C语言,1~199是与语法错误;200~299是PC-lint内部错误,应该决不会发生的;300~399是致命错误,通常是由于超越了某些限制;400~699是警告消息,提示被检查的程序中可能存在错误;700~899是提示信息,这些提示信息可能有错误,也可能是合法的程序,取决于个人的编程风格;900~999则是一些称为可选信息,一般不会自动输出。

PC-lint提供了高级级别设置选项-wLevel,缺省的级别为3级。-w0, -w1 , -w2, -w3, -w4 分别可以生成上述表格中对应告警级别和级别更低的告警,其中级别越低告警越重要。同样,也提供了处理库函数的头文件告警级别的选项-wlib(Level),缺省的级别也是3级,级别对应的含义与前者一样。

2.2.2 选项的规则
通过使用加号"+"和减号"-",以注释的形式插入代码中,来恢复和屏蔽指定的被检查的选项。格式如下:
/*lint option1 option2 ... optional commentary */
或者
//lint option1 option2 ... optional commentary
注意:lint必须是小写,选项的一行不能超过80个字符,否则导致致命的错误,错误信息的编号就是323。如果选项确实有很长,可以通过换行的方式来实现。另外屏蔽和恢复的选项的代码可以放在宏定义中,宏被展开后,这些选项会生效。

2.2.3 选项中的空格
因为空格是用来分隔选项的,除此之外只能出现在圆括号的旁边或是空格自身被引用(例如operator new按语法要求中间就有空格)。举个例子:
-esym(534,printf,scanf,operator new)
-esym(534, printf, scanf, operator new)
-esym( 534 , printf , scanf , operator new )
对于第三个,空格出现在圆括号的旁边,也出现在自身被引用的地方(operator new)。另外operator和new之间出现两个空格也是不合法的,因为它违反了语法规则。另外,也可以使用双引号(""来保护空格,例如:
-"dWORD=unsigned short"

2.2.4 选项的分类
PC-lint的选项有300多种,可以分为下面几类:
(1)禁止错误信息
选项开头使用"-e"可以禁止指定的错误消息,使用"+e"恢复指定的错误消息。如果禁止消息,只不过不让消息输出,并不影响PC-lint的处理过程。顺便提一下前面提到的"-wLevl",这个选项是禁用指定级别及以上的消息的显示。

1)格式一:
-e#  禁止指定的错误消息,#代表数字或是数字匹配符,错误消息的编号为#。
+e#  恢复指定的错误消息,错误消息的编号为#。

举个例子:
/*lint -504*/
...Code.....
/*lint +504*/
第一行关闭了编号为504的错误消息,最后一个行则重新打开了编号为504的错误消息。其中数字也可以包含匹配符号,'?'匹配单个字符,"*"匹配多个字符。
比如:
(1)-e7???, 则关闭了700~799这个范围内的错误消息。
(2)-e1*, 则关闭了所有以1开头的编号的错误消息。
同样匹配符也能使用在-esym, -elib, -elibsym, -efile, -efunc, -emacro, -etemplate, -e(#), --e(#), -e{#} and –e{#}.

2)格式二:
-e(#[,#]...) 为下一个表达式禁止指定的错误消息,在这个表达式结束后被禁止的错误消息自动恢复,#代表数字或是数字匹配符,错误消息的编号为#。
举个例子:
a = /*lint -e(413) */ *(char *)0;
它等价于下面的语句:
a = /*lint -save -e413 */ *(char *)0
/*lint -restore */;
前一种方法更简单且更有效。

3)格式三:
--e( # [,#]... ) 比上面的那个管的更宽一些,它对整个表达式有效,举个例子就明白它与上面的区别了。
举个例子:
a = /*lint --e(413) */ *(int *)0 + *(char *)0;
整个表示式是指*(int *)0 + *(char *)0,下个一表达式指的是*(int *)0。区别一目了然,例子中将禁止两个编号为413 的错误消息, 如果使用 -e(413) ,则只禁止第一个编号为 413 的错误消息。

4)格式四:
-e{ # [, #] …} 对下一个语句或者声明有效
举个例子:
//lint -e{715} suppress "k not referenced"
void f( int n, unsigned u, int k )
{
    //lint -e{732} suppress "loss of sign"
    u = n; // 732 not issued
    //lint -e{713} suppress "loss of precision"
    if(n)
   {
       n = u; // 713 not issued
   }
} // 715 not issued
通过例子可以看出,这种格式放在函数之前,则对整个函数产生作用,放在赋值语句前则只对赋值语句起作用,放在if或while前面,则对这一段语句起作用。在C++的类定义或命名空间声明前放这么个选项,则将整个类或命名空间内的代码中指定的错误消息给禁止了。

5)格式五:
--e{ # [, #] … } 对于其所处的 {} 号区域内的整个代码体有效。 {} 号区域可能是复杂的语句、函数体、C++的类,结构体或联合体的定义、C++的命名空间等。如果这个选项放在一个模块前,而模块前没有 {},则对整个模块生效。

6)格式六:
!e# 仅对其所在行有效。
if( x = f(34) ) //lint !e720
y = y / x;
在这个例子中,仅对那一行禁止编号为720 的错误消息。看一下C语言的代码的用法:
if( x = f(34) ) /*lint !e720 */
y = y / x;
如果有更多的错误信息要禁止,而又无法使用通配符,则可以使用下面的方法:
n = u / -1; //lint !e573 !e721

7)格式七:
-ealetter 参数不匹配禁止

格式八:
-efile( #, file [, file] ... ) inhibits and
+efile( #, file [, file] ... ) re-enables

9)格式九:
-efunc( #, Symbol [, Symbol] ... ) inhibits and
+efunc( #, Symbol [, Symbol] ... ) re-enables

10)格式十:
-elib( # [, #] ... ) inhibits and
+elib( # [, #] ... ) re-enables

11)格式十一:
-elibsym( # [, # ] ... ) inhibits
+elibsym( # [, # ] ... ) re-enables

12)格式十二:
-emacro( #, symbol, ... )
+emacro( #, symbol, ... )

13)格式十三:
-emacro( (#), symbol, ... )
--emacro( (#), symbol, ... )
-emacro( {#}, symbol, … )
--emacro( {#}, symbol, … )

14)格式十四:
-esym( #, Symbol [, Symbol] ... ) inhibits and
+esym( #, Symbol [, Symbol] ... ) re-enables
禁止和恢复指定的符号的错误消息。举个C++的例子(实际应用中不太可能出现):
class X
{
    void f(double, int);
};
分析结果中会提示某行的member X::f(double, int)没有被引用,为了屏蔽这个消息,你可以使用
-esym( 754, X::f )
符号的完整签名为X::f(double, int),然而符号的名字为X::f,而且可以使用符号的名字来禁止错误消息的出现。另外,-esym 和 -e# 选项是独立的,举个例子:
-e714 +esym( 714,alpha )
对于alpha来说,它禁止了编号为714的错误消息,第二个选项并不会恢复编号为714的错误消息,除非前面有个对应的-esym(714,alpha)。

15)格式十五:
-etd( TypeDiff [, ...] ) inhibits
+etd( TypeDiff [, ...] ) re-enables

16)格式十六:
-etemplate( # [,#] ... )
+etemplate( # [,#] ... )
禁止和恢复在扩展模板(expanding templates)时的错误消息。

(2)变量类型大小和对齐选项
1)变量类型大小选项
这组选项允许设置各种变量类型的大小和对齐方式。由于默认的设置于绝大多数的编译器都是一致的,所以这些参数的单独设置通常是没有必要的。使用变量类型大小的选项是为了特定的架构,而不是本地架构。举个例子,你需要为嵌入式系统设置int和pointers通常为16位,那么你应该指定:
lint -si2 -sp2 ...
下面的列表,#号代表一个小的整型值,仅举几个:
-sb# 字节的位数为#,默认的是-sb8,
-sbo# sizeof(bool)就变为一个参数了,默认值为1,
-sc# sizeof(char) 就变为 #,默认值为1,
-slc# sizeof(long char) 就变为 #,默认值为2,
...
2)对齐选项
仅有两个可选择的注释信息来检测不规律的对齐,它们的错误编号是958和959,详细的介绍就省略了吧。

(3)冗长信息选项
冗长信息选项采用-v和+v开头来控制,冗长信息指的是在检测过程中产生的一些与编译过程有关的信息,或者说,冗长信息与编译过程中消息的频率和种类有关。如果使用-v,则冗长信息进被发送到标准输出,而是用+v,冗长信息进则会被发送到标准输出和标准错误中。如果要将错误信息重定向到一个文件中,并想要在终端查看冗长信息和被解释后的错误消息,+v选项是非常有用的。

(4) 标志选项
采用+f,++f,-f,--f开头开介绍标志位。一个标志位在内部采用一个整型值表达。通过认为:
ON  假如整型值大于0
OFF 假如整型值小于或等于0
默认设置时1为ON,0为off,对于的关系如下:
+f...:通过把标志为设置为1而把它设置为ON
-f...:通过把标志为设置为0而把它设置为OFF
++f...:标志位增1
--f...:标志位减1
后面两个选项在只设置局部标志而不影响全局设置时,非常有用。

(5)消息显示选项
消息显示选项用于定义消息输出的格式。
1)控制错误消息的高度。
-h选项被用来控制消息的高度,通常的格式如下:
-h[F][f][a][r][mn][m][m/M/][I]N
s 表示每条消息后的空格。其他的就不介绍了。
2)控制错误消息的宽度。
格式如下:
-width(W,Indent)
例如:-width(99,4)
3)消息格式化选项
格式如下
-format=...
3)附加信息选项
格式如下:
-append(errno,string)
(6)其他选项
1)-A 要求严格使用ANSI C/C++处理。
其他的不介绍了。

2.2.5 库文件检查
这里的库文件时指那些编译后的库文件,比如标准的I/O库,又比如第三方的库文件,例如windows的库文件。关注库文件的重要特色是因为以下两点:
(1)库文件的源代码通常不可获得。
(2)库文件在多个正被你使用pc-lint检查的程序中使用。
库的头文件描述了库的部分或完整的接口。举个例子:
hello.c
#include <stdio.h>
main()
{
    HelloWorld();
    printf( "hello world\n" );
}

如果没有"#include <stdio.h>"这一行代码,使用PC-lint检查上述代码,PC-lint会抱怨printf()既没有声明也没有定义,会给出编号为718错误信息。如果"stdio.h"被当做一个库文件的头文件,那么PC-lint不会要求给出printf()的源代码。

(1)格式一:
+libclass( identifier[, identifier] ... )
用来指定名为identifier的头文件当做库头文件。identifier是其中下面之一:
angle: 所有尖括号包含起来的头文件
foreign:所有在搜索列表中目录下的头文件
ansi:标准ANSI C/C++ 的头文件
all:所有头文件
默认情况下,+libclass(angle,foreign) 是有效的,这也是为什么hello.c的代码没有要求给出printf()源代码的原因。
(2)格式二:
+libdir( directory [, directory] ... )
-libdir( directory [, directory] ... )
指定目录的。
(3)格式三:
+libh( file [, file] ... )
-libh( file [, file] ... )
增加或移出那些已经被 +libclass 和 +/-libdir 已确定的头文件,以达到要求或不要求给出源代码。举个例子:
+libclass( ansi, angle )
+libh( windows.h, graphics.h )
+libh( os.h ) -libh( float.h )
要求所有的ansi和angle(除了float.h),还有那三个windows.h, graphics.h, os.h也会被当做库头文件。

2.2.6 强类型检查
什么是强类型?C/C++的变量都有类型,不同类型之间的赋值可能会产生告警,可以说C/C++变量的类型是强类型。有强类型,自然有弱类型。比如一些脚本语言,它们的变量就不存在具体的类型,可以相互之间赋值,它们就是弱类型语言。为什么在使用PC-lint对C/C++进行检查时,要进行强类型检查呢?因为有诸如使用typedef定义的数据类型,可以避开编译器的类型检查。举个例子:
typedef int Count;
typedef int Bool;
Count n;
Bool stop;
...
n = stop ;

对于这段代码,编译器是不会告警的,但是最后一行代码是错误的。所以,强类型检查选项是必要的。
   强类型检查选项"-strong"和附加选项"-index"可以完全的或部分的对typedef定义的数据类型进行强类型检查,保证相同类型的变量才能相互赋值。
(1)强类型检查选项strong的格式如下:
-strong( flags[, name] ... )
name是强类型,flags是指定的属性,flags参数可以是A、J、X、B、b、l和f。如果name被省略,所有使用typedef定义的数据类型的flags的属性不能被其他的-strong选项所识别。

flags参数  弱化字符
   A       i 忽略初始化
           r 忽略Return语句
           p 忽略参数传递
           a 忽略赋值操作
           c 忽略将常量赋值(包括整数常量、常量字符串等)给强类型的情况
           z 忽略Zero赋值

   X       当把强类型的变量赋值给其他变量的时候进行类型检查。弱化参数i, r, p, a, c, z同样适用于X并起相同的作用。
   J       当强类型与其它类型进行运算时(逻辑运算、关系运算、数学运算等)进行检查
           e 忽略==、!=和?:操作符
           r 忽略>、>=、<和<=
           o 忽略+、-、*、/、%、|、&和^
           c 忽略该强类型与常量进行以上操作时的检查
           z 忽略该强类型与Zero进行以上操作时的检查
   B       类型是Boolean,一般情况下只能使用一个name(指格式中的name), 而且它应该和其他flags联合使用。
           B选项有两个作用:
           1. 出于强类型检查的目的,每一个Boolean操作符都采用返回一个和Type兼容的类型。Boolean操作符就是那些显示
              为true或false,也包括前面提到的四种关系运算符和两种等于判断符,取反操作符!,二元操作符&&和||。
           2. 在所有需要判断Bolean值的上下文中,比如if语句和while语句,都应该检查这个强类型,否则产生告警。
   b      仅仅假定每一个Bolean类操作符都将返回一个与Type类型兼容的返回值。与B选项相比,b选项的限制比较宽松。
   l      库标志,当强类型的对象从库函数中获得值,或者将强类型对象的值作为参数传递给库函数等情况下,不产生告警。
   f      与B或b连用,表示不应该将1位长度的位域当做Boolean类型,否则表示1位长度的位域被缺省假定为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),这个告警就会被禁止。
(2) 另一个强类型检查选项是index,格式如下:
-index( flags, ixtype, sitype [, sitype] ... )
这个选项是对strong选项的补充,它可以和-strong选项一起使用。这个选项指定ixtype是一个排他的索引类型,它可以和强索引类型sitype的数组(或指针)一起使用,ixtype和sitype被假定为后来使用typedef声明来定义的的类型名称。flags可以是c或d,c允许将ixtype和常量作为索引使用,而d允许在不使用ixtype的情况下指定数组的维数(Dimensions)。

 

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 <assert.h>
...
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个空格长度。另外,缩进检查还和代码的编码格式策略相关,需要进行必要的调整。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值