内核测试指南¶
有许多不同的工具用于测试 Linux 内核,因此知道何时使用每种工具可能是一个挑战。本文档简要概述了它们的区别以及它们是如何协同工作的。
编写和运行测试¶
绝大多数内核测试都是使用 kselftest 或 KUnit 框架编写的。这两个框架都提供了基础设施,以简化测试及测试组的运行,并提供了辅助函数来帮助编写新的测试。
如果您希望验证内核的行为——特别是内核的某些特定部分——那么您应该使用 KUnit 或 kselftest。
KUnit 与 kselftest 的区别¶
KUnit (KUnit - Linux 内核单元测试) 是一个完全在内核内部的“白盒”测试系统:由于测试代码是内核的一部分,因此它可以访问未公开给用户空间的内部结构和函数。
因此,KUnit 测试最好针对内核中小型、自包含且可独立测试的部分来编写。这非常符合“单元”(unit)测试的概念。
例如,一个 KUnit 测试可能会测试单个内核函数(甚至是通过某个函数的单个代码路径,例如错误处理分支),而不是整个功能特性。
这也使得 KUnit 测试的构建和运行速度非常快,从而允许在开发过程中频繁地运行它们。
有一个 KUnit 测试风格指南,它可以在 测试风格与命名法 中提供进一步的指导
另一方面,kselftest (Linux 内核自测试) 主要在用户空间实现,其测试是常规的用户空间脚本或程序。
这使得编写更复杂的测试,或者需要更多地操纵整体系统状态的测试(例如,孵化进程等)变得更容易。然而,无法直接从 kselftest 调用内核函数。这意味着只有通过某种方式暴露给用户空间的内核功能(例如通过系统调用、设备、文件系统等)才能使用 kselftest 进行测试。为了解决这个问题,一些测试包含了一个配套的内核模块,用于暴露出更多的信息或功能。不过,如果测试主要或完全在内核内部运行,KUnit 可能是更合适的工具。
因此,kselftest 非常适合测试完整的功能特性,因为这些特性会向用户空间公开可测试的接口,但不会暴露实现细节。这与“系统”或“端到端”(end-to-end)测试非常契合。
例如,所有新的系统调用都应该配有对应的 kselftest 测试。
代码覆盖率工具¶
Linux 内核支持两种不同的代码覆盖率测量工具。这些工具可用于验证测试是否在执行特定的函数或代码行。这对于确定内核被测试覆盖的程度,以及查找未被相应测试覆盖的边界情况(corner-case)非常有用。
将 gcov 与 Linux 内核配合使用 是 GCC 的覆盖率测试工具,可与内核配合使用以获取全局或每个模块的覆盖率。与 KCOV 不同,它不记录按任务划分的覆盖率。覆盖率数据可以从 debugfs 中读取,并使用常规的 gcov 工具进行解析。
KCOV:用于模糊测试的代码覆盖率 是一项可以构建到内核中的功能,允许在按任务级别捕获覆盖率。因此,它对于模糊测试(fuzzing)以及其他需要了解在单个系统调用等期间执行的代码信息的场景非常有用。
动态分析工具¶
内核还支持许多动态分析工具,它们试图在运行中的内核中检测出各类发生的问题。这些工具通常各自专注于查找不同类别的 Bug,例如无效内存访问、诸如数据竞争(data races)之类的并发问题,或者整数溢出等其他未定义行为。
其中部分工具列举如下:
kmemleak 检测可能的内存泄漏。参见 内核内存泄漏检测器
KASAN 检测无效的内存访问,例如越界(out-of-bounds)和释放后使用(use-after-free)错误。参见 内核地址消毒剂 (KASAN)
UBSAN 检测 C 标准未定义的行为,如整数溢出。参见 未定义行为消毒剂 - UBSAN
KCSAN 检测数据竞争。参见 内核并发消毒剂 (KCSAN)
KFENCE 是一个低开销的内存问题检测器,它比 KASAN 快得多,并且可以在生产环境中使用。参见 内核围栏 (KFENCE)
lockdep 是一个锁正确性验证器。参见 运行时锁正确性验证器
运行时验证(RV)支持检查给定子系统的特定行为。参见 运行时验证
内核中还有其他一些调试插桩(debug instrumentation),其中大部分可以在 lib/Kconfig.debug 中找到
这些工具倾向于将内核作为一个整体进行测试,并且不像 kselftest 或 KUnit 测试那样有“通过”与否的概念。通过在启用了这些工具的内核上运行测试,它们可以与 KUnit 或 kselftest 结合使用:这样你就可以确信在测试过程中没有发生这些错误。
其中一些工具与 KUnit 或 kselftest 进行了集成,如果检测到问题,将自动使测试失败。
静态分析工具¶
除了测试运行中的内核之外,还可以使用静态分析工具直接分析内核源代码(在编译时)。内核中常用的这些工具允许检查整个源代码树或其中的特定文件。它们使得在开发过程中检测和修复问题变得更加容易。
Sparse 可以通过执行类型检查、锁检查、值范围检查,以及在检查代码时报告各种错误和警告来帮助测试内核。有关如何使用它的详细信息,请参阅 Sparse 文档页面。
Smatch 扩展了 Sparse,并针对编程逻辑错误提供了额外的检查,例如 switch 语句中缺少 break、错误检查时未使用的返回值、在错误处理路径的返回值中忘记设置错误码等。Smatch 还针对更严重的问题(如整数溢出、空指针解引用和内存泄漏)进行了测试。请参见项目主页 http://smatch.sourceforge.net/。
Coccinelle 是我们可用的另一个静态分析器。Coccinelle 通常用于协助源代码的重构和附带演进(collateral evolution),但它也可以帮助避免在常见代码模式中出现的某些 Bug。可用的检查类型包括 API 测试、内核迭代器正确用法测试、free 操作健全性(soundness)检查、锁行为分析,以及有助于保持一致的内核用法规范的其他测试。详细信息请参见 Coccinelle 文档页面。
不过需要注意的是,静态分析工具存在误报(false positives)的问题。在试图修复错误和警告之前,必须对它们进行仔细评估。
何时使用 Sparse 和 Smatch¶
Sparse 进行类型检查,例如验证带有注解的变量不会引发字节序 Bug,检测不当使用 __user 指针的地方,以及分析符号初始化程序的兼容性。
Smatch 进行流分析(flow analysis),如果允许构建函数数据库,它还会进行跨函数分析。Smatch 试图回答诸如“这个缓冲区是在哪里分配的?”、“它有多大?”、“这个索引能被用户控制吗?”、“这个变量比那个变量大吗?”之类的问题。
通常,在 Smatch 中编写检查要比在 Sparse 中编写检查更容易。尽管如此,Sparse 和 Smatch 的检查内容之间仍有一些重叠。
Smatch 和 Coccinelle 的优势¶
对于编写检查来说,Coccinelle 可能是最简单的工具。它在预处理器之前运行,因此使用 Coccinelle 可以更容易地在宏中查找 Bug。此外,Coccinelle 还能为你生成补丁,这是其他任何工具都做不到的。
例如,使用 Coccinelle,你可以将 kmalloc(x * size, GFP_KERNEL) 批量转换为 kmalloc_array(x, size, GFP_KERNEL),这非常有用。如果你只是抛出一个 Smatch 警告,并试图把转换的工作推给维护人员,他们会感到恼火。你将不得不为每一个警告去争论它是否真的会发生溢出。
Coccinelle 不对变量的值进行分析,而这正是 Smatch 的强项。另一方面,Coccinelle 允许你以简单的方式去做简单的事情。