【经验分享】关于静态分析工具排查 Bug 的方法

发布于:2024-07-17 ⋅ 阅读:(66) ⋅ 点赞:(0)

程序员的日常工作,不是摸鱼扯皮,就是在写 Bug。虽然这是一个梗,但也可以看出,程序员的日常一定绕不开 Bug。而花更少的时间修复软件中的 Bug,且不引入新的 Bug,最好的办法就是使用静态分析工具排查 Bug。

所谓的静态分析,顾名思义,是指在不运行程序的情况下对源代码进行检查,通过算法和模式识别来检测可能存在的错误、漏洞或不符合编码规范的地方。这种方法不仅可以帮助开发者在早期阶段发现 Bug,还能提高代码质量和安全性,减少后期调试的时间和成本。

但是在讨论静态分析工具之前,你知道编译器也进行静态代码分析吗?

编译器的静态分析

编译器的作用,就是生成可执行文件或二进制文件,因此,在默认情况下,编译器并不用于静态代码分析。但随着技术的发展和版本的迭代更新,编译在静态代码分析方面不断改进并变得更好,不同版本的编译器在编译同一个代码时,出现警告数量可能都不一样。

对很多初学者来说,编译器生成的警告非常烦人,并且大多数消息并不关键或不会对应用程序造成任何问题。其实我们应该信任编译器,因为有一件事是肯定的:没有人比编译器本身更了解语言、语法和语义。

那么编译器要怎么进行静态分析呢?

以下面的代码为例,我用不同的编译器进行编译并运行,

#include <stdio.h>

#define ON  0xFF
#define OFF 0x00

void print_message(char status)
{
	if (status == ON)
		printf("ON\n");
	else
		printf("OFF\n");
}

int main()
{
	print_message(ON);

	return 0;
}

这段代码看起来没有任何问题,而且看起来运行后会打印 ON,而实际上却打印 OFF

以 GCC 编译编译运行,结果如下:

在这里插入图片描述

如果源代码是用 Clang 编译器编译,会立刻给我们一个警告:

在这里插入图片描述

[!WARNING]

这是警告的具体意思,是一个整型常数 255 和一个 char 类型的表达式真正进行比较,代码可能存在逻辑上的问题。

可见 Clang 不仅是一个编译器,也是一个非常不错的静态分析工具。当然,这类警告 GCC 也可以发现,不过需要在编译时,加上两个参数 -Wall-Wpedantic。如下图:

在这里插入图片描述

[!NOTE]

-Wall-Wpedantic 的作用是什么?

  1. -Wall 参数告诉 GCC 启用所有非默认的警告选项。这意味着 GCC 将会报告许多常见的编程错误,包括但不限于未使用的变量、可疑的类型转换、可能的除零错误、条件表达式中的常量等。-Wall 可以帮助开发者找到那些可能在编译时没有错误但在运行时会导致问题的代码缺陷。

    例如,以下是一些由 -Wall 开启的警告:

    • uninitialized: 使用未初始化的变量;
    • unused: 定义了但未使用的变量或函数;
    • conversion: 类型转换可能导致数据丢失;
    • pointer-sign: 指针比较中正负符号的差异;
    • format-security: 格式字符串的安全问题;
    • overflow: 数值运算溢出的风险。
  2. -Wpedantic 参数使 GCC 更加严格地遵守标准,它将警告所有不符合 C 或 C++ 标准的代码。这意味着任何与标准不完全一致的语法或行为都会被标记出来,即使这种行为在 GCC 中可能是默认允许的。

    -Wpedantic 主要关注的是标准一致性问题,比如:

    • 使用扩展的语法,如 GNU 特有的语法;

    • 不符合标准的声明或初始化;

    • 未在标准中明确规定的编译器行为;

    • 对于 C++,它还会警告关于 C++11 或更高版本标准中不再推荐的用法;

值得注意的是, -Wpedantic 可能会警告以下情况:

  • 使用 C99 的特性(如复合字面量)在 C89 模式下编译。
  • 在 C++ 中使用 C 风格的字符串或旧的初始化语法。
  • 使用了在标准中没有定义的行为,比如某些类型的隐式转换。

通常,为了最大程度地提高代码质量,开发者会同时使用 -Wall-Wpedantic。这有助于确保代码不仅避免了常见的编程错误,而且也遵循了语言标准,从而提高了代码的可读性和可移植性。

然而,值得注意的是,-Wpedantic 可能会产生大量警告,特别是在处理旧代码或使用了某些非标准但实用的 GNU 扩展时。因此,在实际应用中,开发者可能需要根据具体情况调整警告级别,以避免被过多的警告信息淹没。

由此可见,作为开发者,我们应该注意所有编译器警告。甚至我们需要能够生成更多警告的工具,那种可以分析源代码并发现语言的奇怪结构、潜在问题和不寻常用法的工具。

Clang 和 GCC 在静态分析方面还是比较出色的,但这不是它们的主要作用,这些警告是在编译过程中完成的简单检查的副产品而言。此外,静态代码分析需要大量时间,并且通常不需要每次编译。编译时间对于编译器来说非常重要。在检查代码质量和编译时间之间需要权衡。

正是如此,也促使了静态代码分析工具的产生,这类工具也称为 lint,其中一个开源的静态分析工具 cppcheck,也是本文的主角。

cppcheck

Cppcheck 是 C/C++ 代码的静态分析工具,它提供独特的代码分析来检测错误,并专注于检测未定义的行为危险的编码结构,其中包括以下几个方面的内容:

  • 野指针或悬空指针
  • 除以零
  • 整数溢出
  • 无效的位移操作数
  • 无效转换
  • STL 的无效使用
  • 内存管理
  • 空指针取消引用
  • 越界检查
  • 未初始化的变量
  • 写入常量数据
  • 未使用或重复的代码

Cppcheck 是一个开源项目,目前托管在 SourceforgeGitHub 上,支持 GNU/Linux、Windows 和 Mac OS 操作系统。

安装 cppcheck

安装 cppcheck 的步骤可在 Cppcheck 官网可以找到。

也可以从 Linux 发行版包管理器获取 cppcheck,以 Ubuntu 系统为例,可以运行以下命令来安装 cppcheck:

sudo apt update
sudo apt install cppcheck

如果要使用最新的 cppcheck 版本,则需要下载源代码,编译并安装,过程如下:

wget https://github.com/danmar/cppcheck/archive/1.90.tar.gz
tar xfv 1.90.tar.gz
cd cppcheck-1.90/
make MATCHCOMPILER=yes FILESDIR=/usr/share/cppcheck HAVE_RULES=yes -j4
sudo make MATCHCOMPILER=yes FILESDIR=/usr/share/cppcheck HAVE_RULES=yes install

运行 cppcheck

以下面这段代码为例,代码作用为计算整数数组的所有元素的总和:

#include <stdio.h>

int buf[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

int calc(void)
{
	int result;
	int i;

	for (i = 0; i <= 10; i++)
		result += buf[i];

	return result;
}

int main()
{
	calc();
	return 0;
}

这段代码其实有两处风险,但是 GCC 编译器(版本号为 11.4.0)无法找到这些风险:

在这里插入图片描述

即使加上了 -Wextra-Werror 两个参数,也找不到任何风险:

在这里插入图片描述

[!NOTE]

-Wextra-Werror 的作用是什么?

  1. -Wextra 标志开启了一系列额外的警告,这些警告超出了默认警告的范畴。该参数的目的是捕捉那些可能不易察觉的编程错误,帮助开发者避免未来可能遇到的运行时错误和维护问题。它包含了多个单独的警告标志,涵盖了可能被忽略或不常见的代码问题,包括但不限于以下警告:

    • 使用未初始化的变量;
    • 形参与实参的类型不匹配;
    • 赋值运算符可能被误解为等于运算符;
    • 可能的枚举值溢出;
    • 函数调用的返回值被忽视;
    • 以及其他潜在的代码错误和不良实践。
  2. -Werror 标志的作用是将所有警告视为错误。换句话说,当编译器遇到任何被 -W 标志开启的警告时,它将停止编译过程,就像遇到了一个真正的编译错误一样。这迫使开发者在编译通过之前必须解决所有的警告。

    -Werror 的主要优点是强制执行代码质量标准,确保没有潜在的编程问题遗留到最终的二进制文件中。这在团队开发环境中特别有用,可以确保所有代码都达到一致的高标准。

现在换 Clang 编译器进行编译,同样也无法找到这些风险:

在这里插入图片描述

Clang 编译器也可以使用与 GCC 相同的参数,如下命令:

clang -Wall -Wextra -Werror -Wpedantic main.c -o main

执行命令后,可以找到两个错误:

在这里插入图片描述

但是这两个错误其实是重复的,引起两个错误的原因都是变量 result 未初始化,如果给 result 初始化后,就不会出现这个错误,如下图:

在这里插入图片描述

把程序改回原来的代码,用 cppcheck 检查,结果可以找到这两个错误,如下图:

在这里插入图片描述

除了 Clang 找出来的 result 未初始化之外,cppcheck 还在 for 循环中找到了数组越界。

[!CAUTION]

calc 函数的 for 循环中,使用了 i <= 10 作为循环终止条件,然而数组 buf 的下标是从 0 到 9(共10个元素)。当程序尝试访问 buf[10] 时,这实际上是越界访问,因为 buf[10] 并不存在(数组最后一个元素是 buf[9])。

由此可见,编译器并不能完全充当静态分析工具,逻辑上的错误还是要靠专业的静态分析工具。