分配器的主要调试功能之一是它包括了用于快速识别数据损坏的算法。当检测到损坏时,分配器会导致系统立即出现故障。
本节介绍分配器如何识别数据损坏;您必须了解这一点才能调试这些问题。内存误用通常分为以下种类:
写入超出了缓冲区的结尾
访问未初始化的数据
继续使用已释放的缓冲区
损坏内核内存
阅读接下来的三节时,请牢记这些问题。 它们将有助于了解分配器的设计,并使您可以更有效地诊断问题。
如果在 kmem_cache 的 flags 字段中设置了 KMF_DEADBEEF (0x2) 位,则分配器会尝试通过将特殊模式写入所有已释放缓冲区中,从而使内存损坏易于检测。 此模式即是 0xdeadbeef。 由于典型的内存区域同时包含已分配的内存和已释放的内存,因此每种块的各个节将是分散的;以下是来自 "kmem_alloc_24" 高速缓存的一个示例:
0x70a9add8: deadbeef deadbeef 0x70a9ade0: deadbeef deadbeef 0x70a9ade8: deadbeef deadbeef 0x70a9adf0: feedface feedface 0x70a9adf8: 70ae3260 8440c68e 0x70a9ae00: 5 4ef83 0x70a9ae08: 0 0 0x70a9ae10: 1 bbddcafe 0x70a9ae18: feedface 139d 0x70a9ae20: 70ae3200 d1befaed 0x70a9ae28: deadbeef deadbeef 0x70a9ae30: deadbeef deadbeef 0x70a9ae38: deadbeef deadbeef 0x70a9ae40: feedface feedface 0x70a9ae48: 70ae31a0 8440c54e
从 0x70a9add8 开始的缓冲区是使用 0xdeadbeef 模式填充的,这就直接表明了缓冲区当前是空闲的。 另一个空闲缓冲区从 0x70a9ae28 开始;一个已分配的缓冲区介于它们之间,位于 0x70a9ae00。
您可能已经注意到此内存区域布局中有一些空洞,3 个 24 字节区域应该仅占用 72 字节的内存,而不是此处显示的 120 字节。 此差异将在下一节Redzone(禁区): 0xfeedface中进行说明。
模式 0xfeedface 频繁地在以上缓冲区中出现。 此模式称为 "redzone" 指示器。 通过此模式,分配器(以及调试问题的程序员)可以确定“错误”代码是否超出了缓冲区的边界。 redzone 后面是一些其他信息。 该数据的内容取决于其他因素(请参见内存分配日志记录)。 redzone 及其后缀统称为 buftag 区域。 图 9–1 概述了此信息。
如果在高速缓存中设置了 KMF_AUDIT、KMF_DEADBEEF 或 KMF_REDZONE 标志中的任何一个,则 buftag 会附加到该高速缓存的每个缓冲区。 buftag 的内容取决于是否设置了 KMF_AUDIT。
现在,可以轻易将上述内存区域分解为不同的缓冲区:
0x70a9add8: deadbeef deadbeef \ 0x70a9ade0: deadbeef deadbeef +- User Data (free) 0x70a9ade8: deadbeef deadbeef / 0x70a9adf0: feedface feedface -- REDZONE 0x70a9adf8: 70ae3260 8440c68e -- Debugging Data 0x70a9ae00: 5 4ef83 \ 0x70a9ae08: 0 0 +- User Data (allocated) 0x70a9ae10: 1 bbddcafe / 0x70a9ae18: feedface 139d -- REDZONE 0x70a9ae20: 70ae3200 d1befaed -- Debugging Data 0x70a9ae28: deadbeef deadbeef \ 0x70a9ae30: deadbeef deadbeef +- User Data (free) 0x70a9ae38: deadbeef deadbeef / 0x70a9ae40: feedface feedface -- REDZONE 0x70a9ae48: 70ae31a0 8440c54e -- Debugging Data
在位于 0x70a9add8 和 0x70a9ae28 的空闲缓冲区中,redzone 是使用 0xfeedfacefeedface 填充的。这是确定缓冲区是否空闲的便利方法。
在从 0x70a9ae00 开始的已分配缓冲区中,情况是不同的。请回想一下分配器基础知识,有两种分配类型:
1) 客户机使用 kmem_cache_alloc() 请求的内存,在这种情况下所请求缓冲区的大小等于高速缓存的 bufsize。
2) 客户机使用 kmem_alloc (9F) 请求的内存,在这种情况下所请求缓冲区的大小小于或等于高速缓存的 bufsize。 例如,对 20 个字节的请求将从 kmem_alloc_24 高速缓存得到满足。 分配器会通过在紧邻客户机数据之后放置一个标记(即 redzone 字节)来强制设置缓冲区边界:
0x70a9ae00: 5 4ef83 \ 0x70a9ae08: 0 0 +- User Data (allocated) 0x70a9ae10: 1 bbddcafe / 0x70a9ae18: feedface 139d -- REDZONE 0x70a9ae20: 70ae3200 d1befaed -- Debugging Data
位于 0x70a9ae18 的 0xfeedface 后跟一个 32 位的字,其中包含的内容看似一个随机值。 此数字实际上是缓冲区大小的编码表示形式。 要对此数字进行解码并获得已分配缓冲区的大小,请使用以下公式:
size = redzone_value / 251
因此,在本示例中,
size = 0x139d / 251 = 20 bytes.
这表明所请求缓冲区的大小为 20 个字节。 分配器将执行此解码操作,同时会发现 redzone 字节应该位于偏移量为 20 的位置。redzone 字节是十六进制模式 0xbb,如预期的那样存在于 0x729084e4 (0x729084d0 + 0t20)。
图 9–3 说明了此内存布局的常规形式。
如果分配大小等于高速缓存的 bufsize,则 redzone 字节会覆写 redzone 本身的第一个字节,如图 9–4 中所示。
此覆写操作会导致 redzone 的第一个 32 位字为 0xbbedface 或 0xfeedfabb,具体取决于系统运行的硬件的字节存储顺序。
为什么分配大小以该方式进行编码? 要对大小进行编码,分配器可使用公式(251 * 大小 + 1)。对大小进行解码时,整数除法将废弃余数 '+1'。 但是,加上 1 是有价值的,因为分配器可以通过测试(大小 % 251 == 1)是否成立来检查大小是否有效。 这样,分配器可防止损坏 redzone 字节索引。
您可能想知道在用 redzone 字节覆写字中的第一个字节之前,地址 0x729084d4 上的可疑 0xbbddcafe 是什么。 它是 0xbaddcafe。如果在高速缓存中设置了 KMF_DEADBEEF 标志,则使用 0xbaddcafe 模式填充已分配但未初始化的内存。 分配器执行分配时,会循环通过缓冲区的各个字并验证每个字是否包含 0xdeadbeef,然后使用 0xbaddcafe 填充该字。
系统可能会发出以下故障消息:
panic[cpu1]/thread=e1979420: BAD TRAP: type=e (Page Fault) rp=ef641e88 addr=baddcafe occurred in module "unix" due to an illegal access to a user address
在这种情况下,导致故障的地址是 0xbaddcafe: 出现故障的线程访问了一些从未初始化的数据。
内核内存分配器会对应于之前所述的失败模式发出故障消息。 例如,系统可能会发出以下故障消息:
kernel memory allocator: buffer modified after being freed modification occurred at offset 0x30
由于分配器会尝试验证是否使用 0xdeadbeef 填充了不确定的缓冲区,因此能够检测到此情况。如果偏移的位置为 0x30,则不符合此条件。 由于此条件表明内存损坏,因此分配器会导致系统出现故障。
以下是另一个故障消息示例:
kernel memory allocator: redzone violation: write past end of buffer
由于分配器会尝试验证 redzone 字节 (0xbb) 是否处于它通过 redzone 大小编码所确定的位置,因此能够检测到此情况。 它无法在正确的位置找到签名字节。 由于这表明内存损坏,因此分配器会导致系统出现紧急情况。 其他的分配器故障消息将在稍后讨论。