linux缺页异常机制
目标
分析linux内核的缺页异常机制。linux异常处理、内存管理相关。学习和交流。
代码分析环境:
- arm64架构
- linux 5.10
分为以下几部分
- 一 总体
- 缺页异常概述
- 硬件基础1: arm64中断异常和向量表
- 硬件基础2: arm64下指令异常分类
- 硬件基础3: MMU内存地址映射转换
- 二 缺页异常流程
- [流程1]进入缺页类型异常分支
- [流程2]分配物理页和创建页表项
- 三 附录
- 虚拟地址有效位数和虚拟地址分段
- 异常处理相关寄存器
一 总体
缺页异常概述
linux内核中缺页异常,实际上是借助于“缺页”类型的系统指令异常实现一种内存延迟分配机制,只在必要时分配实际物理内存。“必要”是指有实际访问操作时进行分配,或者说在首次次使用时进行分配,后续使用时因为已经完成了分配不会再触发缺页操作。其根本目的在于避免不必要的实际内存消耗,提升总体内存使用率。这对服务器和桌面应用场景具有重要意义。
但是注意,缺页异常本身具有一定开销,因为CPU为实际分配内存,需要完成一次指令异常响应,这会造成首次访问时的开销、抖动。因此,在嵌入式实时场景往往不引入该机制,物理内存往往一次性完成直接分配。
同时注意,缺页异常的分配只是linux分配的一种类型。是不是所有linux的内存都是通过缺页机制分配的?当然不是。linux内核自身的数据结构都是直接分配的,不存在什么“延迟”。
linux缺页异常的实现基础是arm64硬件的指令异常(同步异常)管理机制。在CPU核心正确初始化后,该CPU核心可响应由内存访问引起的异常,并进一步由linux内核实现完整的缺页分配处理。
硬件基础1: arm64中断异常和向量表
中断异常,arm64架构下官方称为Exception,但为表达更清晰本文仍使用“中断异常”的名称。中断异常本身是一种硬件机制。和名字表达的含义不一样,它并不是说系统进入一种异常状态,而是强调在出现一种异常状态后,系统内核能“妥善”处理,否则,内核进入卡死状态。中断异常要能正常工作,需要操作系统配合进行针对CPU核心的初始化操作。如果操作系统没有正确配置,一个触发的异常会真的异常了,系统进入崩溃状态。
中断异常本质上是一种给到CPU核心的硬件信号,强制引起CPU响应。它的触发条件,或是类型,在arm64下大概分为三类:
- 因内部CPU指令执行相关触发
- 因外部设备触发
- 系统硬件错误
上述第一点官方名称是“同步异常 (synchronous exception)”,因为它和指令执行相关的,所以叫同步。本身中称为“指令异常”,意思是由指令引起的异常。包括访问内存引起的异常(因为访问内存也是执行指令);第二点官方称为“异步异常”(asynchronous exception)。就是中断,因为外部设别中断信号什么时候来,CPU是不确定的,和CPU执行是异步的。所以叫异步中断。为更利于描述更简介,本文中直接称为“中断”;上述第三点较为特别,暂时不讨论。
系统内核正常响应中断异常的关键软件设置,就是“中断异常向量表”后文简称“向量表”。向量表,对指令异常和中断都同一处理,即同一个框架处理,所以只有一张向量表,没有两张。缺页异常,实际上是一种类型的指令异常(本文只讨论指令异常,中断处理相关的部分在其它文章中单独讨论)。也就是说,linux内核中缺页异常实际处理逻辑的入口就是向量表的指令异常(同步异常)响应部分。
中断向量表是每CPU配置的,在多核CPU上需要在每个核心进行独立配置。中断向量表控制关键寄存器是VBAR_EL1(Vector Base Address Register, 向量表基地址寄存器)。 当发生中断异常时,处理器执行流跳转至该寄存器指向的地址。在CPU引导时,该寄存器被填入由代码设定的向量表地址。通常,该过程属于由汇编语言执行的引导阶段。
linux中断向量表结构具体如下所示,
中断向量表 @arch/arm64/kernel/entry.S
.pushsection ".entry.text", "ax"
.align 11
SYM_CODE_START(vectors)
kernel_ventry 1, sync_invalid // Synchronous EL1t
kernel_ventry 1, irq_invalid // IRQ EL1t
kernel_ventry 1, fiq_invalid // FIQ EL1t
kernel_ventry 1, error_invalid // Error EL1t
kernel_ventry 1, sync // Synchronous EL1h
kernel_ventry 1, irq // IRQ EL1h
kernel_ventry 1, fiq_invalid // FIQ EL1h
kernel_ventry 1, error // Error EL1h
kernel_ventry 0, sync // Synchronous 64-bit EL0
kernel_ventry 0, irq // IRQ 64-bit EL0
kernel_ventry 0, fiq_invalid // FIQ 64-bit EL0
kernel_ventry 0, error // Error 64-bit EL0
kernel_ventry 0, sync_invalid, 32 // Synchronous 32-bit EL0
kernel_ventry 0, irq_invalid, 32 // IRQ 32-bit EL0
kernel_ventry 0, fiq_invalid, 32 // FIQ 32-bit EL0
kernel_ventry 0, error_invalid, 32 // Error 32-bit EL0
SYM_CODE_END(vectors)
向量表的作用,简单说就是在CPU发生中断或异常时,在硬件捕获该信号导入目标特权级代码执行时,由目标特权极的向量表决定中断和异常执行处理。当linux内核设置了EL1级别向量表,当CPU产生一个异常时,无论是在EL0还是在EL1,都会最终由硬件控制导入EL1的向量表。
向量表基本格式有如下说明:
- arm64下每个CPU核心的三个异常级别EL1 、El2、El3,各有一张向量表。也就是说,每个CPU核心实际上有三个向量表,分别由vbar_el1, vbar_el2, vbar_el3三个系统寄存器指定基地址。操作系统位于EL1, 由 vbar_el1指定。
- 中断向量表是数组结构。总共有 2048 个字节,平均划分为 4 个段(子表),每个段4个条目。每个条目 128 个字节。
- arm64下中断向量表内存储的直接是执行代码,而不是跳转地址值。上述每个条目 128个字节,可存储最大32条指令(每个汇编指令4个字节)
向量表最重要的是,能区分发生中断或异常时的CPU现场特权级。对于Linux来说,需区分是发生在EL0还是EL1,换句话说,它要能知道是用户态程序异常,还是内核异常。
向量表具体基于硬件机制和表结构设计完成上述功能,4个段中段1和段2表示从同级别进入的中断或异常,段3和段4表表示从更低级别进入的异常。
一个示例,通常段1和段4是不使用的,内核在段2设置的执行入口即是内核自身的中断或异常,在段3就是来自于用户态的中断或异常。
在每一个段中进一步分为4项,如代码注释所示,第一项即为异常处理入口,第二项为IRQ类型中断处理入口,第三项为FIQ类型中断处理入口,第四项为Serror中断处理入口
linux缺页异常的入口可能在1) 向量表段3的第一项,用户态异常总入口;同时在2) 段2的第一项,内核态异常总入口。
具体当发生缺页异常时,如果是由用户态触发的,进入el0_sync宏,如果是内核态触发的,进入el1_sync宏。下面继续分析由向量表进入后的缺页异常处理流程。
硬件基础2: 指令异常分类
尽管arm64下指令异常具有相同的入口,但是指令异常具有不同的类别。包括:
- 指令中止(Instruction Abort):指令本身执行失败。并非是由访存引起。
- 数据中止(Data Abort): 由指令执行的访存过程,遇到了问题,访存失败。尽管访问内存也是因为指令执行,但导致异常的根本原因是内存访问。
本文讨论的缺页异常属于数据中止类的异常。
硬件基础3: MMU内存地址映射转换
即通常说的MMU,或内存管理单元。简单说,内存是有地址的。但是arm64管理下,内存至少有两套地址: 虚拟地址(线性地址)、物理地址。用户程序使用虚拟地址,内核通常使用物理地址。MMU是硬件机制,如果不启用,操作系统仅能使用物理地址;一旦启用,即开启从基于虚拟地址到物理地址的硬件自动转换。
不单独直接使用一套物理地址的直接原因是,安全隔离。因为Linux需要调度很多进程,各进程的行为是没法预期的,可能有破坏性行为,例如踩踏其它进程或系统内核的内存区域。此时通过虚拟地址提供了一层保护, 内核决定了用户态地址映射关系,不会允许用户进程访问不安全区域,这样提供了稳定。
开启虚拟地址到物理地址转换的另一方面重要原因是,arm64硬件CPU告诉缓存机制依赖于MMU开启。只有开启MMU,硬件cache才生效。而硬件cache使CPU能够全速工作,否则CPU处于一种相当低速处理能力严重降低的状态。
一句话。生产用操作系统没有不开MMU的,没有不启用虚拟地址→物理地址转换的。
MMU作用的关键就是页转换表(页表)。一个进程的页表有多级多页。该机制的基本工作原理是,把用户虚拟地址按位数切分为多个“段”,和多级页表。当用户程序尝试访问内存时,硬件自动对用户虚拟地址进行“分段解析”。每一段的数值当作是这一级页表的索引值。总体页表包括多个页目录表 + 最后1级页表。
某一级页表项的值指向下一级页表基地址。当索引到最后一级时可直接获取物理页地址,再加上偏移构成最后物理地址结果。硬件基于最后的物理地址值进行实际访存。
需要说明,整个MMU的实际过程还全程涉及到缓存作用。包括
- TLB页表缓存。
- Cache数据缓存。
因为页表转换过程是Table Walk多级串联,有开销的,ARM64加入TLB,在缓存命中情况下可以直接转换,降低页表转换的处理开销;同时,内存访问本身也是有开销的,所以最后不一定是做物理页访问,在CPU高速缓存中可能直接完成了读写。(该过程本文只提及,不具体讨论)
arm64下页表项固定为64位8字节,因此,一个页表包括512个表项。当表项不够表达虚拟地址映射关系时,需要扩充页表数量。
MMU页表转换涉及到很多硬件细节,具体开发中需要参照完整硬件手册。本文并不准备全部覆盖这些细节, 但是为了中心内容会在一定程度上展示必要细节。
二 缺页异常处理流程
缺页异常总流程包括两个阶段:
- 进入“缺页”类型异常分支;
- 分配物理页和创建页表项
如上所述,向量表中有两个可能的缺页异常入口,一是el0_sync用户态触发进入;而是el1_sync内核态触发进入。用户态程序分配是缺页异常的主要场景,内核并不常见。以下以用户态触发缺页异常来做分析。
[流程1] 进入缺页类型异常分支
从el0_sync以后进入异常处理分支的主要流程有
el0_sync [arch/arm64/kernel/entry.S]
└─ el0_sync_handler [arch/arm64/kernel/entry-common.c]
└─ el0_da []
└─ do_mem_abort [arch/arm64/mm/fault.c]
└─ do_translation_fault
└─ do_page_fault
el0_sync_handler @arch/arm64/kernel/entry-common.c 局部代码有
asmlinkage void noinstr el0_sync_handler(struct pt_regs *regs)
{
unsigned long esr = read_sysreg(esr_el1);
switch (ESR_ELx_EC(esr)) {
case ESR_ELx_EC_SVC64:
el0_svc(regs);
break;
case ESR_ELx_EC_DABT_LOW:
el0_da(regs, esr);
break;
case ESR_ELx_EC_IABT_LOW:
el0_ia(regs, esr);
break;
case ESR_ELx_EC_FP_ASIMD:
el0_fpsimd_acc(regs, esr);
break;
case ESR_ELx_EC_SVE:
el0_sve_acc(regs, esr);
break;
// ...
}
上述,el0_sync_handler 主要通过读取ESR_EL1寄存器(具体参见附录)判断发生异常是属于“指令中止”类型,还是“数据中止”类型。显然缺页异常是数据中止。因此走el0_da(da的意思是data abort)。
el0_da @arch/arm64/kernel/entry-common.c
static void noinstr el0_da(struct pt_regs *regs, unsigned long esr)
{
unsigned long far = read_sysreg(far_el1);
enter_from_user_mode();
local_daif_restore(DAIF_PROCCTX);
far = untagged_addr(far);
do_mem_abort(far, esr, regs);
}
上述主要通过FAR_EL1寄存器读取发生异常时的用户虚拟地址。传入do_mem_abort。
do_mem_abort @arch/arm64/mm/fault.c
void do_mem_abort(unsigned long addr, unsigned int esr, struct pt_regs *regs)
{
const struct fault_info *inf = esr_to_fault_info(esr);
if (!inf->fn(addr, esr, regs))
return;
if (!user_mode(regs)) {
pr_alert("Unhandled fault at 0x%016lx\n", addr);
mem_abort_decode(esr);
show_pte(addr);
}
// ... ...
}
do_mem_abort通过esr_to_fault_info获取该中止类型异常的ESR寄存器ISS段的FSC值(参见附录),该值确定fault_info数据结构。对于用户态缺页异常,该值对应0x5.通过已注册函数指针inf->fn,实际调用do_translation_fault。
fault_info相关定义如下所示
@arch/arm64/mm/fault.c
struct fault_info {
int (*fn)(unsigned long addr, unsigned int esr,
struct pt_regs *regs);
int sig;
int code;
const char *name;
};
static const struct fault_info fault_info[] = {
{ do_bad, SIGKILL, SI_KERNEL, "ttbr address size fault" },
{ do_bad, SIGKILL, SI_KERNEL, "level 1 address size fault" },
{ do_bad, SIGKILL, SI_KERNEL, "level 2 address size fault" },
{ do_bad, SIGKILL, SI_KERNEL, "level 3 address size fault" },
{ do_translation_fault, SIGSEGV, SEGV_MAPERR, "level 0 translation fault" },
{ do_translation_fault, SIGSEGV, SEGV_MAPERR, "level 1 translation fault" },
{ do_translation_fault, SIGSEGV, SEGV_MAPERR, "level 2 translation fault" },
{ do_translation_fault, SIGSEGV, SEGV_MAPERR, "level 3 translation fault" },
{ do_bad, SIGKILL, SI_KERNEL, "unknown 8" },
{ do_page_fault, SIGSEGV, SEGV_ACCERR, "level 1 access flag fault" },
{ do_page_fault, SIGSEGV, SEGV_ACCERR, "level 2 access flag fault" },
{ do_page_fault, SIGSEGV, SEGV_ACCERR, "level 3 access flag fault" },
{ do_bad, SIGKILL, SI_KERNEL, "unknown 12" },
{ do_page_fault, SIGSEGV, SEGV_ACCERR, "level 1 permission fault" },
{ do_page_fault, SIGSEGV, SEGV_ACCERR, "level 2 permission fault" },
{ do_page_fault, SIGSEGV, SEGV_ACCERR, "level 3 permission fault" },
{ do_sea, SIGBUS, BUS_OBJERR, "synchronous external abort" },
{ do_tag_check_fault, SIGSEGV, SEGV_MTESERR, "synchronous tag check fault" },
// ...
};
do_translation_fault @arch/arm64/mm/fault.c
static int __kprobes do_translation_fault(unsigned long addr,
unsigned int esr,
struct pt_regs *regs)
{
if (is_ttbr0_addr(addr))
return do_page_fault(addr, esr, regs);
do_bad_area(addr, esr, regs);
return 0;
}
do_translation_fault通过is_ttbr0_addr判别是用户态地址发生异常,调用do_page_fault。
此后进入分配物理页和创建页表项目流程。
[流程2] 分配物理页和创建页表项
do_page_fault之后的流程有
do_page_fault [arch/arm64/mm/fault.c]
└─ __do_page_fault
└─ handle_mm_fault [mm/memory.c]
└─ __handle_mm_fault
└─ handle_pte_fault
└─ do_anonymous_page
__handle_mm_fault完成pte之前的多级页表创建。在详细描述流程之前介绍页表基础。
关于页表,即内存页地址转换表,完成虚拟地址到物理地址的转换(转换硬件机制暂略)。首先明确,操作系统不止一套页表。 linux内核自身持有一套页表。每个进程各持有一套页表。页表分多级,arm64下最多4级。每一级对应至少一个物理页面,可能不止一个物理页面,更高级别的页表可能包含更多页面。
linux对4级页表有固定的代码标识,分别为:
- pgd页目录表
- pud上级页目录表
- pmd中间页目录表
- pte页表。
上述pgd页目录表地址,对于进程,直接由TTBR0_EL1寄存器指定。pte页表即为指向最终实际数据的页表,它的值表示物理页地址。因此可以认为,前三级页表都是索引。
linux页表有以下关键数据结构
@arch/arm64/include/asm/pgtable-types.h
typedef u64 pteval_t;
typedef u64 pmdval_t;
typedef u64 pudval_t;
typedef u64 p4dval_t;
typedef u64 pgdval_t;
typedef struct { pteval_t pte; } pte_t;
typedef struct { pmdval_t pmd; } pmd_t;
typedef struct { pudval_t pud; } pud_t;
typedef struct { pgdval_t pgd; } pgd_t;
上述pte_t, pmd_t, pud_t, pgd_t分别表示四级页表的某个页表项。
__handle_mm_fault @mm/memory.c 重点局部代码如下
static vm_fault_t __handle_mm_fault(struct vm_area_struct *vma,
unsigned long address, unsigned int flags)
{
struct vm_fault vmf = {
.vma = vma,
.address = address & PAGE_MASK,
.flags = flags,
.pgoff = linear_page_index(vma, address),
.gfp_mask = __get_fault_gfp_mask(vma),
};
unsigned int dirty = flags & FAULT_FLAG_WRITE;
struct mm_struct *mm = vma->vm_mm;
pgd_t *pgd;
p4d_t *p4d;
vm_fault_t ret;
pgd = pgd_offset(mm, address); // --> 位置1
p4d = p4d_alloc(mm, pgd, address);
if (!p4d)
return VM_FAULT_OOM;
vmf.pud = pud_alloc(mm, p4d, address); // --> 位置2
if (!vmf.pud)
return VM_FAULT_OOM;
retry_pud:
if (pud_none(*vmf.pud) && __transparent_hugepage_enabled(vma)) {
ret = create_huge_pud(&vmf);
if (!(ret & VM_FAULT_FALLBACK))
return ret;
} else {
pud_t orig_pud = *vmf.pud;
barrier();
if (pud_trans_huge(orig_pud) || pud_devmap(orig_pud)) {
/* NUMA case for anonymous PUDs would go here */
if (dirty && !pud_write(orig_pud)) {
ret = wp_huge_pud(&vmf, orig_pud);
if (!(ret & VM_FAULT_FALLBACK))
return ret;
} else {
huge_pud_set_accessed(&vmf, orig_pud);
return 0;
}
}
}
vmf.pmd = pmd_alloc(mm, vmf.pud, address); // --> 位置3
if (!vmf.pmd)
return VM_FAULT_OOM;
/* Huge pud page fault raced with pmd_alloc? */
if (pud_trans_unstable(vmf.pud))
goto retry_pud;
// ...
return handle_pte_fault(&vmf); // 位置4
}
__handle_mm_fault后续调用handle_pte_fault→do_anonymous_page
do_anonymous_page @mm/memory.c 重点代码如下
static vm_fault_t do_anonymous_page(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
struct page *page;
vm_fault_t ret = 0;
pte_t entry;
/* File mapping without ->vm_ops ? */
if (vma->vm_flags & VM_SHARED)
return VM_FAULT_SIGBUS;
// 位置1
if (pte_alloc(vma->vm_mm, vmf->pmd))
return VM_FAULT_OOM;
// ....
/* Allocate our own private page. */
if (unlikely(anon_vma_prepare(vma)))
goto oom;
page = alloc_zeroed_user_highpage_movable(vma, vmf->address); // 位置2
if (!page)
goto oom;
if (mem_cgroup_charge(page, vma->vm_mm, GFP_KERNEL))
goto oom_free_page;
cgroup_throttle_swaprate(page, GFP_KERNEL);
__SetPageUptodate(page);
entry = mk_pte(page, vma->vm_page_prot);
entry = pte_sw_mkyoung(entry);
if (vma->vm_flags & VM_WRITE)
entry = pte_mkwrite(pte_mkdirty(entry));
// ...
inc_mm_counter_fast(vma->vm_mm, MM_ANONPAGES);
page_add_new_anon_rmap(page, vma, vmf->address, false);
lru_cache_add_inactive_or_unevictable(page, vma);
setpte:
// 位置3
set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry);
/* No need to invalidate - it was non-present before */
update_mmu_cache(vma, vmf->address, vmf->pte);
unlock:
pte_unmap_unlock(vmf->pte, vmf->ptl);
return ret;
release:
put_page(page);
goto unlock;
oom_free_page:
put_page(page);
oom:
return VM_FAULT_OOM;
}
根据上述关键代码的多级页表创建过程描述如下
1) 获得用户态地址对应的pgd表项指针
函数__handle_mm_fault
由 pgd = pgd_offset(mm, address);
pgd表为第一级L1页表。该页表通常只有一页,在进程运行时已创建,无需在缺页异常创建。
pgd_offset的实现如下
/*
* a shortcut to get a pgd_t in a given mm
*/
#ifndef pgd_offset
#define pgd_offset(mm, address) pgd_offset_pgd((mm)->pgd, (address))
#endif
static inline pgd_t *pgd_offset_pgd(pgd_t *pgd, unsigned long address)
{
return (pgd + pgd_index(address));
};
基于进程页目录表基地址和虚拟地址,确定页目录表项。
对于arm64四级页表,p4d_alloc是一个空操作。p4d=pgd。忽略。
此步操作后,当前已建立页表如下所示

2) 创建PUD表并填充PGD表项
__handle_mm_fault:
调用pud_alloc,传入第一步中的pgd表项指针。包括:
- 使用pud_alloc_one,分配一个新的PUD页。得到pud页地址
- 使用p4d_populate,将PUD页地址填充至用户申请地址对应的pgd表项
- 返回用户地址对应的pud表项指针
此时页表形式如下所示,

3) 创建PMD表并填充PUD表项
__handle_mm_fault:
该过程和第2步类同。
调用pmd_alloc, 传入第二步中的pud表项指针。包括:
- 使用pmd_alloc_one,分配一个新的PMD页。得到pmd页地址
- 使用pud_populate,将PMD页地址填充至用户申请地址对应的pgd表项
- 返回用户地址对应的pmd表项指针
此时页表形式如下所示,

4) 创建pte表填充PMD表项并填充PTE项
do_anonymous_page函数。
- 调用pte_alloc(位置1):
- 调用pte_alloc_one,分配一个新的PTE页。得到PTE页地址
- 使用pmd_populate,将上述地址填充至用户申请地址对应的pmd表项。
- 调用alloc_zeroed_user_highpage_movable,分配实际数据物理页。得到物理页地址
- 调用set_pte_at(位置2)将物理页地址填充至用户地址对应的PTE表项
至此。包括四级页表的缺页异常分配全部完成。此时,页表分配如下所示。

三 附录
虚拟地址有效位数和虚拟地址分段
虚拟地址有效位数是说系统使用的虚拟地址位数,虽然地址最大值是64位,但是系统一般用不了这么多。实际中一般是39或48。
Linux中虚拟地址有效位数由CONFIG_ARM64_VA_BITS_39或CONFIG_ARM64_VA_BITS_48设定。该设置值通过写入TCR_EL1寄存器生效。
虚拟地址有效位数和MMU转换时解析分段有关系。有如下规则:
- 当VA_BITS=39时,只有三级页表,pgd、pmd、pte,虚拟地址解析分段为9:9:9:12,即虚拟地址从高位起第一段9位用于索引pgd表、第二段9位用于索引pmd表、第三段9位用于索引pte表物理、最后12位用于数据偏移
- 当VA_BITS=48时,虚拟地址解析分段为9:9:9:9:12。
异常处理相关寄存器
ESR_EL1(Exception Syndrome Register)
- EL1异常征状寄存器
- 指示导向EL1级别异常发生的原因。
- 因为异常有很多种,OS需要通过该寄存器值判别具体异常类型。
该寄存器64位,主要段意义如下表
| 起始bit:长度bit | 字段标识 | 含义 |
|---|---|---|
| 0:25 | ISS | 指令细分征状:指示大类型下的细分类型 |
| 26:6 | EC | 指令类型征状:指示异常大类型 |
EC编码有如下
| EC值 | 意义 | 说明 |
|---|---|---|
| 0x1 | WFI或WFE指令自陷 | |
| 0x18 | MSR或MRS引起的指令自陷 | 注意不是数据中止 |
| 0x24 | 数据中止data abort | 即data abort数据中止 |
ISS值根据EC值不同具有不同的编码。
进一步的ISS段值意义如下。
在EC=0x24即为数据中止异常时,ISS编码的DFSC(Data Fault Status Code)[5:0]段编码如下。
| DFSC值 | 意义 | 说明 |
|---|---|---|
| 0x0 | 地址大小错误,0级 | |
| 0x1 | 地址大小错误,1级 | |
| 0x4 | 地址转换错误,0级 | |
| 0x5 | 地址转换错误,1级 | |
| 0x4 | 地址转换错误,0级 | |
| 0x5 | 地址转换错误,1级 | |
| 0x13 | 权限错误,0级 | |
| 0x14 | 权限错误,1级 | |
| 上述级别表示,MMU转换的级别,最大4级,从0到3。 |
FAR_EL1: (Fault Address Register)
- EL1 故障地址寄存器
- 指示异常发生的地址,即虚拟地址。
- 该寄存器全位表示一个值,无分段。
TCR_EL1:
该寄存器决定虚拟地址有效位数, 即Linux的VA_BITS

浙公网安备 33010602011771号