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下大概分为三类:

  1. 因内部CPU指令执行相关触发
  2. 因外部设备触发
  3. 系统硬件错误

上述第一点官方名称是“同步异常 (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下指令异常具有相同的入口,但是指令异常具有不同的类别。包括:

  1. 指令中止(Instruction Abort):指令本身执行失败。并非是由访存引起。
  2. 数据中止(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页表转换涉及到很多硬件细节,具体开发中需要参照完整硬件手册。本文并不准备全部覆盖这些细节, 但是为了中心内容会在一定程度上展示必要细节。

二 缺页异常处理流程

缺页异常总流程包括两个阶段:

  1. 进入“缺页”类型异常分支;
  2. 分配物理页和创建页表项

如上所述,向量表中有两个可能的缺页异常入口,一是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。忽略。
此步操作后,当前已建立页表如下所示

绘图1

2) 创建PUD表并填充PGD表项
__handle_mm_fault:
调用pud_alloc,传入第一步中的pgd表项指针。包括:

  1. 使用pud_alloc_one,分配一个新的PUD页。得到pud页地址
  2. 使用p4d_populate,将PUD页地址填充至用户申请地址对应的pgd表项
  3. 返回用户地址对应的pud表项指针

此时页表形式如下所示,
绘图2

3) 创建PMD表并填充PUD表项
__handle_mm_fault:
该过程和第2步类同。
调用pmd_alloc, 传入第二步中的pud表项指针。包括:

  1. 使用pmd_alloc_one,分配一个新的PMD页。得到pmd页地址
  2. 使用pud_populate,将PMD页地址填充至用户申请地址对应的pgd表项
  3. 返回用户地址对应的pmd表项指针

此时页表形式如下所示,
绘图3

4) 创建pte表填充PMD表项并填充PTE项
do_anonymous_page函数。

  1. 调用pte_alloc(位置1):
    1. 调用pte_alloc_one,分配一个新的PTE页。得到PTE页地址
    2. 使用pmd_populate,将上述地址填充至用户申请地址对应的pmd表项。
  2. 调用alloc_zeroed_user_highpage_movable,分配实际数据物理页。得到物理页地址
  3. 调用set_pte_at(位置2)将物理页地址填充至用户地址对应的PTE表项

至此。包括四级页表的缺页异常分配全部完成。此时,页表分配如下所示。
绘图4

三 附录

虚拟地址有效位数和虚拟地址分段

虚拟地址有效位数是说系统使用的虚拟地址位数,虽然地址最大值是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

posted @ 2026-07-05 12:43  happysc  阅读(40)  评论(0)    收藏  举报