找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2785

积分

0

好友

375

主题
发表于 5 小时前 | 查看: 3| 回复: 0

在内存相关的实际开发与调试中,内存异常访问是开发者经常遇到的棘手问题。本文旨在结合一个具体的Oops打印日志、T32栈回溯信息以及内核源码,深入剖析其背后的处理流程,帮助读者加深对Linux内核内存异常处理机制的理解,从而提升快速定位和解决此类问题的能力。

一、不同类型异常处理概览

当发生内存访问异常时,ARM处理器的MMU会触发__dabt_svc异常向量,最终交由do_DataAbort函数进行处理。

_dabt_svcdo_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根据异常地址、fsrpt_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_infodo_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 分支
}

代码逻辑解析:

  1. 上下文判断:中断上下文或内核线程直接进入内核异常处理。
  2. VMA查找:查找异常地址对应的VMA,找不到或地址不合法则进入bad_area
  3. 权限校验:根据FSR判断是读、写还是执行访问,检查VMA的vm_flags是否具备相应权限。不匹配则跳转至bad_area。这是触发段权限错误的直接软件原因
  4. 尝试修复:调用handle_mm_fault尝试修复异常(如COW场景)。修复失败则进入错误处理分支。
  5. 异常收尾:用户态异常发送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指向一个试图写只读段的函数
...

核心差异与根因分析:

  1. 错误类型:FSR为0x80d,对应section permission fault
  2. show_pte输出:关键!异常地址对应的*pgd*pmd均为非零有效值(如0x0080040e),这证明了段级页表映射是存在的,直接排除了Translation Fault。因为是段映射,所以不会打印PTE信息。
  3. 根因pmd值中的AP(访问权限)位被配置为只读,而当前执行的代码(如test_write_ro_section)却试图进行写操作,硬件检测到权限冲突,触发异常。
  4. 后续的diepanic流程与前述一致。

四、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针对页权限错误的核心流程:

  1. 页表遍历:从PGD -> PUD -> PMD -> PTE,逐级找到异常地址对应的PTE项,确认其存在(排除页转换错误)。
  2. 权限校验:对比PTE的硬件权限位与当前访问类型,结合VMA逻辑权限,确认是否不匹配。
  3. 可修复场景处理(不触发Oops)
    • 写时复制(COW):子进程写共享的只读页,会分配新物理页并更新PTE权限。
    • 按需分配:首次访问mmap的匿名页,会分配物理页并建立映射。
    • Swap页换入:访问被换出的页,会从swap分区读回。
  4. 不可修复场景:如写内核只读数据段(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函数中
...

核心差异与根因分析:

  1. 错误类型:FSR为0x807,对应page permission fault
  2. show_pte输出:关键!异常地址对应的*pgd*pmd*pte均为非零有效值。这证明了页级映射完整存在。打印出PTE值(如0x00001075)是页级错误区别于段级错误的标志。
  3. 根因:PTE值中的权限位表明该页可能是用户态只读页。而copy_from_user函数在内核态试图向该地址写入数据(函数误用,本应是copy_to_user),导致权限违规。
  4. 此属非法访问,无法修复,最终触发内核Oops和panic。

总结与思考

通过对Section Translation Fault、Section Permission Fault和Page Permission Fault三种典型内存异常的分析,我们可以看到Linux内核严谨的内存管理和错误处理机制。do_DataAbort根据FSR精确分发,do_page_fault负责复杂的权限校验和修复尝试,而__do_kernel_faultdie则提供了最后一道防线,通过详尽的Oops信息为开发者留下宝贵的调试线索。

分析Oops日志时,应重点关注:

  1. FSR值:识别是转换错误(MAPERR)还是权限错误(ACCERR)。
  2. show_pte输出:判断映射是否存在(值非零),以及是段级(无*pte)还是页级(有*pte)错误。
  3. PC指针:定位触发异常的代码位置。
  4. 调用栈:理解异常的触发上下文。

深入理解这些机制,不仅是操作系统内核学习的进阶之梯,更是解决复杂内存相关Crash问题的实战利器。希望本文的解析能为你梳理清ARM Linux内存Oops的分析脉络。如果你对更多底层技术细节感兴趣,欢迎在云栈社区交流探讨。

原文作者:ArnoldLu
原文地址:https://www.cnblogs.com/arnoldlu/p/8672139.html




上一篇:AI算力成本优化新策略:Anthropic为Claude引入分时定价,详解影响与应对
下一篇:Claude Code记忆系统构建指南:打造可复利增长的项目知识库
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-30 06:25 , Processed in 0.513770 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表