静态检查方法包括两种,一种是通过常规的人工代码检视来发现问题,另外一种是使用PC-Lint等工具软件进行代码静态检查。
常规的人工代码检视在第4章已经讲过,这种方法能发现少量的内存越界和资源泄漏问题,依赖于参与检视的人的技术水平和当时的精神状态。人工检视方法来进行内存越界和泄漏检查效率比较低,成本较高,虽然可能发现一些深层次的Bug,但覆盖率比较低,往往不如PC-Lint之类的软件检查有效。
可能现在还有读者怀疑使用静态检查工具来进行检查的必要性,那我们不妨看如下一段代码:
int main(int argc, char* argv[])
{
int b = 0x12345678;
char a[8];
a[sizeof(a)] = 0xff;
printf("b=0x%x/n", b);
return 0;
}
在给变量a赋值时,发生了数组越界存取,打印出来的b的值变成了0x123456ff,而不是原来的0x12345678(使用MS Visual C++进行编译)。这个错误使用BoundsChecker之类的工具是测试不出来的,不过使用PC-Lint这样的静态检查工具却很容易检查出来。
使用PC-Lint工具检查具有效率高、修改问题的成本低等优点,是一种很有效的方法。尽管PC-Lint能检测出部分栈越界、堆越界、资源泄漏等问题,而且在代码编译完后就可以使用PC-Lint进行检查,但仅仅使用这种方法来进行内存测试是远远不够的。
8.2.2 使用PC-Lint进行强化越界检查
在实际编程中,即便是标准函数也有很多是不够安全的,如C标准库中的许多函数(如strcpy()等)都不够安全。但由于历史原因,现在我们还在大量地使用这些不安全的函数库。特别是一些底层软件,仍然大量存在使用C标准库中不安全函数的情况。另外许多老的代码中都大量使用了C标准库中的不安全函数。那么面对这个问题,我们不禁要问——有没有办法来提高这些不安全函数的安全性呢?
所幸的是,PC-Lint软件中提供了-sem选项来强化函数参数的检查。关于-sem选项的具体使用方法在附录1中有介绍,详细的使用说明可以参阅PC-Lint自带的pc-lint.pdf说明文件。下面就通常实例来讲解如何使用这个选项检测不安全的函数的安全性。
C标准库中的strcpy(char *strDestination, const char *strSource)函数,如果strSource的长度超出了strDestination的空间大小,那么内存越界后程序将发生不可预测的行为,如果strSource指针为NULL也将发生异常。我们在PC-Lint的配置中加上以下选项:
-sem(strcpy, 1P >= 2P, 1p, 2p)
就可以检查strcpy函数,校验它的第1个参数的内存空间是否大于第2个参数的内存空间,并校验第1个参数和第2个参数是否为NULL。
又如memset函数,可以进行以下设置:
-sem(memset, 1P>= 3n, 1p)
这条设置会检查memset函数的第1个参数的空间大小是否大于等于第3个参数值,并校验第1个指针参数是否为空。
再比如itoa(int value, char *string, int radix )函数,当第2个参数的长度小于最大的整数长度时,就会有越界现象,可以用以下设置来进行检查。
-sem(itoa, 2P >= 12)
这条设置会检测第2个参数的内存空间大小是否大于等于12字节,在32位系统中,整数在十进制情况下的最大长度为10位,加上字符结尾符’/0’共11位,十进制下校验第2个参数的空间大于12字节已经够安全了。但是如果转换成二进制的话,整数将变成32位,加上尾字符’/0’就是33位,因此要足够安全的话,需要改成以下形式:
-sem(itoa, ((3n>=10)&&(2P >= 12)) || ((3n<10)&&(2P>32)), 2p)
这个设置的意思是检查当第3个整数参数的值大于等于10时,第2个指针参数的空间大小必须要大于12,当第3个整数参数的值小于10时,校验第2个指针参数的空间大小是否大于32,最后再校验第2个指针参数是否为空。可以测试一下这个设置是否有效,看一下以下代码:
void main(int argc, char* argv[])
{
int x = 100;
char msg[8];
char msg2[32];
(void)itoa(x, msg, 10);
(void)itoa(x, msg2, 2);
}
如果不使用以上设置的话,PC-Lint是没有警告出现的,使用了以上设置后再检查,就发现PC-Lint报出了以下警告信息:
Module: test.cpp
(void)itoa(x, msg, 10);
test.cpp(15): error 426: (Warning -- Call to function 'itoa(int, char *, int)' violates semantic '(((3n>=10)&&(2P>=12))||((3n<10)&&(2P>32)))')
(void)itoa(x, msg2, 2);
test.cpp(16): error 426: (Warning -- Call to function 'itoa(int, char *, int)' violates semantic'(((3n>=10)&&(2P>=12))||((3n<10)&&(2P>32)))')
对于标准库里的函数,即使不进行以上设置,PC-Lint也可能会报出少部分问题,但并不能将问题全部报出,所以要使用标准库函数的话,最好自己写一个检查标准函数的配置以增强其安全性。对于自己写的其他函数,如果对它们进行类似上述设置后,代码的安全性将得到很大提高。
因此,很多类似strcpy()、itoa()等不安全的函数都可以采用这种方式进一步检测其安全性,使错误在静态检查时就可以被发现。
8.2.3 使用PC-Lint强化内存泄漏检查
如果在程序中有自己的资源分配和释放函数——如自己实现了一个内存管理模块,那么对自己的内存分配和释放操作,PC-Lint默认是不会进行检查的。但是PC-Lint提供了-sem选项来进行设置,所以也可以检查自己的内存分配和释放函数。
如MyMalloc()是自己实现的内存分配函数,可以进行以下设置来对这个函数进行检查:
-sem( MyMalloc, @P==malloc(1n));
前面代码静态检查那章已经讲过@P表示函数返回值为指针类型;malloc在-sem选项里是语义表达式的一种,它表示PC-Lint应当设置一个内存分配标志给表达式。因此PC-Lint会像检查malloc()函数一样检查MyMalloc()。
接下来我们试验一下这个选项:如下代码中,先不用-sem选项进行设置,使用PC-Lint检查后可以发现并没有报告内存泄漏方面的信息。再使用上述-sem(MyMalloc, @P==malloc(1n))选项设置后,再用PC-Lint进行检查,就可以发现PC-Lint能够给出内存泄漏报告。
#include <stdlib.h>
void *MyMalloc(size_t size)
{
return (void *)malloc(size);
}
#define MEM_SIZE 128
void main(int argc, char* argv[])
{
char *p = (char *)MyMalloc(MEM_SIZE);
p = (char *)MyMalloc(MEM_SIZE);
}
当然即使使用了-sem选项进行设置,PC-Lint也并不能将错误全部查出,仍然有很多现实编程中存在的错误PC-Lint不能检测。特别是函数参数经过多次传递后,PC-Lint就很难跟踪错误的所在了。所以仅仅采用静态检查方法只能查出部分问题,还有很多问题需要通过其他的方式来检查。
静态检查方法虽然能检查出一些越界存取问题,但栈中是否有溢出,还得靠测试用例进行测试,如越界破坏了某个变量的值后,程序运行必然会有问题,只要设计合适的测试用例就可以测试出来。