在内存相关的实际开发与调试中,内存异常访问是开发者经常遇到的棘手问题。本文旨在结合一个具体的Oops打印日志、T32栈回溯信息以及内核源码,深入剖析其背后的处理流程,帮助读者加深对Linux内核内存异常处理机制的理解,从而提升快速定位和解决此类问题的能力。
一、不同类型异常处理概览
当发生内存访问异常时,ARM处理器的MMU会触发__dabt_svc异常向量,最终交由do_DataAbort函数进行处理。
从_dabt_svc到do_DataAbort的流程并非本文重点,我们直接从do_DataAbort开始分析。该函数的核心逻辑是,根据传入的fsr(Fault Status Register)状态寄存器值,通过fsr_fs()函数计算出索引,从而在fsr_info数组中查找并调用对应的处理函数。
asmlinkage void __exception
do_DataAbort(unsigned long addr, unsigned int fsr, struct pt_regs *regs)
{
const struct fsr_info *inf = fsr_info + fsr_fs(fsr);
struct siginfo info;
if (!inf->fn(addr, fsr & ~FSR_LNX_PF, regs)) // 这里根据fsr从fsr_info中找到对应的操作函数。
return;
...
}
static inline int fsr_fs(unsigned int fsr)
{
return (fsr & FSR_FS3_0) | (fsr & FSR_FS4) >> 6;
}
fsr_info数组列出了ARM架构支持的所有内存访问错误类型,对于Linux内存管理而言,主要关注以下四种:
- Section translation fault(段转换错误)
- Page translation fault(页转换错误)
- Section permission fault(段权限错误)
- Page permission fault(页权限错误)
接下来,我们将通过实例详细分析其中两种权限错误(Section/Page Permission Fault)的处理过程。
二、Section Translation Fault(作为对比)
2.1 栈信息分析
首先,我们看一个Section Translation Fault错误的T32栈回溯实例。
假设fsr值为0x805(二进制100000000101),经过fsr_fs()处理后返回值为5(二进制0101)。因此,inf->fn指向的就是do_translation_fault。
static struct fsr_info fsr_info[] = {
...
{ do_translation_fault, SIGSEGV, SEGV_MAPERR, “section translation fault” },
{ do_bad, SIGBUS, 0, “external abort on linefetch” },
{ do_page_fault, SIGSEGV, SEGV_MAPERR, “page translation fault” },
...
}
其完整的调用栈回溯如下:
__dabt_svc
-> do_DataAbort
-> do_translation_fault
-> do_bad_area
-> __do_kernel_fault
-> die
do_DataAbort根据异常地址、fsr和pt_regs,判断异常发生在内核空间还是用户空间,以及当前CPU模式,并据此选择正确的处理函数。
2.2 入口函数 do_translation_fault
Section Translation Fault类型的错误处理入口是do_translation_fault。
static int __kprobes
do_translation_fault(unsigned long addr, unsigned int fsr,
struct pt_regs *regs)
{
unsigned int index;
pgd_t *pgd, *pgd_k;
pud_t *pud, *pud_k;
pmd_t *pmd, *pmd_k;
if (addr < TASK_SIZE) // TASK_SIZE是用户空间地址的顶部,所以do_page_fault是用户空间处理函数。
return do_page_fault(addr, fsr, regs);
if (user_mode(regs)) // 至此的地址都是内核空间,如果regs显式为用户空间。说明两者冲突,进入bad_area。
goto bad_area;
index = pgd_index(addr);
pgd = cpu_get_pgd() + index;
pgd_k = init_mm.pgd + index;
if (pgd_none(*pgd_k)) // pgd_none()返回0,所以不会进入bad_area。
goto bad_area;
if (!pgd_present(*pgd))
set_pgd(pgd, *pgd_k);
pud = pud_offset(pgd, addr);
pud_k = pud_offset(pdg_k, addr);
if (pud_none(*pud_k)) // pud_none()同样返回0,不会进入bad_area。
goto bad_area;
if (!pud_present(*pud))
set_pud(pud, *pud_k);
pmd = pmd_offset(pud, addr);
pmd_k = pmd_offset(pud_k, addr);
#ifdef CONFIG_ARM_LPAE
index = 0;
#else
index = (addr >> SECTION_SHIFT) & 1;
#endif
if (pmd_none(pmd_k[index])) // 如果此时pmd_k[index]为0,则为异常进入bad_area。
goto bad_area;
copy_pmd(pmd, pmd_k);
return 0;
bad_area:
do_bad_area(addr, fsr, regs);
return 0;
}
如果确认是异常(例如pmd项为空),则进入do_bad_area()处理。该函数根据异常发生时是否处于用户模式(user_mode)进行分流。
void do_bad_area(unsigned long addr, unsigned int fsr, struct pt_regs *regs)
{
struct task_struct *tsk = current;
struct mm_struct *mm = tsk->active_mm;
if (user_mode(regs))
__do_user_fault(tsk, addr, fsr, SIGSEGV, SEGV_MAPERR, regs);
else
__do_kernel_fault(mm, addr, fsr, regs);
}
用户模式处理相对简单,即向当前进程发送SIGSEGV信号。内核模式则交由__do_kernel_fault处理,这正是触发内核Oops并打印详细信息的核心路径。
2.3 内核空间Section Translation Fault处理
__do_kernel_fault的主要工作是打印页表项(PTE)、寄存器状态、栈内容等信息,以协助开发者定位问题根源,其核心是调用die函数。
__do_kernel_fault
-> show_pte // 1. 打印页表信息
-> die
-> __die
-> print_modules // 2. 打印加载的模块
-> __show_regs // 3. 打印寄存器状态
-> dump_mem // 4. 打印栈内存
-> dump_backtrace // 5. 打印调用栈回溯
-> dump_instr // 6. 打印出错指令
-> panic // 7. 内核恐慌
下面是一段典型的Oops打印,我们结合代码对其进行分析:
<1>[153780.197326] Unable to handle kernel paging request at virtual address d8660000 // 0. 错误概述
<1>[153780.204406] pgd = c287c000 // 1. show_pte,当前pgd地址0xc287c000
<1>[153780.207183] [d8660000] *pgd=00000000 // 异常地址0xd8660000和其对应的pgd表项内容0x00000000,问题就出在这里。
<0>[153780.210845] Internal error: Oops: 805 [#1] ARM // 0. die
<4>[153780.215362] Modules linked in: // 2. print_modules
<4>[153780.218475] CPU: 0 Not tainted (3.4.110 #2) // 3. __show_regs 开始
<4>[153780.223083] PC is at __mutex_lock_slowpath+0x34/0xb8
<4>[153780.228118] LR is at dpm_prepare+0x58/0x1d0
<4>[153780.232360] pc : [<c04ad5bc>] lr : [<c01a27a8>] psr: 80000013
<4>[153780.232391] sp : c2d01e58 ip : 00000000 fp : c2cc6800
<4>[153780.243988] r10: c0690bfc r9 : c0690c04 r8 : c3682c68
<4>[153780.249298] r7 : c3682c64 r6 : c2c2c000 r5 : c3682c30 r4 : c3682c64
<4>[153780.255889] r3 : d8660000 r2 : c2d01e5c r1 : 00000000 r0 : c3682c64
<4>[153780.262512] Flags: Nzcv IRQs on FIQs on Mode SVC_32 ISA ARM Segment kernel // Nzcv大写表示置位;IRQ/FIQ都打开;处于SVC_32模式;架构是ARM;处于内核中。
<4>[153780.269866] Control: 10c5383d Table: 2287c059 DAC: 00000015
// ... (此处省略大量寄存器内存dump,即show_extra_register_data的输出)
<0>[153780.261627] Process suspend (pid: 755, stack limit = 0xc2d00268) // 线程名是suspend,pid是755,栈的底部是0xc2d00268
<0>[153780.267730] Stack: (0xc2d01e58 to 0xc2d02000) // 4. dump_mem,栈的顶部通常是8K对齐。
<0>[153780.272155] 1e40: 00000010 c3682c68 // 从栈的sp指针开始dump
// ... (此处省略栈内存具体内容)
<4>[153780.387664] [<c04ad5bc>] (__mutex_lock_slowpath+0x34/0xb8) from [<c01a27a8>] (dpm_prepare+0x58/0x1d0) // 5. dump_backtrace
<4>[153781.396942] [<c01a27a8>] (dpm_prepare+0x58/0x1d0) from [<c01a292c>] (dpm_suspend_start+0xc/0x60)
// ... (此处省略部分调用栈)
<0>[153781.478881] Code: e2808004 e5802008 e58d8004 e58d3008 (e5832000) // 6. dump_instr,出错位置附近的机器码
<4>[153781.485168] —[ end trace 352bcf684b277880 ]— // oops_exit打印信息
<0>[153781.489746] Kernel panic - not syncing: Fatal exception // 7. panic,内核崩溃
__do_kernel_fault首先调用show_pte打印出错的页表项信息,然后就将工作移交给了die。
三、Section Permission Fault(段权限错误)
3.1 核心概念与栈信息
Section Permission Fault(段权限错误)是指CPU访问的虚拟地址对应的段级页表项(PMD,通常对应1MB的段映射)有效存在,但当前访问操作(读/写/执行)的权限与页表项中配置的硬件权限不匹配,从而由MMU触发数据中止异常。这属于权限类访问错误,其核心区别于地址未映射的Translation Fault。
在ARM32架构中,段权限错误对应的FSR值,经fsr_fs()处理后,会匹配到fsr_info中do_page_fault对应的条目,且错误类型为SEGV_ACCERR(权限错误)。
static struct fsr_info fsr_info[] = {
...
{ do_translation_fault, SIGSEGV, SEGV_MAPERR, “section translation fault” },
{ do_page_fault, SIGSEGV, SEGV_ACCERR, “section permission fault” },
{ do_bad, SIGBUS, 0, “external abort on linefetch” },
{ do_page_fault, SIGSEGV, SEGV_MAPERR, “page translation fault” },
{ do_page_fault, SIGSEGV, SEGV_ACCERR, “page permission fault” },
...
}
典型实例:段权限错误的fsr值可能为0x80d。其栈回溯流程与Translation Fault不同,核心处理函数为do_page_fault。
__dabt_svc
-> do_DataAbort
-> do_page_fault
-> do_bad_area
-> __do_user_fault // 用户态异常,发送SIGSEGV信号
-> __do_kernel_fault // 内核态异常,触发Oops
-> die
3.2 入口函数 do_page_fault
Section Permission Fault的处理入口是do_page_fault。它的核心逻辑是查找地址对应的VMA(虚拟内存区域),并校验访问权限。
static int __kprobes
do_page_fault(unsigned long addr, unsigned int fsr, struct pt_regs *regs)
{
struct task_struct *tsk;
struct mm_struct *mm;
struct vm_area_struct *vma;
unsigned int flags;
int fault;
siginfo_t info;
tsk = current;
mm = tsk->mm;
/* 中断上下文或内核线程无mm_struct,直接进入内核异常处理 */
if (in_interrupt() || !mm)
goto no_context;
down_read(&mm->mmap_sem);
vma = find_vma(mm, addr);
if (!vma)
goto bad_area;
if (vma->vm_start <= addr)
goto good_area;
if (!(vma->vm_flags & VM_GROWSDOWN))
goto bad_area;
if (expand_stack(vma, addr))
goto bad_area;
/* 地址对应VMA合法,进入权限校验分支 */
good_area:
info.si_code = SEGV_ACCERR; // 权限错误固定标记为SEGV_ACCERR
flags = 0;
/* 解析FSR中的访问类型,校验VMA权限是否匹配 */
if (fsr & FSR_WRITE) {
flags |= FAULT_FLAG_WRITE;
if (!(vma->vm_flags & VM_WRITE)) // 写访问,但VMA无写权限
goto bad_area;
} else if (fsr & FSR_EXEC) {
flags |= FAULT_FLAG_EXEC;
if (!(vma->vm_flags & VM_EXEC)) // 执行访问,但VMA无执行权限
goto bad_area;
} else {
flags |= FAULT_FLAG_READ;
if (!(vma->vm_flags & VM_READ)) // 读访问,但VMA无读权限
goto bad_area;
}
/* 尝试修复权限异常,如COW写时复制场景 */
fault = handle_mm_fault(mm, vma, addr, flags);
if (unlikely(fault & VM_FAULT_ERROR)) {
if (fault & VM_FAULT_OOM)
goto out_of_memory;
else if (fault & VM_FAULT_SIGBUS)
goto do_sigbus;
BUG();
}
if (fault & VM_FAULT_MAJOR)
tsk->maj_flt++;
else
tsk->min_flt++;
up_read(&mm->mmap_sem);
return 0;
/* 地址无合法VMA或权限不匹配,进入异常处理 */
bad_area:
up_read(&mm->mmap_sem);
bad_area_nosemaphore:
/* 用户态异常直接发送SIGSEGV信号 */
if (user_mode(regs)) {
info.si_signo = SIGSEGV;
info.si_errno = 0;
info.si_addr = (void __user *)addr;
force_sig_info(SIGSEGV, &info, tsk);
return 0;
}
no_context:
/* 内核态异常,进入Oops流程 */
do_bad_area(addr, fsr, regs);
return 0;
... // 省略 out_of_memory 和 do_sigbus 分支
}
代码逻辑解析:
- 上下文判断:中断上下文或内核线程直接进入内核异常处理。
- VMA查找:查找异常地址对应的VMA,找不到或地址不合法则进入
bad_area。
- 权限校验:根据FSR判断是读、写还是执行访问,检查VMA的
vm_flags是否具备相应权限。不匹配则跳转至bad_area。这是触发段权限错误的直接软件原因。
- 尝试修复:调用
handle_mm_fault尝试修复异常(如COW场景)。修复失败则进入错误处理分支。
- 异常收尾:用户态异常发送
SIGSEGV信号;内核态异常进入do_bad_area,最终触发Oops。
3.3 内核空间Section Permission Fault处理
内核态的段权限错误,最终同样会进入__do_kernel_fault -> die的流程。其与Translation Fault的核心区别在于:地址映射存在但权限不匹配,这在show_pte的输出上有明确体现。
以下是典型的Section Permission Fault Oops打印片段:
<1>[23456.789012] Unable to handle kernel paging request at virtual address c0800000
<1>[23456.789500] pgd = c287c000
<1>[23456.789800] [c0800000] *pgd=0080040e, *pmd=0080040e // 关键!PGD和PMD均有有效值,说明映射存在。
<0>[23456.790200] Internal error: Oops: 80d [#1] ARM // FSR为0x80d,对应section permission fault
<4>[23456.790500] Modules linked in:
<4>[23456.790800] CPU: 0 Not tainted (3.4.110 #2)
<4>[23456.791100] PC is at test_write_ro_section+0x10/0x20 // PC指向一个试图写只读段的函数
...
核心差异与根因分析:
- 错误类型:FSR为
0x80d,对应section permission fault。
- show_pte输出:关键!异常地址对应的
*pgd和*pmd均为非零有效值(如0x0080040e),这证明了段级页表映射是存在的,直接排除了Translation Fault。因为是段映射,所以不会打印PTE信息。
- 根因:
pmd值中的AP(访问权限)位被配置为只读,而当前执行的代码(如test_write_ro_section)却试图进行写操作,硬件检测到权限冲突,触发异常。
- 后续的
die、panic流程与前述一致。
四、Page Permission Fault(页权限错误)
4.1 核心概念与栈信息
Page Permission Fault(页权限错误)是Linux系统中最常见的内存权限类异常。它是指CPU访问的虚拟地址对应的页级页表项(PTE,通常对应4KB页映射)有效存在,但PTE中配置的硬件访问权限与当前操作不匹配。
在fsr_info中,页权限错误也由do_page_fault处理,错误类型为SEGV_ACCERR。
{ do_page_fault, SIGSEGV, SEGV_ACCERR, “page permission fault” },
典型栈回溯与段权限错误一致,核心都在于do_page_fault及后续的handle_mm_fault函数。
4.2 处理逻辑与handle_mm_fault
页权限错误与段权限错误共享do_page_fault入口。核心差异在于页表遍历的层级和handle_mm_fault的详细处理。
handle_mm_fault针对页权限错误的核心流程:
- 页表遍历:从PGD -> PUD -> PMD -> PTE,逐级找到异常地址对应的PTE项,确认其存在(排除页转换错误)。
- 权限校验:对比PTE的硬件权限位与当前访问类型,结合VMA逻辑权限,确认是否不匹配。
- 可修复场景处理(不触发Oops):
- 写时复制(COW):子进程写共享的只读页,会分配新物理页并更新PTE权限。
- 按需分配:首次访问mmap的匿名页,会分配物理页并建立映射。
- Swap页换入:访问被换出的页,会从swap分区读回。
- 不可修复场景:如写内核只读数据段(rodata)、执行不可执行(NX)页等,
handle_mm_fault返回错误,最终触发Oops。
4.3 内核空间Page Permission Fault处理
内核态的页权限错误,其Oops打印流程与前两种相同,核心识别点同样在show_pte的输出。
<1>[34567.890123] Unable to handle kernel paging request at virtual address b6f01000
<1>[34567.890600] pgd = c287c000
<1>[34567.890900] [b6f01000] *pgd=3287c067, *pmd=3267e067, *pte=00001075, *ppte=00000000 // 关键!PTE级信息被打印出来,且值有效。
<0>[34567.891300] Internal error: Oops: 807 [#1] ARM // FSR为0x807,对应page permission fault
<4>[34567.891600] Modules linked in:
<4>[34567.891900] CPU: 0 Not tainted (3.4.110 #2)
<4>[34567.892200] PC is at copy_from_user+0x20/0x80 // PC在copy_from_user函数中
...
核心差异与根因分析:
- 错误类型:FSR为
0x807,对应page permission fault。
- show_pte输出:关键!异常地址对应的
*pgd、*pmd和*pte均为非零有效值。这证明了页级映射完整存在。打印出PTE值(如0x00001075)是页级错误区别于段级错误的标志。
- 根因:PTE值中的权限位表明该页可能是用户态只读页。而
copy_from_user函数在内核态试图向该地址写入数据(函数误用,本应是copy_to_user),导致权限违规。
- 此属非法访问,无法修复,最终触发内核Oops和panic。
总结与思考
通过对Section Translation Fault、Section Permission Fault和Page Permission Fault三种典型内存异常的分析,我们可以看到Linux内核严谨的内存管理和错误处理机制。do_DataAbort根据FSR精确分发,do_page_fault负责复杂的权限校验和修复尝试,而__do_kernel_fault和die则提供了最后一道防线,通过详尽的Oops信息为开发者留下宝贵的调试线索。
分析Oops日志时,应重点关注:
- FSR值:识别是转换错误(
MAPERR)还是权限错误(ACCERR)。
- show_pte输出:判断映射是否存在(值非零),以及是段级(无
*pte)还是页级(有*pte)错误。
- PC指针:定位触发异常的代码位置。
- 调用栈:理解异常的触发上下文。
深入理解这些机制,不仅是操作系统内核学习的进阶之梯,更是解决复杂内存相关Crash问题的实战利器。希望本文的解析能为你梳理清ARM Linux内存Oops的分析脉络。如果你对更多底层技术细节感兴趣,欢迎在云栈社区交流探讨。
原文作者:ArnoldLu
原文地址:https://www.cnblogs.com/arnoldlu/p/8672139.html