ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Linux 内核内存管理(三):深入解析 kmemcheck —— 内核版 Valgrind 的未初始化内存检测机制

Linux 内核内存管理(三):深入解析 kmemcheck —— 内核版 Valgrind 的未初始化内存检测机制 文档教程操作系统【免费下载链接】linux-insidesA 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机制的核心目的。注意上一部分我们只看到了早期earlyioremap非早期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-checkyes ./test 28469 Memcheck, a memory error detector 28469 Copyright (C) 2002-2015, and GNU GPLd, 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_KMEMCHECKKernel hacking - Memory Debugging对应的 menuconfig 界面如下进入 kmemcheck: trap use of uninitialized memory 子菜单后可以看到 kmemcheck 提供的一系列运行时配置选项这些选项的核心含义包括kmemcheck: default mode at boot (one-shot)设置 kmemcheck 启动时的默认工作模式可选 one-shot 模式kmemcheck: error queue sizekmemcheck 记录内存错误使用的环形队列大小默认值 64kmemcheck: shadow copy size (5 32 bytes, 6 64 bytes)影子内存拷贝大小默认值 5 对应 32 字节kmemcheck: allow partially uninitialized memory允许内核代码使用部分初始化的内存kmemcheck: allow bit-field manipulation允许对未初始化的位域进行操作。4.1 架构限制目前仅支持 x86_64kmemcheck 目前只实现于 x86_64 架构。在arch/x86/Kconfig中可以看到config X86 ... ... ... select HAVE_ARCH_KMEMCHECK ... ... ...也就是说其他架构并没有与之对应的专属支持。4.2 内核命令行参数kmemcheck0 / 1 / 2除了配置选项还需要向内核命令行传入以下三个参数之一kmemcheck0禁用kmemcheck1启用kmemcheck2one-shot 模式——在检测到第一次使用未初始化内存后自动关闭。事实上该模式在内核中是默认启用的。4.3 工作原理概述把页面标记为 non-presentkmemcheck 的检测思路非常巧妙。当内核尝试分配内存时例如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.cx86_64 架构特定部分位于arch/x86/mm/kmemcheck目录。5.1 早期阶段param_kmemcheckkmemcheck 子系统由两个阶段组成。第一阶段是早期阶段。回顾内核初始化章节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启用或2one-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.cvoid 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。若查找成功则通过pte_val(*pte) ~_PAGE_PRESENT清除 present 位通过pte_val(*pte) | _PAGE_HIDDEN设置隐藏位_PAGE_HIDDEN调用__flush_tlb_one(address)冲刷 TLBtranslation lookaside buffer因为部分页面已改变。从此开始这些页面被 kmemcheck 追踪。由于 present 位被清除kmalloc返回指针后代码一旦访问该内存就会立即触发 page fault。7. 内核实现三page fault 处理路径回顾内核初始化章节Initialization/linux-initialization-2.mdpage 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_activeper-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 还声明了一个特殊的tasklettasklet 机制详见 Interrupts/linux-interrupts-9.mdstatic 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-insidesA book-in-progress about the Linux kernel and its insides.项目地址https://gitcode.com/gh_mirrors/li/linux-insides点击查看免费下载相关推荐Linux 内核内存管理揭秘三kmemcheck —— 内核未初始化内存访问检测机制的原理与实现Linux 内核内存管理揭秘三kmemcheck —— 内核未初始化内存访问检测机制的原理与实现 导读 本文是华中科技大学开放原子开源俱乐部《linux如何永久保存微信聊天记录3步实现数据留痕的终极指南如何永久保存微信聊天记录3步实现数据留痕的终极指南 你是否曾担心更换手机后那些珍贵的微信对话会永远消失家人的温馨对话、重要的工作沟通、朋友的日常分享这些Oumi 配置系统详解YAML 配置的组织、命名约定与 Omegaconf 加载机制Oumi 配置系统详解YAML 配置的组织、命名约定与 Omegaconf 加载机制 configs/ 是 Oumi 框架的配置中枢存放着用于训练、评估、推文档教程操作系统上一篇ForecastleApp CRD深度解析如何通过自定义资源动态管理Kubernetes应用下一篇终极编程记忆OpenCode如何让你永不丢失工作状态创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表