【u-boot】u-boot中异常向量表(ARM64)

内容分享3周前发布
0 0 0

一、异常向量表

从ARM64的u-boot启动文件start.S中,我们可以知道u-boot启动早期会设置异常向量表


/*
 * Could be EL3/EL2/EL1, Initial State:
 * Little Endian, MMU Disabled, i/dCache Disabled
 */
adr	x0, vectors     /* 把当前代码段中名为vectors的符号的地址(PC相对算出来的地址)放入x0 */
/* 读取CurrentEL(当前异常级别)并根据结果跳到对应的标签(3f表示EL3分支,2f表示EL2,1f表示EL1) */
switch_el x1, 3f, 2f, 1f 
3:	msr	vbar_el3, x0
	mrs	x0, scr_el3
	orr	x0, x0, #0xf			/* SCR_EL3.NS|IRQ|FIQ|EA */
	msr	scr_el3, x0
	msr	cptr_el3, xzr			/* Enable FP/SIMD */
#ifdef COUNTER_FREQUENCY
	ldr	x0, =COUNTER_FREQUENCY
	msr	cntfrq_el0, x0			/* Initialize CNTFRQ */
#endif
	b	0f
2:	msr	vbar_el2, x0
	mov	x0, #0x33ff
	msr	cptr_el2, x0			/* Enable FP/SIMD */
	b	0f
1:	msr	vbar_el1, x0
	mov	x0, #3 << 20
	msr	cpacr_el1, x0			/* Enable FP/SIMD */
0:

ARM64有专用的寄存器用于存储异常向量表基地址:
**VBAR_ELx**
(Vector Base Address Register for Exception Level x**),**用于存放异常向量表的基地址。AArch64定义了四个(实际上常用的是EL1、EL2、EL3):


VBAR_EL1
:用于 EL1(内核/操作系统)
VBAR_EL2
:用于 EL2(Hypervisor/虚拟化层)
VBAR_EL3
:用于 EL3(Secure Monitor/TrustZone Monitor)
VBAR_EL0
不存在(因为 EL0 = 用户态,不能直接接收异常向量)

从上述代码可知,u-boot根据不同运行级别,会将异常向量表
vectors
设置到对应运行级别的
VBAR_ELx

vectors
实现在arch/arm/cpu/armv8/exceptions.S中:


	.align	11
	.globl	vectors
vectors:
	.align	7		/* Current EL Synchronous Thread */
	stp	x29, x30, [sp, #-16]!
	bl	_exception_entry
	bl	do_bad_sync
	b	exception_exit

	.align	7		/* Current EL IRQ Thread */
	stp	x29, x30, [sp, #-16]!
	bl	_exception_entry
	bl	do_bad_irq
	b	exception_exit

	.align	7		/* Current EL FIQ Thread */
	stp	x29, x30, [sp, #-16]!
	bl	_exception_entry
	bl	do_bad_fiq
	b	exception_exit

	.align	7		/* Current EL Error Thread */
	stp	x29, x30, [sp, #-16]!
	bl	_exception_entry
	bl	do_bad_error
	b	exception_exit

	.align	7		 /* Current EL Synchronous Handler */
	stp	x29, x30, [sp, #-16]!
	bl	_exception_entry
	bl	do_sync
	b	exception_exit

	.align	7		 /* Current EL IRQ Handler */
	stp	x29, x30, [sp, #-16]!
	bl	_exception_entry
	bl	do_irq
	b	exception_exit

	.align	7		 /* Current EL FIQ Handler */
	stp	x29, x30, [sp, #-16]!
	bl	_exception_entry
	bl	do_fiq
	b	exception_exit

	.align	7		 /* Current EL Error Handler */
	stp	x29, x30, [sp, #-16]!
	bl	_exception_entry
	bl	do_error
	b	exception_exit

上述汇编代码则是u-boot的异常向量表实现,其中每个条目汇编指令几乎是一样的:


align 7
:对齐到128字节 (2^7)。,ARM64 的异常向量表要求每个入口(同步/IRQ/FIQ/SError)占用128字节,不能重叠。所以这里每个handler前都
.align 7
,保证落在正确位置。从u-boot的汇编代码可以清除看见每一个异常条目的位置:

【u-boot】u-boot中异常向量表(ARM64)


stp x29, x30, [sp, #-16]!
:保存当前帧指针 (x29) 和返回地址 (x30/LR) 到栈上。


sp = sp - 16
,然后把这两个寄存器压栈。


bl _exception_entry
:跳到通用的异常入口函数,通常做保存寄存器上下文的工作。
bl do_bad_sync
:调用具体的异常处理函数,比如这里是 “bad sync”(未定义或错误的同步异常)。
b exception_exit
:最终跳到统一的
exception_exit
,在那儿会恢复寄存器、执行
eret
返回。

二、异常进入处理

异常进入处理由
_exception_entry
实现, 异常保存现场的入口函数,用于保存寄存器状态,保证进入异常处理函数时用户寄存器和关键信息不会丢失 ,
_exception_entry
实现(arch/arm/cpu/armv8/exceptions.S)如下 :


/*
 * Enter Exception.
 * This will save the processor state that is ELR/X0~X30
 * to the stack frame.
 */
_exception_entry:
	stp	x27, x28, [sp, #-16]!
	stp	x25, x26, [sp, #-16]!
	stp	x23, x24, [sp, #-16]!
	stp	x21, x22, [sp, #-16]!
	stp	x19, x20, [sp, #-16]!
	stp	x17, x18, [sp, #-16]!
	stp	x15, x16, [sp, #-16]!
	stp	x13, x14, [sp, #-16]!
	stp	x11, x12, [sp, #-16]!
	stp	x9, x10, [sp, #-16]!
	stp	x7, x8, [sp, #-16]!
	stp	x5, x6, [sp, #-16]!
	stp	x3, x4, [sp, #-16]!
	stp	x1, x2, [sp, #-16]!

	/* Could be running at EL3/EL2/EL1 */
	switch_el x11, 3f, 2f, 1f
3:	mrs	x1, esr_el3
	mrs	x2, elr_el3
	mrs	x3, daif
	mrs	x4, vbar_el3
	mrs	x5, spsr_el3
	sub	x6, sp, #(8*30)
	mrs	x7, sctlr_el3
	mrs	x8, scr_el3
	mrs	x9, ttbr0_el3
	b	0f
2:	mrs	x1, esr_el2
	mrs	x2, elr_el2
	mrs	x3, daif
	mrs	x4, vbar_el2
	mrs	x5, spsr_el2
	sub	x6, sp, #(8*30)
	mrs	x7, sctlr_el2
	mrs	x8, hcr_el2
	mrs	x9, ttbr0_el2
	b	0f

1:	mrs	x1, esr_el1
	mrs	x2, elr_el1
	mrs	x3, daif
	mrs	x4, vbar_el1
	mrs	x5, spsr_el1
	sub	x6, sp, #(8*30)
	mrs	x7, sctlr_el1
	mov	x8, #0	/* Not used, EL1 don't have register, like 'scr_el1' */
	mrs	x9, ttbr0_el1
0:
	stp     x2, x0, [sp, #-16]!
	stp	x3, x1, [sp, #-16]!
	stp	x5, x4, [sp, #-16]!
	stp	x7, x6, [sp, #-16]!
	stp	x9, x8, [sp, #-16]!
	mov	x0, sp
	ret

上述汇编代码具体实现步骤如下:

(1)保存通用寄存器

stp	x27, x28, [sp, #-16]!
...
stp	x1, x2, [sp, #-16]!

x1–x30(除 x0 留到后面特殊处理)全部压栈。
stp
Store Pair,把两个寄存器一次存到内存,并更新栈指针
sp

[sp, #-16]!
表示:栈指针先减去16(预留空间),再把两个寄存器存进去。这样做相当于把30个通用寄存器的值保存到栈中。

(2)判断当前异常是在哪个EL发生

switch_el x11, 3f, 2f, 1f


switch_el
是一个宏,根据当前运行级别(EL3 / EL2 / EL1)跳转到不同的标签。因为异常可能发生在EL1内核、EL2 hypervisor或EL3 secure monitor级别。

(3)保存异常相关的系统寄存器

每个EL对应分支,读取一批寄存器,汇编指令如下:


mrs	x1, esr_el1   	// 异常综合状态寄存器
mrs	x2, elr_el1   	// 异常返回地址
mrs	x3, daif      	// 中断屏蔽位
mrs	x4, vbar_el1  	// 向量基地址
mrs	x5, spsr_el1  	// 保存程序状态
sub	x6, sp, #(8*30) // 计算通用寄存器保存区基址
mrs	x7, sctlr_el1 	// 系统控制寄存器
mov	x8, #0        	// EL1没有scr_el1,所以写0占位
mrs	x9, ttbr0_el1 	// 页表基地址

对EL2/EL3的情况,逻辑一样,只是寄存器不同。此处不再展开描述。

(4)把这些系统寄存器继续压栈

stp	x2, x0, [sp, #-16]!
stp	x3, x1, [sp, #-16]!
stp	x5, x4, [sp, #-16]!
stp	x7, x6, [sp, #-16]!
stp	x9, x8, [sp, #-16]!

这里保存ELR、ESR、DAIF、VBAR、SPSR、SCTLR、SCR/HCR(或占位)、TTBR0等关键信息。

(5)准备异常栈帧

mov	x0, sp
ret

最后把
sp
作为参数放到
x0
里返回。也就是说,这样后续的
do_sync

do_irq
等异常处理函数拿到的是一个指针,指向完整的异常上下文(栈帧)。

三、异常处理

在u-boot中,多种异常类型都具有一一对应的异常处理函数,如下表所示:

异常来源 (Current EL) Handler函数 说明
Synchronous (Thread)
do_bad_sync
当前EL线程模式下发生的同步异常(未定义指令、非法访存等)
IRQ (Thread)
do_bad_irq
当前EL线程模式下发生的普通中断
FIQ (Thread)
do_bad_fiq
当前EL线程模式下发生的快速中断
Error (Thread)
do_bad_error
当前EL线程模式下的SError
Synchronous (Handler)
do_sync
当前EL处理器模式下的同步异常
IRQ (Handler)
do_irq
当前EL处理器模式下的普通中断
FIQ (Handler)
do_fiq
当前EL处理器模式下的快速中断
Error (Handler)
do_error
当前EL处理器模式下的SError

四、异常退出处理

异常退出处理由
exception_exit
实现(arch/arm/cpu/armv8/exceptions.S):


exception_exit:
	add	sp, sp, #(8*8)/* see: sys registers size of struct pt_regs */
	ldp	x2, x0, [sp],#16
	switch_el x11, 3f, 2f, 1f
3:	msr	elr_el3, x2
	b	0f
2:	msr	elr_el2, x2
	b	0f
1:	msr	elr_el1, x2
0:
	ldp	x1, x2, [sp],#16
	ldp	x3, x4, [sp],#16
	ldp	x5, x6, [sp],#16
	ldp	x7, x8, [sp],#16
	ldp	x9, x10, [sp],#16
	ldp	x11, x12, [sp],#16
	ldp	x13, x14, [sp],#16
	ldp	x15, x16, [sp],#16
	ldp	x17, x18, [sp],#16
	ldp	x19, x20, [sp],#16
	ldp	x21, x22, [sp],#16
	ldp	x23, x24, [sp],#16
	ldp	x25, x26, [sp],#16
	ldp	x27, x28, [sp],#16
	ldp	x29, x30, [sp],#16
	eret


exception_exit
的作用是从异常栈帧恢复CPU寄存器状态,把在
_exception_entry
保存的通用寄存器(x1 … x30)、以及在进入时读取并压栈的系统寄存器(
ELR_x, ESR_x, DAIF, VBAR, SPSR, SCTLR, SCR/HCR, TTBR0
等)依次恢复到对应寄存器里,最后执行
eret
跳回异常发生前的上下文。

(1)调整堆栈指针位置


add sp, sp, #(8*8)
:把
sp
增加
8*8 = 64
字节。这64字节是用来跳过之前为系统寄存器留出的空间或占位。 在异常返回前,把栈顶调整回来,跳过系统寄存器保存的那一块空间 。

先移动
sp
跳过某个固定大小的区域,以便接下来的
ldp
恰好从正确的位置弹出我们需要的成对保存的寄存器值。


                 _exception_entry (保存)                exception_exit (恢复)
高地址 ─────────────────────────────────────────────────────────────────────────

      stp x27, x28, [sp, #-16]!       ───────►       ldp x27, x28, [sp], #16
      stp x25, x26, [sp, #-16]!       ───────►       ldp x25, x26, [sp], #16
      stp x23, x24, [sp, #-16]!       ───────►       ldp x23, x24, [sp], #16
      stp x21, x22, [sp, #-16]!       ───────►       ldp x21, x22, [sp], #16
      stp x19, x20, [sp, #-16]!       ───────►       ldp x19, x20, [sp], #16
      stp x17, x18, [sp, #-16]!       ───────►       ldp x17, x18, [sp], #16
      stp x15, x16, [sp, #-16]!       ───────►       ldp x15, x16, [sp], #16
      stp x13, x14, [sp, #-16]!       ───────►       ldp x13, x14, [sp], #16
      stp x11, x12, [sp, #-16]!       ───────►       ldp x11, x12, [sp], #16
      stp x9,  x10, [sp, #-16]!       ───────►       ldp x9,  x10, [sp], #16
      stp x7,  x8,  [sp, #-16]!       ───────►       ldp x7,  x8,  [sp], #16
      stp x5,  x6,  [sp, #-16]!       ───────►       ldp x5,  x6,  [sp], #16
      stp x3,  x4,  [sp, #-16]!       ───────►       ldp x3,  x4,  [sp], #16
      stp x1,  x2,  [sp, #-16]!       ───────►       ldp x1,  x2,  [sp], #16

      stp ELR, x0,  [sp, #-16]!       ───────►       ldp x2(ELR), x0, [sp], #16
      stp DAIF, ESR, [sp, #-16]!      ───────►       (跳过,add sp, sp, #64)
      stp SPSR, VBAR, [sp, #-16]!     ───────►       (跳过)
      stp SCTLR, SPbase, [sp, #-16]!  ───────►       (跳过)
      stp TTBR0, SCR/HCR, [sp, #-16]! ───────►       (跳过)

低地址 ─────────────────────────────────────────────────────────────────────────
(2)设置异常返回地址


switch_el x11, 3f, 2f, 1f
:根据当前CPU的Exception Level(可能存放在
x11
中或该宏通过系统寄存器检测EL)跳转到相应标签(EL3->3f, EL2->2f, EL1->1f)。目的是把
x2
写回到正确等级的
ELR_ELx
(EL1/EL2/EL3有各自的ELR寄存器)。

随后三段:


ldp	x2, x0, [sp],#16
	switch_el x11, 3f, 2f, 1f
3:	msr	elr_el3, x2
	b	0f
2:	msr	elr_el2, x2
	b	0f
1:	msr	elr_el1, x2


x2
写入对应EL的
ELR_ELx
,也就是设置好返回地址(异常返回时
eret
会跳回
ELR_ELx
指定的地址)。

(3)恢复通用寄存器

ldp x1, x2, [sp],#16
ldp x3, x4, [sp],#16
...
ldp x29, x30, [sp],#16

这些
ldp
是按对恢复通用寄存器(x1…x30),每条把两个寄存器从栈上取出并把
sp
加16。

恢复顺序是入栈时的逆序(LIFO),这样能把每个寄存器恢复到它进入异常前的值。注意
x0
之前已经在
ldp x2,x0, [sp],#16
中恢复。

(4)eret异常返回


eret
(Exception Return)会做两件关键事:

1、从当前
SPSR_ELx
中恢复程序状态(PSTATE: condition flags, interrupt masks 等)到
PSTATE
。通常
_exception_entry
曾读取并压栈
spsr_elx
,但实际
SPSR
的恢复路径可以是通过在返回前把
spsr_elx
写回
spsr
寄存器(若需要),或由硬件在
eret
时依据
SPSR_ELx
恢复。

2、跳转到
ELR_ELx
指定的地址(之前我们用
msr elr_elX, x2
恢复过)。


eret
运行结果就是把CPU恢复到异常发生前的执行现场并继续执行。

© 版权声明

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
none
暂无评论...