BUG: unable to handle kernel paging request at <地址> 完整排查:dmesg 四要素、坏内存与第三方模块定罪
TL;DR:你的服务器 dmesg 里冒出BUG: unable to handle kernel paging request at ffff...,机器要么卡死重启,要么某个进程被内核杀掉。这行日志本身不是病,它是报告:内核访问了一块它不该碰的内存。真正要做的有三件事,先取四要素(地址、RIP、Call Trace、Modules linked in),再分三路定罪(第三方模块、内存硬件、内核自身 bug),最后决定是换内存条、卸模块还是升级内核。下面给可直接照抄的命令,以及一句"今晚就能做"的自检。
这句话在说什么
打个比方:内核运行在特权态,可以访问几乎整个地址空间。当它访问一个既没映射、也不属于任何进程的地址时,CPU 会触发缺页异常(#PF),内核发现自己"在上层根本没资格碰这里",就会打印这一行,然后把当前进程杀掉;如果当时正在中断上下文或持锁路径,就升级成Kernel panic,整机重启。
所以你看到的不是"某个程序崩了",而是"内核自己走错路了"。这类问题要么是硬件(内存坏了),要么是别人喂了它坏数据(第三方模块),要么是它自己的 bug。
第一步:取出四要素
崩溃之后机器往往已经重启,所以要看上一次开机的日志:
dmesg -T | grep -iE -A40 "unable to handle|NULL pointer dereference|general protection|Oops" # 或 journalctl -k -b -1 | grep -i -A60 "unable to handle"从这一大段里,你只需要盯四个东西:
| 要素 | 长什么样 | 用途 |
|---|---|---|
| 出错地址 | at ffff880562ea7f00,或CR2:那一行 | 判断是内核地址(高位全 f)还是近空指针(很小) |
| RIP / IP 行 | IP: [<ffffffff811612e0>] kmem_cache_alloc+0x50/0xd0 | 出事的函数名;若末尾带[模块名]就是第三方模块 |
| Call Trace | 一串调用栈 | 谁调用到它,根因常常在更下面几层 |
| Modules linked in | 一长串模块名,带PEO标记 | 判断有没有加载树外/未签名模块 |
这四行抓齐,问题基本就定性了一半。下面所有排查都是围绕它们展开。
措辞会随内核版本变,别搜错关键词
这行日志是个"家族",不同年代长得不一样。你搜不到答案,很可能只是搜错了措辞。
| 平台 / 版本 | 实际措辞 |
|---|---|
| v3.10 ~ v4.19(RHEL 6/7 时代) | BUG: unable to handle kernel paging request at <addr>;如果是空指针则是BUG: unable to handle kernel NULL pointer dereference at <addr> |
| v5.4 至今 | 拆成两句:BUG: kernel NULL pointer dereference, address: <addr>和BUG: unable to handle page fault for address: <addr>,后面跟#PF: supervisor write access in kernel mode、#PF: error_code(0x0002) - not-present page |
| ARM64 | Unable to handle kernel paging request at virtual address ffff000018664000,加Mem abort info:、ESR = 0x...、EC = 0x25: DABT (current EL) |
| ARM32 | Unable to handle kernel paging request at virtual address ebfeb0a0,加Internal error: Oops: 80000005 [#1] PREEMPT SMP ARM |
打印这句的代码在arch/x86/mm/fault.c的show_fault_oops()里。x86 的缺页入口是同一个文件里的exc_page_fault(),往下分do_kern_addr_fault()(内核地址空间)和do_user_addr_fault()(用户地址空间)。内核态修复失败后,走kernelmode_fixup_or_oops()进入 oops 流程,最终由kernel/exit.c的make_task_dead()收尾。
顺带一个有用的细节:make_task_dead()里有 oops 计数,超过kernel.oops_limit(默认 10000)会直接panic("Oopsed too often")。反复 oops 本身会继续破坏内存现场,所以取证要趁早。
六个根因,按排查性价比排序
| # | 根因 | 怎么认 | 怎么处置 |
|---|---|---|---|
| 1 | 第三方 / 树外内核模块 | RIP 或 Call Trace 行末尾带[模块名];Tainted:里带P/E/O | 升级或卸载该模块,黑名单验证;找模块厂商 |
| 2 | 内存硬件故障 | Tainted无异常,但随机地址、随机函数崩;EDAC/MCE 有记录 | memtester / memtest86+ 确认后换内存条 |
| 3 | 内核自身 bug | 崩在核心函数(vma_stop、nf_conntrack 清理等),无第三方模块 | 升级到含修复的内核版本 |
| 4 | 内核栈溢出 | 自写驱动,崩在ffffffffffffffff这类地址,RIP 是0xfffffffffffffffe | 把栈上的大结构体改kmalloc() |
| 5 | DMA / IOMMU 问题 | 崩在arch_sync_dma_for_device、__clean_dcache_area_poc这类 DMA 路径 | 查驱动的 DMA 映射;用iommu=off做对照 |
| 6 | 写错地址 / 执行用户态代码 | 自写模块按硬编码地址写sys_call_table;日志有unable to execute userspace code (SMEP?) | 别硬编码内核符号地址(KASLR 会变) |
第 1 类在生产环境里占比最高。Red Hat 知识库对这个报错的结论,十次里有七八次是"你加载了第三方模块"。几个真实案例:
- 一台 HP ProLiant DL380 Gen9 生产机崩溃在
down_read()上,Tainted: P E(专有 + 未签名模块),调用栈全是falcon_lsm_serviceable(一个第三方安全模块)。 - 某 RHEL 7 环境崩溃在
memcpy,栈里全是_ZdlPv [falcon_lsm_serviceable],判定为第三方模块破坏了内存。 - 某虚拟化环境里
veeamdeferio进程崩溃,RIP 落在模块地址,Code:行显示Unable to access opcode bytes(模块代码段已失效),环境变量直接点名veeamsnap驱动。
怎么快速确认是第三方模块干的:
lsmod | grep <模块名> # 它在不在 modinfo <模块名> # 看 license / vermagic cat /proc/sys/kernel/tainted # 内核污染位 sh tools/debugging/kernel-chktaint # 官方解码脚本内核污染位值得记一下:P(1) 专有模块、B(32) 坏页标志、U(64) 用户态请求污染、D(128) 刚 oops 过、W(512) 内核告警、C(1024) staging 驱动、O(4096) 树外模块、E(8192) 未签名模块。
怀疑内存:三条命令从软到硬
如果崩溃地址随机、崩的函数每次都不同、Tainted又很干净,先怀疑内存条。
# 1) 查硬件错误记录(不用重启就能发现坏内存) dmesg | grep -iE "edac|mce|machine check" edac-util -v # edac-utils 包 ras-mc-ctl --summary --errors # rasdaemon 包 mcelog --client # mcelog 包 # 2) 在线压测(能抓到正在使用的坏页,不用停机) memtester 2G 5 # 3) 看内存条位置,准备更换 dmidecode -t memory | grep -iE "Size|Locator|Manufacturer|Serial|Part|Speed"第 2 条最有说服力。Proxmox 论坛有个案例:Intel NUC8 上 KVM 虚机被卡死,内核崩在kvm_intel的vmx_handle_exit(Tainted: P D O)。用户跑memtester 1G 5,第 2 轮 Bit Flip 测试直接报:
Bit Flip : testing 49FAILURE: 0x00000040 != 0x02000040 at offset 0x04fc8910内存故障坐实。宁可跑一次 memtester 花十几分钟,也别在"是内核 bug 还是硬件"之间反复猜。
如果机器可以停机维护,用 U 盘启动 memtest86+,至少完整跑满一轮 pass(最好过夜),记下出错的物理地址交给硬件厂商。想更硬:dmidecode拿到内存条序列号,直接申请更换。
逐步把 RIP 变成"哪一行源码"
有了 RIP 的符号加偏移,就能还原到具体源码行,这一步能省掉大量猜测:
# 内核树自带(需带符号的 vmlinux) scripts/faddr2line vmlinux <symbol>+0x<off>/0x<size> # 或 addr2line -e vmlinux -f -i <addr> # 有 vmcore 时用 crash 工具 crash> bt # 调用栈 crash> sym <地址> # 地址 → 符号 crash> dis -lr <函数> # 反汇编并显示源码行 crash> mod -t # 列出第三方模块及其污染标记没有 vmcore 的时候,确认崩溃函数归属用这招:
grep -n "<函数名>" /boot/System.map-$(uname -r)出于这个目的,生产机建议开 kdump:systemctl status kdump,崩溃后的 vmcore 落在/var/crash/。如果机器一崩就断电、什么日志都没留下,那第一件事是接串口 console 或稳定 kdump,而不是继续排查。
还有一种情况要单独处理:让 oops 直接变成 panic + kdump,避免反复 oops 把现场冲掉:
echo 1 | sudo tee /proc/sys/kernel/panic_on_oops这会让机器在第一次 oops 时就转储,代价是少了一次"自然恢复"的机会,取证场景值得。
几个常见误区
- 一看是
NULL pointer dereference就以为不用管:它和 paging request 是同一个家族,只是地址小于一个页(近空指针)。该走的四要素流程一步都不能省。 Not tainted就断定是内核 bug:ServerFault 上有个典型案例,全新 Ubuntu 服务器每天随机崩一次,崩在kmem_cache_alloc,内核完全没被污染,用户跑 memtester 也没查出错误,帖子至今无解。排除第三方模块只是排除了一条路,不等于答案就是内核 bug,内存控制器层面的问题 memtester 不一定抓得到。- 在 ARM 平台上只看崩溃函数:ARM64 的日志前面往往有
Mem abort info:段,先看EC = 0x25: DABT还是IABT(数据访问还是指令访问),再看这条日志前面一条是哪个驱动打印的,能大幅缩小范围。 - 自写驱动把大结构体放内核栈:内核栈通常只有 8K 或 16K,远小于用户态。有案例是驱动里写了
abc_T abc = {},读写超过 32 字节就越界,RIP 直接变成0xfffffffffffffffe(函数指针被踩)。
今晚就能做的一件事
把你机器的内核污染状态和最近的硬件错误记录查一遍,两条命令:
echo "taint=$(cat /proc/sys/kernel/tainted)"; \ sh tools/debugging/kernel-chktaint 2>/dev/null || cat /proc/sys/kernel/tainted; \ dmesg -T | grep -iE "edac|mce|machine check|unable to handle|Oops" | tail -30taint非 0,尤其是带P/E/O:先去查你加载了哪些第三方模块,这是最常见的元凶,这一步通常就能结案。taint为 0 但日志里有edac/mce记录:走内存硬件路径,跑 memtester,准备换条。- 两样都干净但反复崩:开 kdump 抓 vmcore,把 RIP 用
faddr2line还原成源码行,再去对比内核版本和上游修复。
一个完整排查 walkthrough:三回合定罪
场景:一台云主机在夜里重启了,早上看journalctl -k -b -1有一大段内核日志。
第 1 回合,取四要素。先从日志里摘出地址、RIP、Call Trace、Modules linked in。假设摘出来是这样:
BUG: unable to handle kernel paging request at ffffffffc079ad20 RIP: 0010:0xffffffffc079ad20 Code: Unable to access opcode bytes at RIP 0xffffffffc079acf6. Tainted: G O E地址落在ffffffffc0...这一段,是模块地址区间;RIP 不带符号名,且Code:说读不到 opcode,说明这段代码已经失效(模块被卸载或代码段被回收)。Tainted里的O(树外模块)和E(未签名模块)已经给出了方向。
第 2 回合,查是哪个模块。
lsmod | grep -i <关键字> modinfo <模块名> grep -n "<符号名>" /boot/System.map-$(uname -r)如果反复发生,用黑名单把它摘掉再观察:
echo "blacklist <模块名>" | sudo tee /etc/modprobe.d/blacklist-<模块名>.conf写完这个配置文件,重启机器,看日志里还犯不犯。
第 3 回合,如果摘掉模块还犯,转向内存。查 EDAC/MCE,再跑memtester 2G 5。出现FAILURE或Bit Flip就基本坐实硬件内存故障。
到这里三条路各有了结论:是第三方模块就升级或卸载;是内存就换条;两者都干净就升级内核并抓 vmcore。
分诊表:五分钟决定往哪条路走
| 你观察到的 | 最可能的方向 | 下一步动作 |
|---|---|---|
RIP / Call Trace 末尾带[模块名] | 第三方模块 | modinfo该模块,黑名单验证 |
Tainted含P/E/O | 专有 / 未签名 / 树外模块 | 同上,且别在生产环境保留树外模块 |
| 崩溃地址随机、崩的函数每次都不同 | 内存硬件 | EDAC/MCE → memtester → 换条 |
| 崩在核心函数(调度器、内存管理、网络清理) | 内核自身 bug | 查System.map,升级内核到含修复版本 |
RIP 是0xfffffffffffffffe或地址全f | 栈溢出 / 野函数指针 | 检查自写驱动的大局部变量 |
| 崩在 DMA 相关函数 | DMA / IOMMU | 查驱动映射,iommu=off做对照实验 |
日志里有unable to execute userspace code (SMEP?) | 内核态试图执行用户态代码 | 检查是不是在写sys_call_table这类硬编码地址 |
kdump 落地清单(让下次崩溃有据可查)
一崩就断电、事后什么都查不到,是最难受的情况。按这四条配好,下次崩溃会留下 vmcore:
- [ ]
systemctl enable --now kdump && systemctl status kdump确认服务在跑 - [ ] 预留崩溃转储内存:GRUB 里加
crashkernel=512M(大内存机可加大) - [ ] 崩溃文件落在
/var/crash/,确认分区有足够空间 - [ ] 需要强制取证时临时开
panic_on_oops=1,让第一次 oops 就转储
拿到 vmcore 之后,用crash工具把 RIP 还原成源码行:crash> bt、crash> dis -lr <函数>、crash> mod -t。这一步做完,报障给模块厂商或内核社区时,对方几乎不用反问。
内核源码里这句话是怎么出来的(简读)
想彻底弄明白,看arch/x86/mm/fault.c就够。CPU 触发缺页后进入exc_page_fault(),按地址落在哪片空间分到do_kern_addr_fault()或do_user_addr_fault()。内核态的处理失败,走kernelmode_fixup_or_oops();如果是"坏地址",进__bad_area_nosemaphore()/bad_area_nosemaphore()决定是给进程发 SIGSEGV 还是直接 oops。真正的打印发生在show_fault_oops()里,它把地址、#PF解码、Tainted状态一次打全,再由arch/x86/kernel/dumpstack.c的oops_begin()/oops_end()负责寄存器转储,最后kernel/exit.c的make_task_dead()结束这个进程。
明白了这条链路,你就能反过来读日志:show_fault_oops()打印的那些行,本来就是内核在替你做完"四要素"采集。
别把 "Not tainted" 当免罪符
ServerFault 上那个案例值得记住:一台全新 Ubuntu 服务器每天随机崩一次,崩在kmem_cache_alloc,内核完全没有被污染(Not tainted),用户跑memtester也没查出任何错误,帖子至今没有答案。
所以排查顺序是"先排除能排除的",不是"排除完就一定有答案"。第三方模块、内存硬件、内核 bug 这三条路各走一遍,是为了把可能性收窄到某一条,而不是保证一定落在某一条上。内存控制器层面的偶发问题,在线 memtester 不一定抓得到,得靠 EDAC/MCE 的长期记录,或者用 memtest86+ 离线跑满一轮才能暴露。
如果三条路都走干净了还在犯,把下面这些整理好,去内核社区或发行版厂商报障:
- 完整的那段 oops(含地址、RIP、Call Trace、Modules linked in、Tainted)
uname -r的内核版本,以及lsmod全量输出- 硬件型号、BIOS 版本、内存条信息
- 复现条件(多少并发、多久一次、有没有特定操作)
带上这些,对方几乎不用反问,直接就能对着你的栈去查 commit。
出处
- 内核源码:
arch/x86/mm/fault.c的show_fault_oops()/exc_page_fault()/do_kern_addr_fault()/do_user_addr_fault()/kernelmode_fixup_or_oops();arch/x86/kernel/dumpstack.c的oops_begin()/oops_end();kernel/exit.c的make_task_dead() - kernel.org 文档:
Documentation/admin-guide/tainted-kernels.rst与tools/debugging/kernel-chktaint - Red Hat 知识库:7024709(falcon 模块 down_read)、6193041(falcon 模块 memcpy)、522043(liscal 模块 update_curr)、7024709 / 7087720(veeamsnap)
- Proxmox 论坛 thread 52792:kvm_intel
vmx_handle_exit崩溃,memtester 定位内存故障 - Arch Linux 论坛 thread 250210:休眠唤醒后 soundcore / i915 崩溃,BIOS 关闭 Deep Sleep 后消失
- LKML 2011-03:
fs/proc/task_mmu.c的vma_stop()未检查ERR_PTR,Linus 给出的修复补丁 - Stack Overflow 55004444:内核栈溢出导致
ffffffffffffffff地址崩溃 - Stack Overflow 38195587、ServerFault 582750:内存控制器故障与"至今无解"的悬案
- GitHub espressif/esp-hosted#544:
esp_hosted_ng树外模块的 DMA 路径崩溃
本文首发于 CSDN 专栏《运维漏洞指南》。