跳到主要内容

6.2 精准打击:使用 slub_debug 参数埋雷

上一节我们讲,SLUB 调试机制这把枪已经架好了(CONFIG_SLUB_DEBUG),但默认情况下保险没开。

现在,是时候教你怎么打开保险,并往这片内存雷区里扔几颗手雷了。

我们要用到的核心工具,是内核命令行参数 slub_debug。它允许你在不重新编译内核、不动一行代码的情况下,精细地控制调试逻辑的开启与关闭。这就像你在飞机上瞄准了一片区域(slab 缓存),然后决定是扔传单(仅日志)还是扔炸弹(强制崩溃)。

但在扔炸弹之前,我们需要先看一眼炸弹的引信

6.2.1 拆解 slub_debug:你的军火清单

slub_debug 参数的语法非常直观,但选项很多。你可以把它想象成是一排拨动开关,每个开关控制一种检测机制。

先来看看官方给定的这套「军火清单」:

[插入原文 Table 6.1 的内容翻译或保留]

  • F (Sanity checks): 基本健全性检查。在分配和释放时检查元数据是否损坏。
  • Z (Red zoning): 红区。在对象前后填充特定区域,抓越界。
  • P (Poisoning): 投毒。填充特定魔数,抓未初始化内存访问(UMR)或释放后重用(UAF)。
  • U (User tracking): 用户跟踪。记录是谁分配/释放了这个对象(保存栈信息)。
  • T (Trace): 跟踪分配/释放的轨迹。
  • A (Failure tracking): 失败统计,记录哪些地方分配失败了。

这些开关不仅可以单独使用,还可以组合使用。除此之外,还有一个非常有用的 sysfs 接口文档(虽然看起来有点年头了):Documentation/ABI/testing/sysfs-kernel-slab

但在深入实战之前,我们得先理解一个最核心的概念——Poisoning(投毒)。因为这是大多数诡异内存错误的「照妖镜」。

6.2.2 原理深挖:那些奇怪的十六进制数

SLUB 调试机制怎么知道你动过哪块内存?靠的是「染色」。

内核在 include/linux/poison.h 里定义了几种特殊的颜料。我们可以把内存看作一块画布,内核在这些画布的特定位置涂上特定的颜色。如果你修改了这些颜色,内核就知道是你干的。

来看看这些颜色(魔数)的配方:

// include/linux/poison.h
#define POISON_INUSE 0x5a /* for use-uninitialised poisoning (ASCII 'Z') */
#define POISON_FREE 0x6b /* for use-after-free poisoning (ASCII 'k') */
#define POISON_END 0xa5 /* end-byte of poisoning */

这三个宏定义就是整个投毒机制的基石。让我们来逐一拆解:

  1. POISON_FREE (0x6b):这是最常用的「毒药」。 当你开启 slub_debug=P 或者创建缓存时带上 SLAB_POISON 标志,内核会在对象被释放后,或者在刚分配出来还没写入数据前,把内存填充为 0x6b(对应 ASCII 字符 k)。

    • 含义:这块内存现在是「死的」。如果你读取它,应该看到满屏的 0x6b;如果你看到了别的值,说明要么你还没初始化就读了(UMR),要么你释放后又写入了(UAF)。
  2. POISON_INUSE (0x5a): 这个值主要用于填充红区内部,或者某些特殊的状态标记。如果你在红区里看到了 0x5a,说明这里是安全的边界;如果它变了,说明你把边界炸穿了。

  3. POISON_END (0xa5): 这是个哨兵。每个 slab 对象的最后一个合法字节会被设置为这个值。它就像地雷旁边的警示牌,一旦被踩烂(覆盖),内核立刻报警。

6.2.3 未初始化内存读取(UMR):它在沉默中爆发

光看定义太空泛了。让我们拿第 5 章写过的测试代码 ch5/kmembugs_test.c 做个实验。

里面的 umr_slub() 函数非常简单(也非常愚蠢):它申请了 32 字节内存,然后什么都不填,直接读。这是典型的「未初始化内存读取」(Uninitialized Memory Read, UMR)。

/* 代码逻辑示意 */
q = kmalloc(32, GFP_KERNEL);
/* 故意不写 memset 或赋值 */
printk("q[3] is 0x%x\n", q[3]); /* 直接读垃圾值 */

场景一:没有调试保护(裸奔状态)

如果我们在没有任何 slub_debug 参数的情况下运行这个测试,内核日志会是什么样?

大概是这样:

[ 6845.100813] testcase to run: 10
[ 6845.101126] test_kmembugs:umr_slub(): testcase 10: simple UMR on slab memory
[ 6845.101771] test_kmembugs:umr_slub(): q[3] is 0x0
[ 6845.102203] q: 00000000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
[ 6845.102946] q: 00000010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................

看到那一排整齐的 00 了吗?

这非常危险。因为它看起来很「干净」,就像是这块内存被初始化成了 0。但这纯属巧合——内核分配给我们的恰好是一块被清零过的页面(比如由页面分配器回收的干净内存)。如果你依赖这个「巧合」,你的代码在其他环境、其他时刻一定会崩溃,而且这种 Bug 是不可复现的。

现在,让我们换个姿势,开启调试模式再跑一次。

场景二:开启 Poison(投毒模式)

虽然我们不在这里展示新的输出(留个悬念),但我可以告诉你后果:那 32 字节里不再是 00,而是全部变成了 6b

如果你去读 q[3],你不会得到 0x0,你会得到 0x6b6b6b6b

这就产生了一个巨大的认知反差

  • 之前:你以为读到的是 0,你开心地继续用了,结果三天后因为某个条件触发,程序在几千行之外崩了。
  • 现在:你读到的是满屏的 k0x6b),你立刻就知道:「哦,我忘了初始化这块内存。」

这就是 SLUB 调试的威力——它把不确定的垃圾值,变成了确定的错误标志。

6.2.4 手把手配置:slub_debug 的实战语法

理解了原理,现在我们把开关拨上去。

只要你的内核配置了 CONFIG_SLUB_DEBUG=y(我们在上一章已经确认过了),你就可以在启动时传递 slub_debug 参数。

语法格式如下:

slub_debug=<Flags>,<SlabList>
  • Flags: 就是我们刚才提到的 F, Z, P, U 等字母的任意组合。比如 FZPU。不要留空格,直接连在一起。
  • SlabList (可选): 指定要调试哪个具体的缓存名。如果不填,默认作用于所有 slab 缓存。

几个典型的用法:

  • 全开(穷追猛打型)slub_debug=FZPU

    这会给所有缓存开启全套检查。⚠️ 警告:这会让系统变慢非常非常多,内存开销也会剧增,除非是在专门做故障复现,否则别在生产力环境这么干。

  • 精准打击(针对某个缓存)slub_debug=,kmalloc-256

    注意逗号前面是空的,这意味着「使用默认标志(通常可能没有)」,但只针对 kmalloc-256 这个缓存。如果你想针对 kmalloc-256 开启 P 和 Z: slub_debug=PZ,kmalloc-256

  • 全部关闭(虽然默认就是关的)slub_debug=-

实战演示:开启全套监控

假设我们需要复现一个极其刁钻的 Bug,决定对所有缓存开启红区、投毒、健全性检查和用户跟踪。我们需要修改 GRUB 配置,在内核启动参数后面加上:

slub_debug=FZPU

保存,重启,进入系统。

验证一下是不是真的生效了。看内核启动参数:

$ cat /proc/cmdline
BOOT_IMAGE=/boot/vmlinuz-5.10.60-dbg02-gcc root=UUID=<...> ro quiet splash 3 slub_debug=FZPU

很好,参数已经传进去了。但这只是启动时的命令,内核真的听进去了吗?

我们需要去看 slab 缓存在 sysfs 下的实际状态。拿 kmalloc-32(专门分配 32 字节小对象的缓存)来开刀。

$ export SLAB=/sys/kernel/slab/kmalloc-32
$ sudo cat ${SLAB}/sanity_checks ${SLAB}/red_zone ${SLAB}/poison ${SLAB}/store_user
1
1
1
1
$

看到那四个 1 了吗?

这意味着:

  • sanity_checks (F): ON
  • red_zone (Z): ON
  • poison (P): ON
  • store_user (U): ON

现在的内存分配器就像一个武装到牙齿的安检员,每一个字节的进出都要被它摸一遍。

系统已经准备好了。接下来,我们把那些有问题的测试代码扔进去,看看这套机制是怎么报错的。