Visual Studio /analyze的好处

Visual C++编译器的分析功能是提高代码质量的一个好方法。它基本上是一个21世纪的“lint”来识别许多编码错误。许多错误对于程序员来说是很难看到的,但是,通过对代码进行适当的注释,/analyze的不屈不挠的眼睛将可靠地找到它们。
我最近在一个大型代码库上使用/analyze,发现了大约500个值得修复的bug。这些错误包括超出范围的读写、printf参数错误、逻辑错误等等。这些错误有可能导致内存损坏、崩溃和所有类型的意外行为。在我进行代码清理时,有几个崩溃bug咬了我的同事,因此证明了这些bug不仅仅是理论上的问题。
列出/analyze发现的所有bug,创建起来很乏味,阅读起来也很无聊。相反,我创建了示例代码来演示/analyze在哪些方面做得最好。分析工作做得很糟糕的地方会被保存到以后的文章中。

缓冲区溢出

当调用需要传递指针和缓冲区大小的函数时,很容易传递错误的大小。我看到的一些错误包括传递错误的硬编码常量、传递错误的命名常量或传递错误的缓冲区大小。在本例中,当需要_countof()时使用sizeof(),这意味着该计数是应该的两倍,并且可能存在缓冲区溢出:

void TestBufferWarnings(const wchar_t* pSource)
{
    wchar_t buffer[10];
    wcscpy_s(buffer, sizeof(buffer), pSource);
}
/analyze发出的警告包括:

warning C6057: Buffer overrun due to number of characters/number of bytes mismatch in call to ‘wcscpy_s’
warning C6386: Buffer overrun: accessing ‘argument 1′, the writable size is ’20’ bytes, but ’40’ bytes might be written

缓冲区溢出可能导致难以诊断的崩溃,并使您的软件容易受到黑客的攻击。/analyze特性可以准确地诊断这种潜在的溢出,因为wcscpy_s函数和许多其他C运行时函数一样,都用表示契约的特殊宏进行了注释。wcscpy_s注释的简化版本如下所示:

wcscpy_s(_Out_z_cap_(cc) wchar_t* pD, size_t cc, const wchar_t* pS);

相关部分为“_Out_z_ucap_u(cc)wchar_t*pD”。这表示pD指向一个输出缓冲区,该缓冲区将填充以空结尾的字符串,其字符大小由“cc”参数指定。除了允许/分析诊断此大小不匹配之外,注释还可以帮助进行其他类型的分析,通过让/analyze假设调用wcscpy_后,缓冲区将填充以空结尾的字符串。注释在实现wcscpy_s时也有帮助,因为/analyze知道它应该验证wcscpy_usalways null终止输出缓冲区。
另一个令人惊讶的常见的缓冲区溢出是当代码使用硬编码索引写入数组的更多元素时。这似乎很荒谬,但在任何大型代码库中,这些错误确实会蔓延进来:

int ReturnFromArray()
{
    int array[2] = {};
    return array[2];
}

以下是/analyze为上述代码生成的警告:

warning C6201: Index ‘2’ is out of valid index range ‘0’ to ‘1’ for possibly stack allocated buffer ‘array’
warning C6385: Invalid data: accessing ‘array’, the readable size is ‘8’ bytes, but ’12’ bytes might be read: Lines: 33, 34

格式字符串不匹配

编写printf格式字符串和传递的参数不匹配的代码非常容易。其中一些不匹配表示可移植性问题(依赖字节顺序或“int”与指针的大小),而其他则表示未定义的行为或有保证的崩溃。下面的函数演示了我发现的许多不匹配:

void TestPrintfWarnings()
    {
        int x = 0;
        float f = 1.0f;
        void* p = 0;
        __int64 i64 = 0;
        std::string s = “Hello”;

        printf(“%f, %d, %08x, %d, %p\n”, x, f, p, i64, s);
    }

以下是/analyze为上述代码生成的警告:

warning C6272: Non-float passed as argument ‘2’ when float is required in call to ‘printf’
warning C6273: Non-integer passed as parameter ‘3’ when integer is required in call to ‘printf’: if a pointer value is being passed, %p should be used
warning C6273: Non-integer passed as parameter ‘4’ when integer is required in call to ‘printf’: if a pointer value is being passed, %p should be used
warning C6328: ‘__int64’ passed as parameter ‘5’ when ‘int’ is required in call to ‘printf’
warning C6066: Non-pointer passed as parameter ‘6’ when pointer is required in call to ‘printf’

前两个警告(6272和6273)是明确的-不要错配float和int参数。

第三个警告(6273)-使用%08x打印指针-不太清楚。%08x很适合打印指针,直到您将代码移植到64位,此时指针将无法正确打印,并且以下参数也可能会偏移,因此是错误的。

第四个警告相当明显-不要使用%d打印64位整数-但这是一个非常容易犯的错误。由于x86处理器上的字节排序,只要64位整数足够小,并且没有其他printf参数跟随,这种打印64位整数的方法实际上是可行的。如果随后出现其他参数,则根据调用约定,它们可能会偏移四个字节,从而导致不正确的输出或崩溃。打印64位整数的最佳便携方法是使用%lld。%I64d不能与gcc一起工作。

第五条警告可能要清楚得多,但它确实准确地描述了问题所在。打印带有“%s”的字符串时,应该传递一个char*。传递一个字符串对象是一个非常糟糕的主意-尽管在我清理的代码库中非常常见。我们使用的string类是16个字节,但是当传递给printf样式的函数时它是有效的,因为string对象的第一个成员是指向字符串的char*。但是,如果后面有任何其他参数,它们将偏移12个字节,这很容易引起问题。

/analyze之所以能够找到这些错误,是因为它知道printf使用标准printf样式的格式字符串。像往常一样,它知道这一点,因为printf函数被注释了:

printf(_In_z_ _Printf_format_string_ const char * _Format, …);

/analyze发现的其他警告包括缺少参数和额外参数。

逻辑错误

/analyze可以找到许多其他的错误。作为一个例子,考虑一下这个简单的函数:

bool TestConditionalWarnings(int val)
{
    return val & kFlag1 || val & kFlag3 || kFlag4;
}
其目的是查看是否设置了任何标志位,但检查kFlag4时出现错误意味着该表达式将始终为true,这/analyze表明:

warning C6236: (<expression> || <non-zero constant>) is always a non-zero constant

注释(Annotations)的重要性

大多数/analyze警告(当然,大多数关于严重问题的准确警告)都需要在函数声明上添加注释。如果你真的想在你的代码中发现错误,那么你需要注释所有接受printf风格参数的函数,以及所有接受buffer和size参数的函数。这些注释很容易实现,它们迫使您思考这些函数的实际契约是什么,并且它们可以显著提高代码的安全性和可靠性。

 

posted on 2020-07-31 10:27  活着的虫子  阅读(716)  评论(0编辑  收藏  举报

导航