-
Notifications
You must be signed in to change notification settings - Fork 38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
10_ARMv8_异常处理(一) - 入口与返回、栈选择、异常向量表 #47
Labels
Comments
carloscn
added a commit
that referenced
this issue
Apr 14, 2022
* [10_ARMv8_异常处理](#47) [2022-4-13] [2022-4-14]
carloscn
added a commit
that referenced
this issue
Apr 16, 2022
* [10_ARMv8_异常处理(一) - 入口与返回、栈选择、异常向量表](#47) [2022-4-13] [2022-4-14]
This was referenced Oct 1, 2022
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
10_ARMv8_异常处理
终于,我们从ARMv8的一些汇编指令的学习当中出来了,开始研究ARMv8体系架构的设计。这里面的机制十分的重要,所以不得不依赖笨叔的视频,同样自己也要阅读ARMv8的手册,希望自己一字一句的去理解,不懂的去搜索,争取把这部分知识补充完整。
1. ARMv8 fundamentals1
这部分在笨叔的视频和讲义里面只是粗略的讲了一下high-level的版图设计,但我觉得这个十分重要,没有这个基础后面很难去理解异常处理的过程,而且在high-level上面,人为的设定了很多规则,我觉得有必要整理一下。大纲总结如下:
1.1 EL/PL/secure/non-secure
这里主要提到了一些概念和术语,我们需要理解这些概念还背后术语的一些潜在的设计意义,挖掘出设计者的意图。文档在这一章开篇就提到:异常等级(Exception level)、特权等级(Privilege level)。ARMv8内部包含4个异常等级:
这里就有些需要注意的了,特权等级是在ARMv7里面的概念,ARMv8属于借鉴过来,PLn对应ELn。
ARMv8提供了两个安全状态(Secure States),一个叫普通世界(normal world),一个叫安全世界(secure world),这两个世界并行的运行在同一个硬件设备上面,安全世界的工作重心是抵抗软件和硬件的攻击。在EL3下的secure monitor,游走于secure world和normal world。
在normal world的hypervisor(VMM)代码运行在系统上并且管理着多个Guest OS,所以每一个操作系统都运行在VMM上面,每个操作系统在同一时间都不知道有其他操作系统在运行。
Guest OS kernels,这部分分为两类:
Hypervisor,总是normal world的,when present,主要是给rich os kernels提供服务。
Secure Firmware,这段程序必须运行在boot时间,用于初始化trusted os,平台,还有secure monitor。
Trusted OS,在EL1和Guest OS并行运行,提供一个runtime环境执行安全应用。OPTEE-OS
1.2 Execution States
ARMv8定义了两个执行状态,aarch64和aarch32。这两个执行状态和异常等级没有概念上面的交织,也就是aarch64和aarch32都有相应的异常等级和特权模式。在aarch64执行状态时,使用A64指令,在aarch32执行状态,使用A32或者T32(Thumb)指令集。
AArch32模式,有一点不同的是关于trust-os的位置,trusted-os软件执行在secure EL3,在aarch64执行模式,是执行在EL1中的。ARMv7和aarch32很像。
1.3 Changing Exception levels
对于异常模式的变迁,ARMv8和ARMv72是一致的。异常模式的变迁不光是EL层级的变迁,在EL层级里面还有很多处理器的模式(mode)。这里有个术语叫做[take an exception],可以理解为异常的激发。当take an exception的时候,处理器的模式(mode)就会被改变。不同的EL层级内包含不同的模式集合。我理解这部分在ARMv8里面已经拿掉了,似乎没有这个概念,全部都被异常处理cover。
SVC
) is executedSMC
instruction (Secure Monitor Call) is executed or when the processor takes an exception which is configured for secure handling.Provided to support switching between Secure and Non-secure states.ARM64处理器内部的中断分为两种,FIQ和IRQ,FIQ叫快速中断请求,IRQ就是普通的中断请求。FIQ的优先级高于IRQ。
在ARM64处理器中异常模式有以下规则:
ERET
指令ERET
之后只能保留在本层或者是更低的层级。1.4 Changing execution state
32位的应用程序可以运行在64位的上面,这个在x86的架构似乎也能看见。在ARM64处理器上也是支持这样的使用场景的。这样的执行是有个条件的,也就是在执行32位程序的时候,必须让处理器处于AArch32的执行状态。为了实现这样的执行,处理器只能在更高的EL层级去执行32位的程序。首先,32位的应用程序在EL0产生一个
SVC
指令向supervisor call,接着会产生一个IRQ,这个IRQ就会切换到AARCH64内的EL1,等着程序运行完毕之后任务ERET
到EL0。32/64混合的程序没办法在ARM64上运行,还有64位程序就没办法在32位系统上运行了。由于在执行32位程序的时候,处理器会升一个EL,因此即使AArch64状态能够支持AArch32,但是是在低一级的权利模式。一个AArch64的操作系统能够运行32/64两种类型的应用,hypervisor也是一样,AArch64的hypervisor能够管理AArch32和AArch64的guest操作系统。AArch32的并没有这个能力。
2. ARMv8 exception handling
在ARM的世界我们应该纠正一个概念,“中断”。通用的中断的定义就是可以阻止正常软件流程执行的中断。然而在ARM的术语里面,不能仅仅用中断来表示中断,我们需要更上一层的定义,异常(Exception)。异常包含狭义的中断,也包含条件变量(conditions)或者是系统事件(system event),这些行为都有专门的异常处理函数来处理。在ARCH64里面有三类异常,中断(interrupts),中止(aborts) 和复位(reset)。
2.1 exception types
interrupts
在AArch64体系架构中有两种类型的中断,IRQ和FIQ。FIQ的优先级高于IRQ,这部分中断设计的时候我们再说。即便我们对于中断的概念已经很熟悉了,但这里面需要澄清几个有关中断的概念。
对于触发中断,在CPU上有专门的动词术语,assert一个中断,take一个异常。中断线由外部外设assert,中断线的信号被接入到中断控制器,中断控制器根据中断优先级判断先发出哪些信号,再根据中断向量表查找到中断处理函数的地址。由于中断并非执行在给定时间内,因此中断属于异步异常。
aborts
同样包含两列aborts,取指失败(Instruction Aborts)或者是访问数据失败(Data Aborts)。
在内存上,发生这种异常的场景可能有两种,在访问内存时,外部存储器返回一个错误,或者指定访问的地址没有关联到真实的内存中(MMU产生该错误,一个操作系统可以使用MMU abort动态的分配内存给应用程序)。
在指令上,aborts发生在取指还未执行的时候。data abort发生在存储和加载指令。
abort异常属于同步异常。
reset
reset异常有着最高的异常等级。当异常发生的时候ARM处理器就会跳转到指令所在的位置。
RVBAR_ELn
包含跳转地址,n应该是该处理器的最高权限,armv8里面就应该为n=3。该异常不可屏蔽,不可禁止,并且尝尝执行在上电后的初始化的时候。Exception generating instructions
一些异常需要由CPU外部的触发中断线间接性的塑造异常,一些系统调用指令也可以直接的产生异常,这些指令通常会向处理器请求一些运行在更高权限环境中的服务。
有几个需要注意的点:
HCR_EL2.TGE
被置位的时候,这个时候异常会发生在EL2。2.2 中断嵌套
我们要注意一个比较重要的概念,中断嵌套(中断抢占),从定义上来看4:
从处理器的角度来看,ARM64是支持中断嵌套的,我们可以从三个角度来看5:
2.2.1 ARM Core的中断嵌套
首先来看ARM CORE支持中断嵌套吗?答案 支持! 但是是有一个前提,在进入中断处理时,PSTATE的I、F、A等比特位是MASK的,软件中需要主动unmask后,那么就可以中断嵌套了。如下图所示,正是ARM Core支持中断嵌套的一个示例(或者叫模型):
每次调用要保存中断的上下文。
2.2.2 GIC中断控制器中断嵌套
继续看GIC中断控制器支持中断嵌套吗?答案 支持中断抢占,支持中断嵌套。介绍以下优先级和抢占的概念:
GICD_IPRIORITYn
或GICR_IPRIORITYn
表示),它是一个 8 位无符号值。 0x00 是可能的最高优先级,0xFF 是可能的最低优先级。ICC_PMR_EL1
)。 该寄存器设置将中断转发到该 PE 所需的最低优先级。 只有优先级高于寄存器值的中断才会发送给 PEGIC中断控制器的中断嵌套是可以配置的。分为Without preemption和With preemption两种模式:
那么对于一个gicv3的IP,优先级肯定是有的,它到底是Without preemption 还是With preemption呢? 如何配置的呢?
请查略
ICC_BPRn_EL1
寄存器,该寄存器定义优先级值字段分成两部分的点,即组优先级字段和子优先级字段。 组优先级字段确定组 1 中断抢占。 换句白话来解释就是,中断优先级被分成了两部分,如下图所示,ICC_BPRn_EL1
寄存器的BIT[2:0]定义了下图中的N的值6:2.2.3 Linux Kernel and RTOS
Linux Kernel
继续看Linux Kernel操作系统有没有使用中断嵌套?答案 没有使用!
首先查看
ICC_BPRn_EL1
寄存器的配置:https://elixir.bootlin.com/linux/v5.13.19/source/drivers/irqchip/irq-gic-v3.c#L1005
写入的是0,也就是意味着,N=0, 即下图的第一行,也就是说抢占是开启的。
继续看,针对每一个 INTID(中断号) 的priority的配置,如下所示,在gic初始化阶段,给每一个 INTID(中断号) 都配置成了一样的优先级,值位0XA5。 也就是所有中断的优先级都是一样的。
https://elixir.bootlin.com/linux/v5.13.19/source/drivers/irqchip/irq-gic-v3.c#L803
事实上,在gicv3代码中,提供了一个接口,可以单独针对某一中断设置优先级。查略该函数用途,仅仅是为NMI中断设置的(注:Linux Kernel中armv8体系目前还没有该中断,ARMV9新增了一类NMI中断),其值为0Xa5 & 0x7f = 0x25,该值小于0XA0,所有该优先级大于其它中断的优先级。
https://elixir.bootlin.com/linux/v5.13.19/source/drivers/irqchip/irq-gic-v3.c#L460
##### RTOS的中断抢占(ARM Cortex-CM3)478
关于RTOS系统如何驱动CPU进行中断嵌套,参考78。内部使用了就绪链表的结构体,这部分暂时不在ARMv8中展开讨论。
Exception handling registers9
我们在做加减乘除的指令的时候从
PSTATE
寄存器的高4位读取NZCV的值,来确定状态。今天要涉及的是PSTATE
DAIF的值,这部分是异常处理标志位。根据手册的翻译,如果异常发生,PSTATE信息会被存入到Saved program status register(SPSR_ELn),只有3个,SPSR_EL1, SPSR_EL2, SPSR_EL3。DAIF就是exception bit mask bits,还有SPSel
当异常发生的时候,PSTATE寄存器的值(current EL, DAIF, NZCV etc)都会被复制到SPSR_ELn,返回的地址会被也会被存储到ELR_ELn中。
有几点需要注意:
SPSR_EL1,2,3是三个不同的寄存器实体
在同步或者SError异常,ESR_ELn也会被更新,用于表明产生异常的原因。
我们已经看见SPSR已经为异常的返回记录了必要的状态信息,还要知道link registers用于存储程序地址信息。ARM64提供一个独立的链接寄存器用于函数调用和异常返回。
我们在做汇编跳转的实验的时候需要备份X30寄存器的地址,在异常返回的时候它的值也是会被指令地址更新。X30子函数的返回地址,ret指令返回;ELR_ELx存储异常返回地址,使用ERET指令**。
ELR_ELn用于存储从一个异常返回到程序的地址。对于一些异常,这个地址是产生异常的下一条指令地址,但有些情况,例如SVC指令的执行,因为发生了异常,我们需要回来的时候继续执行原指令而不是下一条指令,这个就有说法了。异步异常,ELR_ELn指向一个还没有被执行的第一条指令的地址,这个时候我们在handler的代码里面是可以修改ELR_ELn指向的地址的,比如发生了abort指令的异常,异常结束之后我们还需要回到发生异常的那条指令上去。所以就必须要对地址进行减4或者减8。
SPSR和ELR寄存器,每一个异常等级都有自己的栈寄存器SP_ELn。这些寄存器用于指示不同EL层级专用的栈。如果没有专用的栈指针,当函数执行到异常处理的handler的时候,SP指针就会被异常处理函数覆盖,所以每一级的SP_EL都会存储自己的原始的地址。
handler可能会把SP_ELn切换到SP_EL0。比如,SP_EL1可能指向一块由内核进行安全维护的比较小的内存区域,SP_EL0指向一块内核没有负责安全维护的比较大的内核任务栈空间(有可能发生溢出),这个时候可能EL1溢出到EL0。这个时候需要写[SPSel]位,来进行SP_EL的切换:
4. Syn and Asyn exceptions
异步和同步异常在ARM64处理器里面处理的方式不太一样,而且使用的寄存器还有寄存器的行为都是不一样的。这一小节笔记主要就是来分析和记录一下同步异步处理的不同点,后面还有关于aarch32和aarch64混合的时候,arm64的一些行为。ARM的手册在最开始的时候就强调一个事情,return address是否包含具体的发生异常的原因。这个有什么用不太清楚,手册里面注明,对于同步的异常,回退地址总能包含发生异常的原因,然而对于异步的异常,回退地址可能会包含发生异常的原因。
异步的异常主要有三种:
同步的异常包含:
4.1 sync aborts
同步异常的触发有很多种情况,这里需要注意的是,异常不代表错误,异常状态也可能是系统正常处理的一部分,比如,在Linux里面内存缺页错误,还有我们之前说预计加载指令的page fault10,这都是为了提升性能必须要制造的异常场景。同步的异常处理包含:
在ARMv7架构里面,预取指令prefetch,数据错误Data abort,未定义异常undef是分开的指令。而在AARCH64架构里面,都视为同步异常,如果我们想在AARCH64体系架构里面知道具体的异常是什么东西,那就需要配合其他寄存器来获取更全面的信息,ESR_ELn,FAR_ELn。
4.2 Handling synchronous exceptions
有个AARCH32和AARCH64混合的场景,我们在运行32位程序的时候需要进入到EL1执行,这个时候32位的程序发生了异常,需要进入到EL2处理,此时EL2还是AARCH64的,这个时候我们想要获取到32位程序的异常,需要进入到EL2的FAR寄存器读取信息,EL1的AARCH32的执行状态的FAR,全部清0。
对于比较高等级的EL2(hypervisor),EL3(secure monitor):
4.3 system calls
EL0 call EL1
执行应用程序想要使用特权指令这个时候就需要系统调用。其中的一种方法就是SVC指令。当应用程序call这个SVC的时候,就会产生异常,处理器理所应当的进入到了更高一级的EL。这个时候如果我们想传递一些参数,就通过写寄存器的方式。
EL1 call EL2/3
如果在EL0,是不允许直接call到EL2或者EL3的,必须通过EL0 call到EL1的os kernel,由kernel发送请求到EL2/3。在EL1的os kernel通过HVC指令call到hypervisor,或者os kernel通过SMC指令call到secure monitor的。如果在处理器里面没有EL3,这个时候就会出现新的异常unallocted instruction。注意,我看手册里面是EL1可以call HVC也可以直接call SMC。
EL2 call EL3
EL2可以call EL3通过SMC指令。如果在EL2/3 向后call HVC指令,也会有个同步异常发生在本异常等级。
4.4 Exe state & Exception level
官方手册里面给了一个图表示EL0 如何 call进入EL1的,可以说这个是一个直接关系,EL0是没有任何异常处理机制的。
我们补充一下关于EL1 call进EL2/3的图,对于同步的异常而言,没什么好说的了,就是一级一级的call进去,但是对于异步的异常而言,就有点不一样了,我们EL1需要call EL2或者EL3的时候,需要配置HCR寄存器决定路由信息,同样的EL3也是需要路由信息的配置的。否则会产生新的异常。
还有一个比较特殊的情况,当有一个从AARCH32发生的异常到AARCH64处理,必须做一些特殊的考虑,AARCH64需要访问32位的寄存器了,直接访问肯定会hang死,所以ARM处理做了一些映射处理:
HCR_EL2.RW记录了EL1运行在哪个模式, 1代表aarch64, 0是aarch3211
关于返回的时候,也是通过寄存器找返回地址和返回的模式,在SPSR寄存器M[4:0]记录了返回的模式。
4.5 Exception Table
4.5.1 Table
采用基地址+offset的模式,ARMv8有三个表,VBAR_EL1, VBAR_EL2, VBAR_EL3。 高11位有效。
VBAR_ELn + 0x000
+ 0x080
+ 0x100
+ 0x180
+ 0x200
+ 0x280
+ 0x300
+ 0x380
+ 0x400
+ 0x480
+ 0x500
+ 0x580
+ 0x600
+ 0x680
+ 0x700
+ 0x780
如果kernel code执行在EL1,IRQ一个中断过来,这时候SP_EL1,而且SPSel bit被置位,执行地址就应该是 VBAR_EL1 + 0x280。
4.5.2 Linux Kernel
以下是linux 4.14内核的entry.S文件中书写的异常向量表。
看一下Linux内核发生异常的时候如何调用函数的,这里以el1_fiq_invalid为例,这个异常没有被Linux kernel实现。
异常向量表
inv_entry 1 , BAD_FIQ
,指定el为1,备份sp到x0,把esr_el1(存储具体异常原因的)被分到x2接着跳转到bad_mode里面。4.5.3 save context
Linux内核的arch/arm64/include/uapi/asm/ptrace.h 和arch/arm64/include/asm/ptrace.h 里面定义了一下结构体,用于存储异常上下文。
5. Excepiton Entry
5.1 Entry
上面罗列了很多异常元素的概念,现在需要有机的将上面的内容串起来,看一下ARMv8到底如何处理的异常。CPU需要做的事情:
操作系统需要做的事情:
在操作系统执行ERET指令,CPU需要从ELR_ELx寄存器中恢复PC的指针;从SPSR_ELx寄存器恢复处理器状态。
5. Examples
5.1 switch EL2 to EL1
基于树莓派和QEMU,上电跳转的时候benos处于EL2下,请把benos切换到EL1中运行。
【提示】:
【程序流】:
Node 1: judge ELn
要判断当前的是EL几,可以通过访问
CurrentEL
12系统寄存器来确定,这个是个64位的寄存器。EL,bit[3:2]
Node 2: set HCR_EL2 reg (about AArch64)
这个
HCR_EL2
11主要用于设定我们的目标异常等级。主要是配置这个:
Node 3: MMU closing
SCTLR_EL1
13EL1的系统控制寄存器上面可以关掉MMU。Node 4: Clear DAIF
SPSR_EL2寄存器14
Node 5: Set entry
ELR_EL2
15设定异常返回地址。5.2 register exception vectors
// entry.S
需要在boot.S中注册向量表
kernel最后跳转:
Reference and Change Log
[1]: 2022-8-13: 增加 2.3章节,解说中断嵌套机制
Footnotes
ARM Cortex-A Series Programmer's Guide for ARMv8-A - Fundamentals of ARMv8 ↩
ARM Cortex-A Series Programmer's Guide for ARMv7-A - ARM Processor Modes and Registers ↩
【内核教程第六十四讲】Linux内核异常处理 - 16:51 ↩
RTOS系列(1):基础知识——中断嵌套 ↩ ↩2
[ARM异常]-ARM体系中是否支持中断嵌套 ↩
GICv3_Software_Overview_Official_Release_B.pdf ↩
RTOS内功修炼记(一)—— 任务到底应该怎么写 ↩ ↩2
RTOS内功修炼记(二)—— 优先级抢占调度到底是怎么回事? ↩ ↩2
ARM Cortex-A Series Programmer's Guide for ARMv8-A - AArch64 Exception Handling ↩
04_ELF文件_加载进程虚拟地址空间 ↩
Arm Cortex‑A78AE Core Technical Reference Manual - HCR_EL2, Hypervisor Configuration Register, EL2 ↩ ↩2
Arm A-profile Architecture Registers - CurrentEL, Current Exception Level ↩
Arm Armv8-A Architecture Registers - SCTLR_EL1, System Control Register (EL1) ↩
Arm Armv8-A Architecture Registers - SPSR_EL2, Saved Program Status Register (EL2) ↩
Arm A-profile Architecture Registers - ELR_EL2, Exception Link Register (EL2) ↩
The text was updated successfully, but these errors were encountered: