本文转载自: 兰新宇:自动检测 Linux 错误的工具 - 静态篇

Overview

怎么检测出代码中存在的 bug?目前主要的手段一是靠测试,研发自测、QA 测试,二是靠 code review。测试如果自动化程度不高,会耗费大量的人力,而 code review 取决于 reviewer 的能力和责任心,也难免存在疏忽。

当代码量越来越大时,通过肉眼来发现错误也变得愈发的困难,这时就需要借助一些工具,自动地帮我们找到代码中潜在的问题。这样的工具可分为两类:在代码的编译阶段分析缺陷的(静态检测),和在代码的运行阶段识别异常的(动态检测)。

市面上 static analysis tools 繁多,这里介绍三种免费的、适用于 Linux 内核的,且笔者亲身使用过的。

Sparse

这是由 Linus Torvalds 本尊于 2003 年开发的(可能是 semantic parser 的意思),是 Linux 内核原生的、默认的代码静态检查工具。

之所以说它是默认的,从它的用法上就可以看出端倪。下载sparse源码后,执行 “make” 命令编译,然后 “make install” 将其安装到系统环境变量可识别的路径(比如 “/usr/bin”),然后在编译内核/驱动时, make 参数加上 “C=2”(或 “C=1”)即可。

C 表示 Checker,打开 Linux 顶层 Makefile 看下,”CHECK” 变量的初始设定就是 “sparse” 。

1
2
make C=1  [targets] Check re-compiled c source with $$CHECK (sparse by default)'
make C=2 [targets] Force check of all c source with $$CHECK'

sparse 侧重于数据类型的检查,其在预编译阶段就埋下了自己的 hook:

1
2
3
4
5
#ifdef __CHECKER__
#define __iomem __attribute__((noderef, address_space(2)))
#else
#define __iomem
#endif

像 “__iomem” 代表的I/O memory,映射后是不适合直接用指针(虚拟地址)读写的,而应该使用和
CPU arch 相适配的专有 API 来访问(比如 readl 和 writel)。如果你违反了这个规则,sparse 就会给出告警。

类似地还有表示用户态地址的 “__user” 关键字,是不适合在内核态直接读写的。

Smatch

Sparse 可以说是简单高效,但其检测范围有限,一个更强大的工具是 smatch。其使用方法同 sparse 类似,也是下载源码后 make,稍有区别的是由于它不是嫡子,需要手动指定下它的安装路径,并且声明 -p(代表 project)为 “kernel”:

1
make CHECK="~/<path-to-smatch>/smatch -p=kernel" C=1

smatch 擅长 flow analysis ,下面以伪代码来展示一个案例:

1
2
3
4
5
# alloc mememory for A

# check whethe A is on list, if not, add it to list

# function return

如果 A 被加入链表,那么 A 的内存会在链表销毁时释放,但是如果没有加入链表,内存就没有释放,smatch 会在函数返回时提示 “possible memory leak of A”。

当然,这属于程序设计的不妥当,正常应该先确定 A 不在链表,再为 A 分配内存。类似的还有异常返回时 mutex 没有 unlock 等,都算是资源泄露。

另一个 smatch 的优势是跟踪变量在生命周期内的值,同样通过伪代码的形式来说明:

1
2
3
4
5
6
7
int A;

if (...)
# add object to link(and assign a non-zero value to A)

if (A)
# remove object from link

第二个 if 的意图是判断 A 非 0,说明之前被加入过链表,那么之后需要从链表移除,但其隐含了一个假设,就是变量 A 会被初始化为 0。但 C 语言里 local variable 是否初始化为 0 是编译器的行为,是不可靠的,smatch 会提示 “uninitialized symbol A”。

此外,如果你用的一个内核 API 有了一个更推荐的版本,smatch 也会给你指出来,比如用更简洁的 kvcalloc 替换 kvmalloc_array。

其中当然不可避免地会有一些 false positive 的 warning/error,但笔者并不愿将其称之为误报。虽然仔细分析报告点的代码逻辑后,发现没有问题,但这样的代码多少是有些别扭的,语义上不是很直接,结构上是存在可以优化的空间的。

文末链接里有 smatch 的开发者 Dan Carpenter 的一个分享,他提到 smatch 还在不断演进,还有不少可以加强的地方,比如 scheduling in atomic 的错误:

1
2
3
4
5
spin_lock(A)
mutex_lock(B)
# do something
mutex_unlock(B)
spin_unlock(A)

要说它的缺点么,大概就是文档比较匮乏吧。这是作者自己说的,笔者在使用过程中也有同感,里面好多 scripts 不会用,白瞎了。

Cppcheck

以上介绍的两种工具都主要面向 Linux 内核,而更为广泛使用的一个工具是 cppcheck,从名字上也可看出来,只要是 C/C++ 程序,它都可以。

不需要手动去找源码来下载,直接 “yum/apt install cppcheck” 就可以安装(从安装方式的差异也能看出,它确实被用的更普遍,进了默认的软件镜像源)。

用法也很简单,直接 cppcheck <file-name>/<folder-name>即可检测单个文件,或目录中所有文件。 默认仅检测 error 级别的,可加上 “–enable=warning” 来使能 warning 级别的(甚至可以是 style 级别)。

cppchek 算是对 compiler 的一种补充吧, 来看几个例子:

1
2
3
pdev = get_dev_by_id(...);
if (!pdev)
dev_err(pdev->dev, ...);

指针判空之后,为了打印信息的需要(表示哪个 dev 出了问题),无意中造成了 null pointer deference。这种异常分支的错误靠测试很难发现(corner case),因为平时走不进去,但一旦进去了,就是系统 crash 的大雷啊。

再比如下面这个,如果 “pdev->dev” 为空,那么 if 条件判断还会继续,那 “pdev->dev->type” 不又是妥妥的「空指针引用」嘛。

1
if (!pdev->dev && !pdev->dev->type)

其他像内存泄露的一些问题,也能被 cppcheck 检查出来。

小结

静态检查工具可以在编译阶段就发现很多潜在的隐患,是保障代码质量的一种低成本方案。比如 memory leak 吧,目前 Linux 内核也就能用 kmemleak 来检查,但往往需要重新编译内核不说,没执行到的异常分支造成的泄露还检测不出来。所以,目前有些公司要求代码提交必须通过 cppcheck 检测,不失为一种值得效仿的业界最佳实践( best practice)。

这三种静态工具在检测范围上有一些重叠的地方,但也各有各的特色。如果条件允许的话,建议都用上,反正使用成本都比较低,如果非只能选一种的话,那笔者会选择效果最为拔群的 smatch。


参考资料:

  1. Sparse: a look under the hood
  2. Finding kernel problems automatically
  3. sparse(1) - Linux manual page
  4. Smatch: New ideas for Static Analysis
  5. Smatch: pluggable static analysis for C