文章目录
- 一、异常简介
- 1.1 Exception levels
- 1.2 异常类型
- 二、系统调用简介
- 2.1 SVC指令
- 2.2 VBAR
- 2.3 系统调用保存现场
- 2.4 系统调用返回
- 三、Linux 内核分析
- 参考资料
一、异常简介
在ARM64体系架构中,异常是处理器在执行指令时可能遇到的不寻常情况或事件。这些异常可以是由软件或硬件引发的。ARM64体系架构定义了一套异常模型,用于处理和响应这些异常情况。
1.1 Exception levels
Armv8-A体系结构定义了一组异常级别,EL0到EL3,其中:
● 如果ELn是异常级别,则n的值增加表示软件执行权限增加。
● 在EL0执行称为无特权执行。EL0为非特权模块,应用层。
● 在EL1执行称为特权特权执行。EL1为特权模块,操作系统内核层。
● EL2提供对虚拟化的支持。
● EL3支持在两种安全状态(安全状态和非安全状态)之间切换。
EL0 Applications.
EL1 OS kernel and associated functions that are typically described as privileged.
EL2 Hypervisor.
EL3 Secure monitor.
一个armv8系统实现可能不包括所有的Exception级别。所有实现都必须包括EL0和EL1。EL2和EL3是可选的。
从上面可看到armv8最大支持EL0~EL3四个exception level,EL0的execution privilege最低,EL3的execution privilege最高。当发生异常的时候,系统的exception会迁移到更高的exception level或者维持不变,但是绝不会降低。此外,不会有任何的异常会去到EL0。
1.2 异常类型
在Linux ARM64架构中,同步异常和异步异常与处理器的异常处理机制和中断控制器有关。
(1)同步异常(Synchronous Exceptions):
在ARM64架构中,同步异常与当前指令的执行直接相关,它们在指令执行期间同步地引发。常见的同步异常包括:
●未定义指令异常:当尝试执行在ARM64架构中未定义的指令时,会引发该异常。包括在不适当的异常级别执行指令、禁用的指令、以及未被分配的指令位模式。
●非法执行状态异常:当PSTATE.IL(非法执行状态)设置为1时,尝试执行指令会引发该异常。
●堆栈指针(SP)未对齐异常:当堆栈指针未按照对齐要求进行对齐时,会触发该异常。
●程序计数器(PC)未对齐异常:当尝试执行具有错误对齐的指令时,会引发该异常。
●引发异常的指令:特定指令(如SVC、HVC或SMC)可能触发异常。
●被捕获指令异常:当尝试执行被定义为在较高异常级别被捕获的指令时,会引发该异常。
●内存相关异常:由内存地址转换系统引发的异常,包括与指令执行或内存访问相关的指令异常和数据异常。
●数据地址未对齐异常:当尝试使用错误对齐的地址访问内存时,会触发该异常。
●调试异常:与调试相关的各种异常,例如断点指令异常、断点异常、观测点异常、向量捕获异常和软件单步异常。
●被捕获的浮点异常(如果支持):在支持浮点异常捕获的实现中,当发生被捕获的IEEE浮点异常时,会引发该异常。
对于同步异常,Linux内核会根据异常类型执行相应的异常处理程序,例如调用适当的异常处理函数、进行错误处理或发送信号给相关进程。
(2)异步异常(Asynchronous Exceptions):
在ARM64架构中,异步异常是与当前指令执行无直接关联的异常,它们以异步的方式引发。常见的异步异常包括:
在Armv8-A架构中,被带入AArch64状态的异步异常也被称为中断。中断分为两种类型:
物理中断(Physical interrupts):这些是从处理器外部发送给处理器的信号。它们包括:
SError:系统错误中断。
IRQ:普通中断请求。
FIQ:快速中断请求。
虚拟中断(Virtual interrupts):软件在EL2执行时可以启用和挂起的中断。虚拟中断从EL0或EL1传递到EL1。
虚拟中断的名称与物理中断对应:
vSError:虚拟系统错误中断。
vIRQ:虚拟普通中断请求。
vFIQ:虚拟快速中断请求。
对于异步异常,ARM64架构中的处理方式通常是由中断控制器来管理和处理。中断控制器会根据中断优先级和配置,将异步事件传递给处理器,并触发相应的中断处理程序。
二、系统调用简介
2.1 SVC指令
系统调用属于异常的同步软件异常。
系统调用是通过执行SVC、HVC或SMC指令触发的,这里我们指讨论SVC指令:
默认情况下,执行SVC指令会生成一个Supervisor Call,这是一个针对EL1的同步异常。这为在EL0执行的软件提供了一种调用在EL1执行的操作系统或其他软件的机制。
在ARM64体系架构中EL0是用户空间,EL1是内核空间。
SVC指令提供了在不同执行级别(EL)之间进行通信和交互的机制。通过执行SVC指令,EL0中的用户空间程序可以请求EL1中运行的操作系统或其他软件执行特权操作。
SVC Generate exception targeting Exception level 1
Supervisor Call causes an exception to be taken to EL1.
2.2 VBAR
当处理器处于使用AArch64执行状态时,当处理器执行到一个异常级别(Exception level)引发异常时,执行将被强制转移到异常向量(exception vector)所指示的地址上。异常向量表(vector table)位于该异常级别的内存中,占据一系列以字对齐的地址。
在ARMv8-A体系结构中,每个异常级别都有一个关联的向量基地址寄存器(Vector Base Address Register,VBAR),它定义了该异常级别对应的异常基地址。向量基地址决定了异常向量表在特定异常级别下的起始地址。当异常发生时,处理器使用与当前异常级别相关联的VBAR寄存器来查找相应的向量表,并跳转到该表中对应的异常向量。
通过为每个异常级别使用独立的向量表和VBAR寄存器,ARMv8-A体系结构在不同特权级别下的异常处理中提供了灵活性和定制性。该机制能够高效处理异常和中断,确保在不同情况下的适当处理和恢复。
处理器在用户空间EL0执行系统调用时,即执行svc指令,发生了异常,处理器跳转和执行相关的异常处理指令。异常相关的处理指令存储在一个表中,即异常向量表。对于ARM64体系架构,EL1对应一个异常向量表,其地址存放在向量基址寄存器(VBAR_EL1, Vector Base Address Register )中。
对于AArch64状态的异常,the vector table提供以下信息:
以下是关于异常的一些信息
(1)异常类型:
— Synchronous exception.
— SError.
— IRQ.
— FIQ
(2)异常级别和相关信息:
异常发生的异常级别:指示异常发生的特权级别,例如EL0(用户级别)、EL1(操作系统级别)等。
正在使用的堆栈指针:用于跟踪异常处理期间的堆栈操作。
寄存器文件的状态:指示异常发生时寄存器文件中各个寄存器的值和状态。
说明:
(1)这个表有四个异常条目,每个异常条目有四种异常类型:同步异常(Synchronous exception),IRQ,FIQ和SError。
(2)每一个异常入口占用0x80 bytes空间,每一个异常入口可以放置多32条指令。ARMv8指令集支持64位指令集,但每一条指令的位宽是32位,而不是64位。
0x80(128) / 4 = 32
四个异常条目:
(1)如果发生异常并不会导致exception level切换,并且使用的栈指针是SP_EL0,对应着第一个异常条目。
(2)如果发生异常并不会导致exception level切换,并且使用的栈指针是SP_EL1/2/3,对应着第二个异常条目。
(3)如果发生异常会导致exception level切换,并且比目的exception level低一级的exception level运行在AARCH64模式,对应着第三个异常条目。
(4)如果发生异常会导致exception level切换,并且比目的exception level低一级的exception level运行在AARCH32模式,对应着第四个异常条目。
对于AARCH64模式异常指令svc,处理器会导致exception level切换,异常等级从EL0切换到EL1,对应着第三个异常条目:如果发生异常会导致exception level切换,并且比目的exception level低一级的exception level运行在AARCH64模式,那么使用第三个异常条目。
Lower Exception level, where the implemented level immediately lower than the target level is using AArch64.b
对于异常指令svc属于同步异常(Synchronous exception),因此svc异常处理器会跳转到 VBAR_EL1 + 0x400 地址处的异常向量中。
2.3 系统调用保存现场
当系统调用从用户态到内核态的时候,首先要做的第一件事情,就是将用户态运行过程中的 CPU 上下文保存起来,其实主要就是保存在这个结构的寄存器变量里。这样当从内核系统调用返回的时候,才能让进程在刚才的地方接着运行下去。
应用层程序执行svc系统调用指令时,会将用户态此时所有通用寄存器的状态保存起来,以便从系统调用返回时恢复状态。将进程用户态此时所有通用寄存器的状态保存在该进程的内核栈中的pt_regs栈框中。
在内核栈的最高地址端,存放的是另一个结构 pt_regs,这个结构体保存着进程从应用层进入到内核层时,用户态寄存器的状态。
struct pt_regs结构体:
/*
* This struct defines the way the registers are stored on the stack during an
* exception. Note that sizeof(struct pt_regs) has to be a multiple of 16 (for
* stack alignment). struct user_pt_regs must form a prefix of struct pt_regs.
*/
struct pt_regs {
union {
struct user_pt_regs user_regs;
struct {
u64 regs[31];
u64 sp;
u64 pc;
u64 pstate;
};
};
u64 orig_x0;
#ifdef __AARCH64EB__
u32 unused2;
s32 syscallno;
#else
s32 syscallno;
u32 unused2;
#endif
u64 orig_addr_limit;
/* Only valid when ARM64_HAS_IRQ_PRIO_MASKING is enabled. */
u64 pmr_save;
u64 stackframe[2];
};
如下图所示:
关于进程内核栈请参考:Linux 进程管理之内核栈和struct pt_regs
2.4 系统调用返回
当操作系统的异常处理(这里只描述svc异常)完成后,执行一条ERET指令就可以从异常返回。寄存器ELR_ELx存放svc指令异常返回的地址,发生系统调用svc指令时,系统肯定是在用户空间的地址,将svc指令的下一条指令的地址保存在寄存器ELR_ELx中,当系统调用返回时,即执行ERET指令时,返回svc异常指令现场,从寄存器ELR_ELx取出svc指令的下一条指令的地址,执行该指令。执行ERET指令时会从寄存器ELR_ELx恢复PC指针。
三、Linux 内核分析
(1)汇编入口:
// linux-5.4.18/arch/arm64/kernel/entry.S
/*
* Exception vectors.
*/
.pushsection ".entry.text", "ax"
.align 11
ENTRY(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
#ifdef CONFIG_COMPAT
kernel_ventry 0, sync_compat, 32 // Synchronous 32-bit EL0
kernel_ventry 0, irq_compat, 32 // IRQ 32-bit EL0
kernel_ventry 0, fiq_invalid_compat, 32 // FIQ 32-bit EL0
kernel_ventry 0, error_compat, 32 // Error 32-bit EL0
#else
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
#endif
END(vectors)
Linux 内核使用vectors作为异常向量表,刚好和arm官方手册Vector Base Address Register (VBAR)的相对应。
前面说到对于同步异常指令svc,处理器会跳转到 VBAR_EL1 + 0x400 地址处的异常向量中。
VBAR_EL1 存放的是vectors的基地址。
一个表项128字节,十六进制即 0x80。
偏移 | 异常描述 |
---|---|
Current Exception level with SP_EL0 | |
0x00 | kernel_ventry 1, sync_invalid // Synchronous EL1t |
0x80 | kernel_ventry 1, irq_invalid // IRQ EL1t |
0x100 | kernel_ventry 1, fiq_invalid // FIQ EL1t |
0x180 | kernel_ventry 1, error_invalid // Error EL1t |
Current Exception level with SP_ELx, x>0 | |
0x200 | kernel_ventry 1, sync // Synchronous EL1h |
0x280 | kernel_ventry 1, irq // IRQ EL1h |
0x300 | kernel_ventry 1, fiq_invalid // FIQ EL1h |
0x380 | kernel_ventry 1, error // Error EL1h |
Lower Exception level, where the implemented level immediately lower than the target level is using AArch64 | |
0x400 | kernel_ventry 0, sync // Synchronous 64-bit EL0 |
0x480 | kernel_ventry 0, irq // IRQ 64-bit EL0 |
0x500 | kernel_ventry 0, fiq_invalid // FIQ 64-bit EL0 |
0x580 | kernel_ventry 0, error // Error 64-bit EL0 |
Lower Exception level, where the implemented level immediately lower than the target level is using AArch32 | |
0x600 | kernel_ventry 0, sync_invalid, 32 // Synchronous 32-bit EL0 |
0x680 | kernel_ventry 0, irq_invalid, 32 // IRQ 32-bit EL0 |
0x700 | kernel_ventry 0, fiq_invalid, 32 // FIQ 32-bit EL0 |
0x780 | kernel_ventry 0, error_invalid, 32 // Error 32-bit EL0 |
vectors的基地址 + 0x400 =
kernel_ventry 0, sync // Synchronous 64-bit EL0
// linux-5.4.18/arch/arm64/kernel/entry.S
/*
* EL0 mode handlers.
*/
.align 6
el0_sync:
kernel_entry 0
mrs x25, esr_el1 // read the syndrome register
lsr x24, x25, #ESR_ELx_EC_SHIFT // exception class
cmp x24, #ESR_ELx_EC_SVC64 // SVC in 64-bit state
b.eq el0_svc
cmp x24, #ESR_ELx_EC_DABT_LOW // data abort in EL0
b.eq el0_da
cmp x24, #ESR_ELx_EC_IABT_LOW // instruction abort in EL0
b.eq el0_ia
......
在el0_sync汇编函数中,首先通过kernel_entry保存异常现场。然后从esr_el1寄存器中读取异常类型(EC),当异常类型为ESR_ELx_EC_SVC64是,跳转到 el0_svc 汇编函数。
// linux-5.4.18/arch/arm64/kernel/entry.S
/*
* SVC handler.
*/
.align 6
el0_svc:
gic_prio_kentry_setup tmp=x1
mov x0, sp
bl el0_svc_handler
b ret_to_user
ENDPROC(el0_svc)
el0_svc 汇编函数跳转到 el0_svc_handler 函数。
(2)C语言入口
// linux-5.4.18/arch/arm64/kernel/syscall.c
asmlinkage void el0_svc_handler(struct pt_regs *regs)
{
el0_svc_common(regs, regs->regs[8], __NR_syscalls, sys_call_table);
}
// linux-5.4.18/arch/arm64/include/asm/ptrace.h
/*
* This struct defines the way the registers are stored on the stack during an
* exception. Note that sizeof(struct pt_regs) has to be a multiple of 16 (for
* stack alignment). struct user_pt_regs must form a prefix of struct pt_regs.
*/
struct pt_regs {
union {
struct user_pt_regs user_regs;
struct {
u64 regs[31];
u64 sp;
u64 pc;
u64 pstate;
};
};
u64 orig_x0;
......
};
// linux-5.4.18/include/uapi/asm-generic/unistd.h
#define __NR_syscalls 436
// linux-5.4.18/arch/arm64/include/asm/syscall.h
typedef long (*syscall_fn_t)(const struct pt_regs *regs);
// linux-5.4.18/arch/arm64/kernel/sys.c
const syscall_fn_t sys_call_table[__NR_syscalls] = {
[0 ... __NR_syscalls - 1] = __arm64_sys_ni_syscall,
#include <asm/unistd.h>
};
static void el0_svc_common(struct pt_regs *regs, int scno, int sc_nr,
const syscall_fn_t syscall_table[])
{
regs->orig_x0 = regs->regs[0];
regs->syscallno = scno;
invoke_syscall(regs, scno, sc_nr, syscall_table);
}
static void invoke_syscall(struct pt_regs *regs, unsigned int scno,
unsigned int sc_nr,
const syscall_fn_t syscall_table[])
{
long ret;
if (scno < sc_nr) {
syscall_fn_t syscall_fn;
syscall_fn = syscall_table[array_index_nospec(scno, sc_nr)];
ret = __invoke_syscall(regs, syscall_fn);
}
regs->regs[0] = ret;
}
static long __invoke_syscall(struct pt_regs *regs, syscall_fn_t syscall_fn)
{
return syscall_fn(regs);
}
(3)流程简介
el0 svc
-->el1 vectors
-->kernel_ventry 0, sync // Synchronous 64-bit EL0
-->el0_sync
-->el0_svc
-->el0_svc_handler
-->el0_svc_common
-->invoke_syscall
-->__invoke_syscall
-->syscall_fn(regs)
从系统调用表中sys_call_table根据系统调用号取出对应的系统调用回调函数,然后去执行对应的系统调用回调函数。
参考资料
Linux 5.4.18
armv8手册
https://blog.csdn.net/luteresa/article/details/120263414
https://zhuanlan.zhihu.com/p/578252899
http://www.wowotech.net/238.html