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。
第一步:取出四要素
崩溃之后机器往往已经重启,所以要看上一次开机的日志:
bash
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 | 一长串模块名,带 P E O 标记 |
判断有没有加载树外/未签名模块 |
这四行抓齐,问题基本就定性了一半。下面所有排查都是围绕它们展开。
措辞会随内核版本变,别搜错关键词
这行日志是个"家族",不同年代长得不一样。你搜不到答案,很可能只是搜错了措辞。
| 平台 / 版本 | 实际措辞 |
|---|---|
| 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驱动。
怎么快速确认是第三方模块干的:
bash
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 又很干净,先怀疑内存条。
bash
# 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 的符号加偏移,就能还原到具体源码行,这一步能省掉大量猜测:
bash
# 内核树自带(需带符号的 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 的时候,确认崩溃函数归属用这招:
bash
grep -n "<函数名>" /boot/System.map-$(uname -r)
出于这个目的,生产机建议开 kdump:systemctl status kdump,崩溃后的 vmcore 落在 /var/crash/。如果机器一崩就断电、什么日志都没留下,那第一件事是接串口 console 或稳定 kdump,而不是继续排查。
还有一种情况要单独处理:让 oops 直接变成 panic + kdump,避免反复 oops 把现场冲掉:
bash
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(函数指针被踩)。
今晚就能做的一件事
把你机器的内核污染状态和最近的硬件错误记录查一遍,两条命令:
bash
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 -30
taint非 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 回合,查是哪个模块。
bash
lsmod | grep -i <关键字>
modinfo <模块名>
grep -n "<符号名>" /boot/System.map-$(uname -r)
如果反复发生,用黑名单把它摘掉再观察:
bash
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 专栏《运维漏洞指南》。