☰
Linux 内核内存管理(三):深入解析 kmemcheck —— 内核版 Valgrind 的未初始化内存检测机制
2026/9/30 6:47:50 网站建设 项目流程
  • 文档
  • 教程
  • 操作系统

【免费下载链接】linux-insides

A book-in-progress about the Linux kernel and its insides.

项目地址:https://gitcode.com/gh_mirrors/li/linux-insides
点击查看免费下载

kmemcheck是 Linux 内核提供的内存调试机制,专门用于捕获内核代码对未初始化内存的非法访问。本文是 linux-insides 内存管理章节的第三部分,在介绍 fix-mapped 地址与 ioremap(见 MM/linux-mm-2.md)之后,系统讲解 kmemcheck 的启用方法、工作原理,并逐层剖析其在内核中的实现:从命令行参数解析、影子页分配,到页面隐藏、缺页异常(page fault)处理与单步执行的完整闭环。读完本文,你将掌握 kmemcheck 的配置参数含义、它在 x86_64 上"以缺页异常为陷阱"的设计思想,以及每个关键代码路径的职责。

1. 背景:为什么在深入学习内存管理前要先讲调试机制

在上一部分我们了解了两个内存管理概念:

  • Fix-Mapped 地址:虚拟内存中的特殊区域,其对应的物理映射在编译期即可计算,代表地址在编译期恒定、物理地址仅在启动过程中设定的页面;
  • ioremap:将输入/输出相关的物理内存映射到虚拟内存空间的能力。

所有 memory-mapped I/O 地址都不会被内核直接使用。在内核使用这类内存之前,必须先将其映射到虚拟地址空间,这正是ioremap机制的核心目的。注意,上一部分我们只看到了早期(early)ioremap,非早期(non-early)版本要等到我们了解 vmalloc 等各类内存分配器之后才能深入理解。

因此在进入 Linux 内核非早期的内存管理之前,需要先了解一些提供特殊调试能力的机制,例如内存泄漏检测、内存控制等。理解了这些机制之后,再回头理解内存管理的全貌会容易得多。kmemcheck 正是这样一个调试机制,本部分围绕它展开。

2. 从/proc/iomem与/proc/ioports看物理设备内存布局

在深入 kmemcheck 之前,先看两个观察系统内存布局的入口。执行:

$ sudo cat /proc/iomem 00000000-00000fff : reserved 00001000-0009d7ff : System RAM 0009d800-0009ffff : reserved 000a0000-000bffff : PCI Bus 0000:00 000c0000-000cffff : Video ROM 000d0000-000d3fff : PCI Bus 0000:00 000d4000-000d7fff : PCI Bus 0000:00 000d8000-000dbfff : PCI Bus 0000:00 000dc000-000dffff : PCI Bus 0000:00 000e0000-000fffff : reserved ... ... ...

可以看到系统为每个物理设备展示的内存映射:第一列是各类型内存使用的寄存器地址范围,第二列列出了位于这些寄存器范围内的内存种类。再比如:

$ sudo cat /proc/ioports 0000-0cf7 : PCI Bus 0000:00 0000-001f : dma1 0020-0021 : pic1 0040-0043 : timer0 0050-0053 : timer1 0060-0060 : keyboard 0064-0064 : keyboard 0070-0077 : rtc0 0080-008f : dma page reg 00a0-00a1 : pic2 00c0-00df : dma2 00f0-00ff : fpu 00f0-00f0 : PNP0C04:00 03c0-03df : vga+ 03f8-03ff : serial 04d0-04d1 : pnp 00:06 0800-087f : pnp 00:01 0a00-0a0f : pnp 00:04 0a20-0a2f : pnp 00:04 0a30-0a3f : pnp 00:04 ... ... ...

/proc/ioports列出当前已注册的、用于与设备进行输入/输出通信的端口区域。可以看到,这些 I/O 相关内存都不能被内核直接使用,必须先经ioremap映射,而 kmemcheck 正是接下来要掌握的调试工具。

3. kmemcheck 是什么:内核版的 Valgrind

从名字即可猜出,kmemcheck 用于"检查内存"。其核心作用是检查内核代码是否访问了未初始化的内存。举个简单的 C 程序例子:

#include <stdlib.h> #include <stdio.h> struct A { int a; }; int main(int argc, char **argv) { struct A *a = malloc(sizeof(struct A)); printf("a->a = %d\n", a->a); return 0; }

这里为A结构分配了内存,然后尝试打印a字段的值。如果直接编译:

gcc test.c -o test

编译器不会提示a字段未初始化。但用 valgrind 运行:

~$ valgrind --leak-check=yes ./test ==28469== Memcheck, a memory error detector ==28469== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al. ==28469== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info ==28469== Command: ./test ==28469== ==28469== Conditional jump or move depends on uninitialised value(s) ==28469== at 0x4E820EA: vfprintf (in /usr/lib64/libc-2.22.so) ==28469== by 0x4E88D48: printf (in /usr/lib64/libc-2.22.so) ==28469== by 0x4005B9: main (in /home/alex/test) ==28469== ==28469== Use of uninitialised value of size 8 ==28469== at 0x4E7E0BB: _itoa_word (in /usr/lib64/libc-2.22.so) ==28469== by 0x4E8262F: vfprintf (in /usr/lib64/libc-2.22.so) ==28469== by 0x4E88D48: printf (in /usr/lib64/libc-2.22.so) ==28469== by 0x4005B9: main (in /home/alex/test) ... ... ...

kmemcheck 对内核所做的事情,正如 valgrind 对用户态程序所做的事情:检测未初始化内存的使用。这是理解整个机制的一把钥匙。

4. 启用 kmemcheck:内核配置与架构支持

要启用该机制,需要在内核配置菜单中打开CONFIG_KMEMCHECK:

Kernel hacking -> Memory Debugging

对应的 menuconfig 界面如下:

进入 "kmemcheck: trap use of uninitialized memory" 子菜单后,可以看到 kmemcheck 提供的一系列运行时配置选项:

这些选项的核心含义包括:

  • kmemcheck: default mode at boot (one-shot):设置 kmemcheck 启动时的默认工作模式,可选 one-shot 模式;
  • kmemcheck: error queue size:kmemcheck 记录内存错误使用的环形队列大小(默认值 64);
  • kmemcheck: shadow copy size (5 => 32 bytes, 6 => 64 bytes):影子内存拷贝大小,默认值 5 对应 32 字节;
  • kmemcheck: allow partially uninitialized memory:允许内核代码使用部分初始化的内存;
  • kmemcheck: allow bit-field manipulation:允许对未初始化的位域进行操作。

4.1 架构限制:目前仅支持 x86_64

kmemcheck 目前只实现于 x86_64 架构。在arch/x86/Kconfig中可以看到:

config X86 ... ... ... select HAVE_ARCH_KMEMCHECK ... ... ...

也就是说,其他架构并没有与之对应的专属支持。

4.2 内核命令行参数:kmemcheck=0 / 1 / 2

除了配置选项,还需要向内核命令行传入以下三个参数之一:

  • kmemcheck=0:禁用;
  • kmemcheck=1:启用;
  • kmemcheck=2:one-shot 模式——在检测到第一次使用未初始化内存后自动关闭。事实上该模式在内核中是默认启用的。

4.3 工作原理概述:把页面标记为 non-present

kmemcheck 的检测思路非常巧妙。当内核尝试分配内存时,例如:

struct my_struct *my_struct = kmalloc(sizeof(struct my_struct), GFP_KERNEL);

即有人想要访问一个页面时,就会产生page fault 异常。这是通过让 kmemcheck 把内存页面标记为non-present(非驻留)实现的(关于页表与 P 位的详细知识可参考 X86/linux-x86-1.md 中关于 Paging 的章节)。当 page fault 异常发生时,异常处理程序知道这一点,并在 kmemcheck 已启用的情况下把控制权转交给它。kmemcheck 完成检查后,页面会被重新标记为present,被中断的代码得以继续执行。这里有一个微妙的细节:当被中断代码的第一条指令执行时,kmemcheck 会再次把页面标记为non-present,这样下一次内存访问就会被再次捕获。

5. 内核实现(一):初始化与命令行解析

kmemcheck 的实现分为两部分:

  • 通用部分:位于mm/kmemcheck.c;
  • x86_64 架构特定部分:位于arch/x86/mm/kmemcheck目录。

5.1 早期阶段:param_kmemcheck

kmemcheck 子系统由两个阶段组成。第一阶段是早期阶段。回顾内核初始化章节(Initialization/linux-initialization-7.md),内核命令行是在初始化过程中由do_initcall_level、do_early_param函数解析的。在mm/kmemcheck.c中可以看到param_kmemcheck函数,它会在早期命令行解析期间被调用:

static int __init param_kmemcheck(char *str) { int val; int ret; if (!str) return -EINVAL; ret = kstrtoint(str, 0, &val); if (ret) return ret; kmemcheck_enabled = val; return 0; } early_param("kmemcheck", param_kmemcheck);

如前所述,param_kmemcheck可能取0(禁用)、1(启用)或2(one-shot)。实现很简单:用kstrtoint把kmemcheck命令行选项的字符串值转换为整数,并存入全局变量kmemcheck_enabled。early_param("kmemcheck", param_kmemcheck)将该解析函数注册为早期启动参数回调。

5.2 第二阶段:kmemcheck_init 与自检

第二阶段在内核初始化早期 initcall 阶段执行,由kmemcheck_init表示:

int __init kmemcheck_init(void) { ... ... ... } early_initcall(kmemcheck_init);

kmemcheck_init的主要目标是调用kmemcheck_selftest并检查其返回值:

if (!kmemcheck_selftest()) { printk(KERN_INFO "kmemcheck: self-tests failed; disabling\n"); kmemcheck_enabled = 0; return -EINVAL; } printk(KERN_INFO "kmemcheck: Initialized\n");

若自检失败则返回EINVAL并关闭 kmemcheck。kmemcheck_selftest检查各种与内存访问相关的**操作码(opcode)**的编码长度,例如rep movsb、movzwq等。若操作码的尺寸与期望值一致,返回true,否则返回false。关于 initcall 机制的细节可以参考 Concepts/linux-cpu-3.md。

6. 内核实现(二):影子页分配与页面隐藏

现在看分配路径。当有人调用:

struct my_struct *my_struct = kmalloc(sizeof(struct my_struct), GFP_KERNEL);

经过一系列函数调用后,会到达kmem_getpages。该函数定义于mm/slab.c,主要目标是以给定标志分配物理页。其末尾的关键代码:

if (kmemcheck_enabled && !(cachep->flags & SLAB_NOTRACK)) { kmemcheck_alloc_shadow(page, cachep->gfporder, flags, nodeid); if (cachep->ctor) kmemcheck_mark_uninitialized_pages(page, nr_pages); else kmemcheck_mark_unallocated_pages(page, nr_pages); }

这里的逻辑是:如果 kmemcheck 已启用,且SLAB_NOTRACK位没有在 flags 中置位(SLAB_NOTRACK表示不追踪未初始化内存),则为刚分配的页面设置non-present位。此外,若缓存对象带有构造函数(ctor,后续章节详解),则将页面标记为未初始化;否则标记为未分配。

6.1 kmemcheck_alloc_shadow:影子页的建立

kmemcheck_alloc_shadow定义于mm/kmemcheck.c:

void kmemcheck_alloc_shadow(struct page *page, int order, gfp_t flags, int node) { struct page *shadow; shadow = alloc_pages_node(node, flags | __GFP_NOTRACK, order); for(i = 0; i < pages; ++i) page[i].shadow = page_address(&shadow[i]); kmemcheck_hide_pages(page, pages); }

首先为**影子位(shadow bits)**分配内存空间。若页面中该位被置位,表示该页面被 kmemcheck 追踪。分配影子空间后,用该位填充所有已分配的页面,最后调用kmemcheck_hide_pages,参数为已分配页面的指针与页面数量。

6.2 kmemcheck_hide_pages:清除 present 位、设置 hidden 位

kmemcheck_hide_pages是架构特定函数,实现位于arch/x86/mm/kmemcheck/kmemcheck.c。其主要目标是为给定页面设置non-present位:

void kmemcheck_hide_pages(struct page *p, unsigned int n) { unsigned int i; for (i = 0; i < n; ++i) { unsigned long address; pte_t *pte; unsigned int level; address = (unsigned long) page_address(&p[i]); pte = lookup_address(address, &level); BUG_ON(!pte); BUG_ON(level != PG_LEVEL_4K); set_pte(pte, __pte(pte_val(*pte) & ~_PAGE_PRESENT)); set_pte(pte, __pte(pte_val(*pte) | _PAGE_HIDDEN)); __flush_tlb_one(address); } }

这里遍历所有页面,为每个页面查找对应的页表项(page table entry)。若查找成功,则:

  1. 通过pte_val(*pte) & ~_PAGE_PRESENT清除 present 位;
  2. 通过pte_val(*pte) | _PAGE_HIDDEN设置隐藏位_PAGE_HIDDEN;
  3. 调用__flush_tlb_one(address)冲刷 TLB(translation lookaside buffer),因为部分页面已改变。

从此开始,这些页面被 kmemcheck 追踪。由于 present 位被清除,kmalloc返回指针后代码一旦访问该内存,就会立即触发 page fault。

7. 内核实现(三):page fault 处理路径

回顾内核初始化章节(Initialization/linux-initialization-2.md),page fault 处理程序位于arch/x86/mm/fault.c,由do_page_fault函数表示。__do_page_fault开头可以看到如下检查:

static noinline void __do_page_fault(struct pt_regs *regs, unsigned long error_code, unsigned long address) { ... ... ... if (kmemcheck_active(regs)) kmemcheck_hide(regs); ... ... ... }

7.1 kmemcheck_active:per-CPU 上下文的 balance 字段

kmemcheck_active获取 per-CPU 的kmemcheck_context结构,并返回该结构balance字段与零比较的结果:

bool kmemcheck_active(struct pt_regs *regs) { struct kmemcheck_context *data = this_cpu_ptr(&kmemcheck_context); return>if (kmemcheck_fault(regs, address, error_code)) return;

7.2 kmemcheck_fault:判断缺页原因

首先,kmemcheck_fault检查该缺页是否由正确的原因触发。先检查标志寄存器,确认处于普通内核模式:

if (regs->flags & X86_VM_MASK) return false; if (regs->cs != __KERNEL_CS) return false;

若这些检查未通过,则从kmemcheck_fault返回,视为与 kmemcheck 无关的 page fault。之后,尝试查找与缺页地址相关的页表项,若找不到则返回:

pte = kmemcheck_pte_lookup(address); if (!pte) return false;

kmemcheck_fault的最后两步是调用kmemcheck_access函数——它检查对给定页面的访问,并通过设置 present 位来重新显示页面地址。

7.3 kmemcheck_access:错误记录与环形队列

kmemcheck_access承担主要工作:检查触发 page fault 的当前指令。若发现错误,其上下文会被保存到环形队列中:

static struct kmemcheck_error error_fifo[CONFIG_KMEMCHECK_QUEUE_SIZE];

这里CONFIG_KMEMCHECK_QUEUE_SIZE对应第 4 节配置菜单中的"错误队列大小"。

kmemcheck 还声明了一个特殊的tasklet(tasklet 机制详见 Interrupts/linux-interrupts-9.md):

static DECLARE_TASKLET(kmemcheck_tasklet, &do_wakeup, 0);

当它被调度执行时,运行的是arch/x86/mm/kmemcheck/error.c中的do_wakeup函数。do_wakeup会调用kmemcheck_error_recall,打印由 kmemcheck 收集到的错误。

8. 内核实现(四):恢复 present 位与单步执行陷阱

kmemcheck_fault函数末尾会调用:

kmemcheck_show(regs);

该函数会为给定页面重新设置 present 位:

if (unlikely(data->balance != 0)) { kmemcheck_show_all(); kmemcheck_error_save_bug(regs); >static unsigned int kmemcheck_show_all(void) { struct kmemcheck_context *data = this_cpu_ptr(&kmemcheck_context); unsigned int i; unsigned int n; n = 0; for (i = 0; i <>int kmemcheck_show_addr(unsigned long address) { pte_t *pte; pte = kmemcheck_pte_lookup(address); if (!pte) return 0; set_pte(pte, __pte(pte_val(*pte) | _PAGE_PRESENT)); __flush_tlb_one(address); return 1; }

8.1 TF 标志:单步模式让隐藏-检查循环自动继续

在kmemcheck_show函数末尾,若 TF 标志未设置则设置之:

if (!(regs->flags & X86_EFLAGS_TF)) >
  • 文档
  • 教程
  • 操作系统

【免费下载链接】linux-insides

A book-in-progress about the Linux kernel and its insides.

项目地址:https://gitcode.com/gh_mirrors/li/linux-insides
点击查看免费下载

相关推荐

上一篇:ForecastleApp CRD深度解析:如何通过自定义资源动态管理Kubernetes应用
下一篇:终极编程记忆:OpenCode如何让你永不丢失工作状态

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询