跳到主要内容

第 5 章 窥视深渊 —— 内存破坏与未定义行为的动态分析

有一类 Bug,比逻辑错误更让人头疼,也更隐蔽。

逻辑错误通常会让程序立刻停下来,或者至少表现出某种「坏掉」的症状——算错了、卡死了、吐出奇怪的数据。但内存破坏不是。内存破坏就像是在你的公寓墙壁里悄悄滋生的一种霉菌。当你发现墙面发黑时,腐蚀已经深入结构了;当你看到内核 panic 时,数据损坏可能已经在那个指针返回后的几百行代码里蔓延开了。

更糟糕的是,在 C 语言这种贴近底层的语言里,我们手握着手术刀,但没有安全网。use-after-freebuffer overflowstack overflow……这些术语听起来像是一个老练的工程师才能解决的问题,但真相是:无论你写了多少年代码,只要是人写的东西,只要是指针,就会滑。

传统的调试方法——看日志、加 print、甚至用 gdb 单步跟踪——在内存错误面前往往是低效的。因为当你在 0xffff8880010 处停下来观察时,真正的罪行可能发生在十分钟前的 0xffff8880008。你正在看的是犯罪现场,而不是监控录像。

我们需要的是一种机制,一种能在动作发生的那一刻就吹哨子的机制。不是等程序崩了再验尸,而是在手指触碰那块不该触碰的内存的瞬间,抓住它。

这正是本章的任务:构建这套动态监控体系。我们会引入 Linux 内核里的三套「重火力武器」——KASAN、UBSAN 和 KFENCE。它们有的像探照灯,把整个内存空间照得通亮,任何越界操作都无处遁形(但耗电巨大);有的像声纳,偶尔扫描一下,平时几乎感觉不到存在(适合潜伏在生产环境)。

为了用好这些工具,我们不能只做一个只会敲配置选项的操作员。我们需要理解:影子内存是如何映射物理内存的?编译时插桩到底在代码里塞了什么?为什么某些 Bug 只有 Clang 能抓到,而 GCC 放过了?

这些问题,就是本章的核心。


5.1 准备环境与武器库

在深入机制之前,先把手边的家伙什儿备齐。好消息是,我们的「硬件环境」——也就是你那台用于编译和运行内核的开发机——配置依然和第 1 章里描述的一模一样。你不需要买新的 ARM 板子,也不需要什么特殊的模拟器。

所有的示例代码——包括我们将要故意写出的「带 Bug 驱动」——都已经躺在书里的 GitHub 仓库里了:

🔗 https://github.com/PacktPublishing/Linux-Kernel-Debugging

你可以直接把它 clone 下来,作为我们演练的靶场。

⚠️ 注意 别在生产环境或者你正在做重要项目的机器上直接跑这些测试用例。 我们会故意触发内核 panic,会故意制造内存泄漏。 请在虚拟机或者专门的测试板上搞。

在这个环节里,唯一的新面孔是 Clang 编译器

在第 1 章里,我们可能默认你还在用老牌的 GCC。但在内存检测这个领域,Clang(以及它背后的 LLVM 基础设施)不仅仅是 GCC 的替代品,它在某些场景下是必须的。后面讲到某些特定的越界检测(比如全局变量的「左越界」)时,你会发现 Clang 的 diagnostics 能力比 GCC 要激进得多。

别担心,如果你还没装它,我们会在 「Building your kernel and modules with Clang」 那一节专门讲怎么把它整合进构建流程。

环境确认无误,编译器也备好了。接下来,我们就要开始摆弄这些能让内核「暴露真容」的配置项了。