5.2 内存到底出了什么问题?
上一节我们准备好了一个用来「搞破坏」的内核源码树,甚至把编译器都换成了更锋利的 Clang。这一切准备工作都是为了对付那个在 C 语言世界里最古老、最狡猾,也最致命的敌人:内存问题。
这一节,我们先把代码搁一边,重新审视一下我们要打的怪兽。如果你觉得这一节有点像在复习计算机基础,请耐心一点——只有知道怪兽长什么样,你才能在它出现时认出它。
令人不安的真相
C 语言就像一把没有安全护罩的链锯。它极其强大,能让你精确地操纵每一个比特——但这也意味着,只要你手滑一下,它就会毫不留情地切断你的腿。
在第 2 章里,我们其实已经略带调侃地提到过这个话题。但现在,既然我们要深入内核调试的腹地,就不能再开玩笑了。内存错误不是「也许会发生」,而是「必然会发生在你写代码的某个瞬间」。
为了方便你的记忆,我把我们在第 2 章列出的那些内存Bug清单再搬出来。这不仅仅是列表,这是你的敌人通缉令:
-
不正确的内存访问:
- 使用未初始化的变量:也就是 Uninitialized Memory Read (UMR)。你以为变量里有值,其实读进去的是一堆随机垃圾。
- 越界访问:读写过头了。不管是向左(下溢)还是向右(上溢),只要你跨过了分配给你的那块内存边界,你就进入了未定义领域。
- 释放后重用:内存都还给系统了,你还试图去摸它。这叫 Use-After-Free (UAF)。
- 返回后重用:函数栈都退出了,你还留着指向局部变量的指针。这叫 Use-After-Return (UAR)。
- Double-free:把同一块内存释放了两次。这通常会让内存管理器直接崩溃。
-
内存泄漏:只借不还。系统运行久了,内存像漏水桶一样流干。
-
数据竞争:两个线程毫无默契地同时修改同一块数据,结果全看运气。
-
(内部)碎片化:虽然不是显式的 Bug,但会让你的内存利用率低得可怜。
除了最后一个碎片化问题,前面每一个都属于 C 语言标准里的未定义行为。在用户空间,这可能导致程序悄悄崩溃或数据损坏;但在内核空间,这通常意味着系统崩溃,或者更糟——安全漏洞。
这不仅仅是 Bug,这是漏洞
这一点怎么强调都不为过:安全漏洞,本质上就是被利用的 Bug。
想象一下,一个黑客精心构造了一个特定的数据包,利用内核驱动里的一个「越界写」漏洞,改写了页表。这时候,这就不再是一个简单的 Segfault,而是一次特权提升。
这也是为什么我们需要 KASAN、UBSAN 这种重型武器——它们不仅仅是 Debug 工具,它们是安全审计的第一道防线。
除非你运行的是开启了所有安全补丁的最新稳定内核,否则你就是在裸奔。GitHub 上有一个叫 linux-kernel-exploitation 的仓库,里面陈列了大量针对老内核的攻击手段。你可以去看看,那不仅是黑魔法,那是你如果不写安全代码就会面临的现实。
工具箱预览:谁在干活?
好了,恐惧营销到此为止。问题是真实的,但解决方案也是现成的。
为了捕捉上面那些幽灵,内核社区积攒了一整套工具箱。我们可以把它们分为两大类:动态分析(运行时抓现行)和静态分析(代码里找隐患)。
本书的主角是第一类:动态分析工具。这一章和下一章,我们要深入的就是下面这四张王牌:
- KASAN (Kernel Address Sanitizer):重炮手。通过编译时插桩,配合影子内存,能捕捉绝大多数内存访问错误。它是这一章的主角。
- UBSAN (Undefined Behavior Sanitizer):语言律师。专门抓 C 语言里的未定义行为,比如整数溢出、错位的指针。
- SLUB debug:专门针对 slab 分配器的调试机制(下一章讲)。
- kmemleak:找内存泄漏的探测器(下一章讲)。
至于静态分析(如 Sparse、Smatch)和事后分析(如 crash、kdump),那是另一条战线的故事。
为了让你心里有个谱,我整理了一张表。这张表告诉你,面对某种特定的 Bug,你应该找谁帮忙。
(Table 5.1 – A summary of tools...)
| 你可能遇到的 Bug | 谁能抓到它? | 备注 |
|---|---|---|
| Uninitialized Memory Read (UMR) | GCC/Clang 编译器警告, KASAN | 现代编译器对局部变量未初始化的警告已经很准了。KASAN 也能抓,但编译器警告是第一道防线。 |
| Out-of-Bounds (OOB) memory accesses | KASAN | 这是 KASAN 的主场。 无论是堆上还是栈上的越界,KASAN 几乎百发百中。 |
| Use-After-Free (UAF) | KASAN | 同样是 KASAN 的强项。它能在你触碰释放内存的瞬间截停系统。 |
| Use-After-Return (UAR) | KASAN | 需要特定的 KASAN 配置选项才能开启,比较难抓,但能抓到。 |
| Double-free | KASAN | KASAN 会极其敏锐地检测到重复释放。 |
| Memory leakage | kmemleak | KASAN 不管这个,这是 kmemleak 的专职工作(下一章见)。 |
| Data races | KCSAN (Kernel Concurrency SANitizer) | 表中没列,但这是专门查并发问题的工具,第 8 章会聊到锁的时候再展开。 |
表注说明:
- [1]:现在的 GCC/Clang 足够聪明,如果你开启了
-Wuninitialized,甚至开启自动初始化选项,编译器就能帮大忙。 - [2]:KASAN 几乎能抓到表中列出的所有动态内存错误——这真的是个神奇的工具。
- [3]:所谓的「Vanilla kernel」(原味内核),指的是没有开启任何调试选项的发行版内核。在这种状态下,很多错误只会悄无声息地发生,或者很久以后才引爆。
这一节我们建立了目标清单。在接下来的几节里,我们将深入 KASAN 和 UBSAN 的内部机制。你会发现,虽然配置它们需要点耐心,但一旦你看到它精准地报出第一行 Bug 日志时,你会发现一切都值了。