首页
/
行业洞察
/
正文
INDUSTRY INSIGHT · 深度
内核page fault排查实战:从oops日志到vmalloc越界与use-after-free定位
📅 2026/10/8 22:16:21
✍️ 爱科研究院
👁 阅读 3,247
1. 从一次深夜告警说起page fault 为什么会让内核直接躺平凌晨两点收到一台线上服务器的告警业务进程全部失联SSH 连不上带外控制台只剩一行反复刷屏的日志unable to handle page fault for address: ffff8xxx...。这种场景对做内核或者系统运维的人来说并不陌生——它不是普通的用户态段错误而是内核自己在访问某个虚拟地址时触发了缺页异常并且异常处理路径没能把它兜住最终走到oops甚至直接 panic。先把概念理清楚。page fault缺页异常本身不是错误它是虚拟内存机制正常运转的一部分。进程访问一个尚未建立物理映射的虚拟页时CPU 触发缺页异常内核的缺页处理程序负责分配物理页、填充内容、建立页表项然后重新执行那条指令。用户态的缺页绝大多数都能被优雅处理最多是 SIGSEGV。但一旦这个异常发生在内核态情况就完全不同了内核代码访问的地址如果落在非法区域或者对应的页表项根本不该缺失缺页处理程序会发现这个地址我处理不了于是打印unable to handle page fault接着就是寄存器 dump 和调用栈。这里有个关键点很多人会混淆unable to handle page fault和kernel NULL pointer dereference经常一起出现但它们的含义有细微差别。前者强调的是缺页处理程序无法为该地址建立映射后者强调的是访问了一个空指针。实际上内核空指针解引用往往就表现为一次无法处理的缺页因为地址 0 附近没有映射。所以看到这条日志第一反应应该是内核代码访问了一个没有有效映射的虚拟地址接下来要做的就是搞清楚这个地址是什么、从哪来、为什么没有映射。我处理过的这类问题里触发源大致可以归成几类驱动代码里对vmalloc返回指针的越界访问、结构体成员偏移算错导致访问到野地址、释放后使用use-after-free让指针指向了已经归还的虚拟区间、以及页表项被意外清除。这几类的排查手法差别很大但入口都是同一份 oops 日志。下面我按实际排查顺序把整个链路拆开讲。2. 读懂 oops 日志从寄存器到调用栈的完整信息提取2.1 先看那行地址它决定了排查方向unable to handle page fault for address: ffff8xxx这行里的地址是出错的虚拟地址是整个排查的锚点。拿到它之后第一件事是判断它属于哪一类虚拟地址区间。x86_64 上内核空间的布局大致是这样的地址区间用途典型特征ffff8880_00000000起直接映射区direct map物理内存的线性映射地址连续ffffc900_00000000起vmalloc 区非连续内存模块、大块分配走这里ffffea00_00000000起vmemmapstruct page 数组ffffffff_80000000起内核代码段内核镜像本身如果出错地址落在 vmalloc 区那基本可以锁定是vmalloc/vzalloc相关的分配出了问题如果落在直接映射区但明显越界可能是数组越界或者指针运算错误如果是个很小的值比如0x18、0x2a那几乎可以确定是空指针加偏移。这一步判断能帮你砍掉一大半无关代码。2.2 寄存器 dump 里藏着谁在访问oops 日志里会打印一堆寄存器其中最关键的是RIP出错指令地址、CR2触发缺页的线性地址和上面那行地址一致、以及各个通用寄存器。RIP告诉你内核正在执行哪条指令时出的错配合Code:那行反汇编字节可以精确定位到源码行。我习惯的做法是记下RIP的值用gdb vmlinux或者addr2line -e vmlinux -f -i RIP反查源码位置。看Code:那行的字节对照反汇编确认是哪条访存指令mov、cmp之类。看通用寄存器里哪个寄存器的值接近出错地址那个寄存器大概率就是肇事指针。举个我实际遇到的例子RIP指向一个mov 0x18(%rax), %rbx而RAX的值是ffffc90001234000出错地址是ffffc90001234018。这就非常清楚了——代码拿了一个 vmalloc 区的指针然后访问它的第 0x18 字节偏移但这个指针指向的内存已经被释放或者根本没分配那么大。偏移 0x18 对应结构体的某个成员顺着结构体定义就能找到是哪一行代码。2.3 调用栈要从下往上读调用栈Call Trace是从当前函数往调用者方向打印的最上面是出错点最下面是调用链的根。读的时候要注意两点一是内联函数可能不显示二是尾调用优化会让栈看起来断了。如果栈里有?或者地址看起来不对说明栈可能被破坏了这时候要结合RSP和栈内存 dump 一起看。我一般会先把调用栈里出现的函数名全部记下来然后对照源码画出调用关系。如果出错函数是个通用函数比如memcpy、kfree那真正的问题在调用它的业务代码里需要往上一层甚至上两层找。这一步不要偷懒很多新手看到memcpy出错就以为是memcpy的问题其实memcpy只是受害者。3. vmalloc 区的坑为什么这块内存最容易出问题3.1 vmalloc 和 kmalloc 的本质区别要理解 vmalloc 相关的 page fault得先搞清楚它和 kmalloc 的区别。kmalloc分配的是物理连续的内存返回的地址在直接映射区虚拟地址和物理地址只差一个固定偏移访问它不需要额外的页表项TLB 命中率高。vmalloc分配的是虚拟连续但物理不连续的内存内核需要专门为它建立页表项把一段虚拟地址映射到分散的物理页上。这个差异带来几个后果第一vmalloc 的分配和释放都要操作页表开销比 kmalloc 大得多第二vmalloc 区的地址范围有限x86_64 上默认是 32TB 左右但实际可用受配置影响大量分配可能耗尽第三vmalloc 返回的指针如果被越界访问很容易踩到没有映射的虚拟地址因为 vmalloc 区里相邻的虚拟区间之间往往有 guard page 或者干脆没映射。3.2 一个典型的越界访问案例我遇到过这样一个问题某个驱动用vmalloc分配了一块缓冲区大小是按count * sizeof(struct item)算的但代码里在填充数据时循环条件写成了i count多访问了一个元素。这个多出来的元素落在缓冲区末尾之后而 vmalloc 分配的缓冲区末尾通常紧跟着一个未映射的页guard page于是访问它直接触发unable to handle page fault。排查这类问题的关键是确认分配大小和实际访问范围。我会在代码里找到vmalloc调用点算出分配了多少字节再找到出错地址相对于返回指针的偏移两者一对比就知道是不是越界。如果偏移量刚好等于分配大小或者略大于它那基本可以坐实越界。提示vmalloc 分配的缓冲区末尾不一定总有 guard page取决于内核配置和分配器实现。但越界访问落在未映射区域时表现就是本文讨论的这种 page fault。3.3 释放后使用更隐蔽的一类比越界更隐蔽的是 use-after-free。vfree之后那段虚拟地址的页表项被清除但指针变量如果没置空后续代码继续用它访问就会触发缺页。这类问题的难点在于出错点往往离真正的 bug 点很远中间可能隔了好几个函数调用、甚至隔了一段时间。定位 use-after-free 我一般用两个手段一是打开 KASANKernel Address Sanitizer它能在释放时做标记访问时立刻报错并给出释放点的调用栈二是如果 KASAN 因为性能原因不能常开就在vfree前后加日志记录指针值和调用栈然后和出错时的地址比对。KASAN 的代价是内存占用和性能开销都比较大生产环境慎用但在复现环境里它是定位这类问题的利器。4. 页表层面的排查从 CR2 到页表项的手工走查4.1 为什么有时候要看页表大部分 page fault 通过代码审查就能定位但有一类问题必须下沉到页表层面页表项被意外修改或清除。比如某个驱动错误地操作了页表、内存热插拔导致映射变化、或者硬件故障让页表项损坏。这种情况下代码逻辑看起来没问题但页表状态不对只能手工走查。4.2 手工走查页表的步骤在能进入调试器比如通过 kdump 拿到 vmcore或者用 QEMU 调试内核的前提下可以按下面的步骤走从 oops 日志拿到CR3页表基址和出错地址CR2。把CR2按 x86_64 四级页表结构拆成 PGD、PUD、PMD、PTE 的索引。依次读取各级页表项检查 present 位、rw 位、user 位是否符合预期。如果某一级页表项的 present 位是 0说明映射在这一级就断了问题出在更上层。x86_64 的地址拆分规则是bits 47:39 是 PGD 索引bits 38:30 是 PUD 索引bits 29:21 是 PMD 索引bits 20:12 是 PTE 索引bits 11:0 是页内偏移。用gdb连上 vmcore 后可以用x/gx命令逐级读取。这个过程比较繁琐但能给出确定性的结论。4.3 一个页表项被清除的真实场景我处理过一个案例某模块在初始化时用vmalloc分配内存并建立映射但在某个错误处理路径里调用了vfree而正常路径下这块内存还在被使用。由于错误处理路径只在特定条件下触发平时测试根本复现不了直到线上某个边界条件命中才爆发。用 kdump 拿到 vmcore 后走查页表发现出错地址对应的 PTE 的 present 位是 0而相邻地址的映射都正常这就锁定了是这块内存被单独释放了。再结合代码里的vfree调用点很快找到了那个错误处理路径。这个案例给我的教训是错误处理路径的释放逻辑要和正常路径严格对称任何提前释放都要反复确认没有其他引用。后来我在代码审查里加了一条规则所有vfree/kfree调用点必须能说清楚谁还持有这个指针。5. 定位工具链从 crash 到 ftrace 的实战组合5.1 crash 工具分析 vmcore 的主力crash是分析内核转储的标配工具。拿到 vmcore 和对应的 vmlinux 后几条常用命令能快速定位问题# 打开 vmcore crash vmlinux vmcore # 查看 panic 时的日志和寄存器 crash log crash bt # 查看出错地址对应的页表 crash vtop ffffc90001234018 # 查看某个结构体变量的内容 crash struct my_struct ffffc90001234000 # 查看调用栈上某个函数的参数 crash bt -fvtop这条命令特别有用它直接帮你走查页表输出虚拟地址到物理地址的映射关系如果映射不存在会明确告诉你。比起手工拆页表它省事得多。5.2 ftrace 和 kprobe动态追踪的利器如果问题能复现但拿不到 vmcore可以用 ftrace 和 kprobe 做动态追踪。比如怀疑某个函数访问了非法地址可以在它入口处挂 kprobe打印参数和调用栈# 挂 kprobe 到目标函数 echo p:myprobe my_function ptr%di /sys/kernel/debug/tracing/kprobe_events echo 1 /sys/kernel/debug/tracing/events/kprobes/myprobe/enable cat /sys/kernel/debug/tracing/trace_pipeftrace 的function_graphtracer 能画出完整的调用图配合set_ftrace_filter只看关心的函数对理清调用关系很有帮助。我通常先用 function_graph 确认调用路径再用 kprobe 打印关键变量的值两步下来基本能锁定问题。5.3 工具选型的取舍工具适用场景代价局限crash vmcore已 panic需要事后分析需要配置 kdump依赖转储完整性KASAN内存越界、use-after-free性能内存开销大生产环境慎用ftrace动态追踪调用路径开销较小需要问题可复现kprobe打印特定变量值有一定开销需要知道挂载点gdb QEMU开发阶段单步调试环境搭建复杂不适合生产选工具的原则是能复现就用动态追踪不能复现就靠 vmcore。两者都没有的话只能靠代码审查加日志效率会低很多。6. 几个容易踩的坑和我的实操心得6.1 别被地址看起来正常骗了有一次出错地址是ffff888012345678落在直接映射区看起来完全正常我一开始以为是硬件问题。后来仔细算了一下这个地址对应的物理地址超出了实际安装的内存范围——是代码里用了一个错误的物理地址做phys_to_virt转换。所以地址落在合法区间不代表它指向合法的物理内存直接映射区也要检查物理地址范围。6.2 注意编译优化对调用栈的干扰-O2编译下函数内联和尾调用优化会让调用栈丢失一些帧看起来像是从 A 直接跳到了 C。遇到这种情况不要怀疑栈坏了先看反汇编确认是不是尾调用。我一般会在复现环境里用-O0或者-Og重新编译可疑模块让调用栈更清晰定位完再换回优化版本验证。6.3 日志级别和 printk 的坑排查阶段经常要加 printk但要注意 printk 本身可能触发缺页或者死锁。比如在缺页处理路径里加 printk如果 printk 又要访问某个未映射的地址就会递归触发异常。另外 printk 的日志级别如果设得太低可能被 console 的日志级别过滤掉看不到输出。我的习惯是用pr_err或者pr_emerg保证能打出来同时避免在原子上下文里做耗时操作。6.4 复现环境的价值这类问题最难的是复现。我的经验是尽量在 QEMU 里搭一个和线上配置接近的环境用相同的内核版本、相同的模块、相同的参数。QEMU 的好处是可以随时挂 gdb、可以打快照回滚、可以用-s -S从启动第一条指令开始调试。很多在线上只能靠 vmcore 猜的问题在 QEMU 里单步跟一遍就清楚了。搭环境的投入看起来大但比起在线上反复试错性价比高得多。6.5 记录和复盘每次定位完这类问题我都会写一份简短的复盘出错地址是什么、属于哪个区间、根因是什么、修复方案是什么、有没有类似的代码需要一起检查。这份复盘不只是给自己看团队里其他人遇到类似问题可以直接参考。内核问题的模式其实就那么几类积累多了下次看到 oops 日志基本能猜个八九不离十。最后分享一个我常用的快速判断技巧拿到unable to handle page fault的日志先看地址的低位。如果低位是个很小的值比如 0x0 到 0x1000 之间大概率是空指针加偏移如果地址落在 vmalloc 区且偏移量接近某个分配大小大概率是越界如果地址看起来完全随机那可能是野指针或者内存损坏这时候要优先怀疑 use-after-free 或者栈溢出。这个技巧不能替代完整分析但能帮你在第一时间把排查方向缩小到一两个可能性上省下不少时间。
📌 标签:
工业官网
设计趋势
AI 建站
SEO
获取完整报告 →
RELATED ARTICLES
推荐阅读
2026/10/8 22:16:21
用Comate开发我的第一个MCP:从零搭建到TaoToken统一Key接入
2026/10/8 22:16:21
论文字数不够怎么办?科迅捷AI帮你充实内容不注水
2026/10/8 22:16:21
【词汇专栏】Long Context:长上下文——AI的超长记忆与TaoToken统一调用实践
2026/10/8 23:11:28
高职院校数据治理与数智校园建设规划方案 ——基于DCMM扩展模型的数据治理体系与数据资产建设路径
2026/10/8 23:11:28
直流电源和万用表使用教程,直流电源原理、万用表实操 1
2026/10/8 23:11:28
Dive into Claude Code 上下文管理深度教程:5级压缩管线+9个上下文源,搞定200K窗口难题
2026/10/8 23:11:28
重学网工之-BGP配置
2026/10/8 23:11:28
论文AI率太高怎么降?靠谱可信的降AI率平台推荐,降AI率不达标全额退款
2026/10/8 23:06:28
DeepBot上下文工程揭秘:history-pruner与token压缩背后的代码实现原理
2026/10/8 0:04:11
Agent Skills 完全指南:原理、写法、安装与实战避坑
2026/10/8 0:04:11
Agent Skills 实战:从 Genkit 定义到 GKE 部署与排查
2026/10/8 0:04:11
Agent Skills 实战:从设计到调试的完整指南
2026/10/8 5:02:14
Jev+Agent接管浏览器:browser-use实战与jev-ultrafast性能优化
2026/10/7 9:55:49
多智能体集群实战:DeepAgents编排、MCP与A2A协议及Skills体系
2026/10/7 14:02:03
hindsight:面向LLM应用的事后可观测性工程实践
2026/10/8 4:30:43
我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
2026/10/8 2:46:15
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026/10/8 4:32:33
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)