一、实验目的
1.深刻理解中断的原理和机制
2.掌握CPU访问中断控制器的方法
3.掌握Arm体系结构的中断机制和规范
4.实现部分异常处理
二、实验过程
1.在bsp目录下新建文件prt_vector.S,构建异常向量表
.section .os.vector.text, "ax"
.global OsVectorTable
.type OsVectorTable,function
.align 13
OsVectorTable:
.set VBAR, OsVectorTable
.org VBAR // Synchronous, Current EL with SP_EL0
EXC_HANDLE 0 OsExcDispatch
.org (VBAR + 0x80) // IRQ/vIRQ, Current EL with SP_EL0
EXC_HANDLE 1 OsExcDispatch
.org (VBAR + 0x100) // FIQ/vFIQ, Current EL with SP_EL0
EXC_HANDLE 2 OsExcDispatch
.org (VBAR + 0x180) // SERROR, Current EL with SP_EL0
EXC_HANDLE 3 OsExcDispatch
.org (VBAR + 0x200) // Synchronous, Current EL with SP_ELx
EXC_HANDLE 4 OsExcDispatch
.org (VBAR + 0x280) // IRQ/vIRQ, Current EL with SP_ELx
EXC_HANDLE 5 OsExcDispatch
.org (VBAR + 0x300) // FIQ/vFIQ, Current EL with SP_ELx
EXC_HANDLE 6 OsExcDispatch
.org (VBAR + 0x380) // SERROR, Current EL with SP_ELx
EXC_HANDLE 7 OsExcDispatch
.org (VBAR + 0x400) // Synchronous, EL changes and the target EL is using AArch64
EXC_HANDLE 8 OsExcDispatchFromLowEl
.org (VBAR + 0x480) // IRQ/vIRQ, EL changes and the target EL is using AArch64
EXC_HANDLE 9 OsExcDispatch
.org (VBAR + 0x500) // FIQ/vFIQ, EL changes and the target EL is using AArch64
EXC_HANDLE 10 OsExcDispatch
.org (VBAR + 0x580) // SERROR, EL changes and the target EL is using AArch64
EXC_HANDLE 11 OsExcDispatch
.org (VBAR + 0x600) // Synchronous, L changes and the target EL is using AArch32
EXC_HANDLE 12 OsExcDispatch
.org (VBAR + 0x680) // IRQ/vIRQ, EL changes and the target EL is using AArch32
EXC_HANDLE 13 OsExcDispatch
.org (VBAR + 0x700) // FIQ/vFIQ, EL changes and the target EL is using AArch32
EXC_HANDLE 14 OsExcDispatch
.org (VBAR + 0x780) // SERROR, EL changes and the target EL is using AArch32
EXC_HANDLE 15 OsExcDispatch
.text
在向量表中,每组4类异常共16类异常均定义有其对应的入口,且其入口均定义为 EXC_HANDLE vecId handler 的形式
2.在 prt_reset_vector.S 中的 OsEnterMain: 标号后加入代码,进行异常向量表的设置
3.定义异常向量表中的EXC_HANDLE宏
这些宏定义用于实现一发生异常之后就立即保存CPU寄存器的值,然后跳转到对应的异常处理函数进行异常处理,该宏定义在prt_vector.S的开头:
4.在 src/bsp/prt_vector.S 文件中实现异常处理函数
OsExcDispatch:
mrs x5, esr_el1
mrs x4, far_el1
mrs x3, spsr_el1
mrs x2, elr_el1
stp x4, x5, [sp,#-16]!
stp x2, x3, [sp,#-16]!
mov x0, x1 // x0: 异常类型
mov x1, sp // x1: 栈指针
bl OsExcHandleEntry // 跳转到实际的 C 处理函数, x0, x1分别为该函数的第1,2个参数。
ldp x2, x3, [sp],#16
add sp, sp, #16 // 跳过far, esr, HCR_EL2.TRVM==1的时候,EL1不能写far, esr
msr spsr_el1, x3
msr elr_el1, x2
dsb sy
isb
RESTORE_EXC_REGS // 恢复上下文
eret //从异常返回
.globl OsExcDispatchFromLowEl
.type OsExcDispatchFromLowEl, @function
.align 4
OsExcDispatchFromLowEl:
mrs x5, esr_el1
mrs x4, far_el1
mrs x3, spsr_el1
mrs x2, elr_el1
stp x4, x5, [sp,#-16]!
stp x2, x3, [sp,#-16]!
mov x0, x1
mov x1, sp
bl OsExcHandleFromLowElEntry
ldp x2, x3, [sp],#16
add sp, sp, #16 // 跳过far, esr, HCR_EL2.TRVM==1的时候,EL1不能写far, esr
msr spsr_el1, x3
msr elr_el1, x2
dsb sy
isb
RESTORE_EXC_REGS // 恢复上下文
eret //从异常返回
此处实现了异常处理函数OsExcDispatch 和 OsExcDispatchFromLowEl,观察异常向量表可以发现,前一个异常处理函数用于大多数异常情况,而后一个异常处理函数则处理用户态引发的系统调用。
继续分析可以发现,这两个汇编函数的基本架构十分类似,都是将系统寄存器esr_el1、far_el1、spsr_el1、和elr_el1的值保存入栈,然后调用在另外的C语言文件中定义的实际异常处理函数OsExcHandEntry以及OsExcHandleEntry,最后当异常处理函数运行结束后,再恢复这些系统寄存器的值,即实现了上下文的保存与恢复,这里有一个需要注意的点,在上下文恢复时执行了指令add sp, sp, #16去跳过far_el1、esr_el1这两个系统寄存器的恢复,原因是当HCR_EL2.TRVM等于1时,EL1的异常等级没有权限修改这两个寄存器,一旦修改会陷入到EL2等级进行异常处理。
上述提及的四个系统寄存器的作用分别为:
1.esr_el1:该寄存器保存最后一次异常的详细信息,包括异常类型、异常原因等
2.far_el1:该寄存器用于保存最后一次发生内存访问异常的虚拟地址
3.spsr_el1:该寄存器保存异常发生时的程序状态(条件码、中断屏蔽位等)
4.elr_el1:该寄存器保存异常返回地址,用于异常处理结束后返回程序的正常运行位置
5.实现C语言异常处理函数
在bsp目录下新建prt_exc.c 文件,实现汇编函数中实际的 OsExcHandleEntry 和 OsExcHandleFromLowElEntry 异常处理函数:
#include "prt_typedef.h"
#include "os_exc_armv8.h"
extern U32 PRT_Printf(const char *format, ...);
// ExcRegInfo 格式与 OsExcDispatch 中寄存器存储顺序对应
void OsExcHandleEntry(U32 excType, struct ExcRegInfo *excRegs)
{
PRT_Printf("Catch a exception.\n");
}
// ExcRegInfo 格式与 OsExcDispatchFromLowEl 中寄存器存储顺序对应
void OsExcHandleFromLowElEntry(U32 excType, struct ExcRegInfo *excRegs)
{
PRT_Printf("Catch a exception from low exception level.\n");
}
6.定义系统寄存器结构体ExcRegInfo
因为我们定义的两个异常处理函数的第2个参数是 struct ExcRegInfo * 类型,而在汇编函数中传入的参数是异常类型和栈指针,所以我们需要定义一个系统寄存器结构体来接收从栈顶开始的系统寄存器,同时结构体中的变量顺序应该与栈中自栈顶至栈帧的寄存器顺序一致:
7.将新创建的文件纳入构建系统:
按照指导书说明将start.S中的FPU注释掉,发现执行后没有输出,具体原因在后文中的作业进行介绍
8.启用FPU,然后在main函数中通过SVC实现一条系统调用
在main函数中使用OS_EMBED_ASM(被定义为__asm__ __volatile__),进行C与ASM的混合编程,在汇编代码中,首先设置异常发生时的状态为0b00000(保存于系统状态寄存spsr_el1),然后保存异常结束后返回后的地址为EL0Entry,设置systemcall number为1,使用SVC进行系统调用。
在内联汇编中,"Clobbers"用于指示可能会被汇编代码更改的寄存器,例如汇编代码修改了EAX和EBX寄存器,"Clobbers"列表将为"eax", "ebx"
9.最后修改 OsExcHandleFromLowElEntry 函数实现 1 条系统调用
此处先取出ESR系统寄存器中的高六位,表示异常类型,然后取出寄存器x0、x8的值,并打印出相应的参数,表明顺利完成系统调用。
三、测试与分析
1.构建项目,正常运行使用系统调用
四、Lab4作业
查找启用FPU前异常出现的位置和原因。禁用FPU后PRT_Printf工作不正常,需通过调试跟踪查看异常发生的位置和原因 elr_el1 esr_el1寄存器。
1.首先在start.S中注释掉启用FPU的汇编代码,禁用FPU
2.然后运行程序,并在另一个终端中启动调试客户端
3.逐步调试分析,查看何处产生异常,并跳转进入异常处理函数
调试发现在main函数内的第四条指令运行之后产生异常,进入异常处理函数
4.反汇编main函数,查看异常产生的原因
5.查看elr_el1和esr_el1寄存器中的值
elr_el1为0x400021e8,根据上文的分析可知该寄存器保存的是异常返回的地址
esr_el1的值为0x1fe00000,转换为二进制:0b00011111111000000000000000000000记录了异常的详细信息,查阅ARM用户手册以及其他资料,对这个寄存器的值进行分析:
该寄存器的31-26位表示异常原因:
0b000111,经查阅资料知本处异常由访问SME、SVE或浮点数相关功能引起,与上文调试分析的原因一致
该寄存器的25位IL表示异常时的指令长度:
0表示16位指令,1表示32位指令,很显然我们的指令为32位,故被设置为1
该寄存器的24-0位表示指定异常后的一些附带信息,表明同一EC异常下的不同异常情况
此处设置为0b1111000000000000000000000,表示浮点数相关功能异常
分析发现,由于此处异常由PRT_Printf函数中访问浮点寄存器引起,并且每次异常处理又会调用PRT_Printf函数,故陷入无限递归,所以禁用FPU后发现运行程序没有任何输出,并且调试发现陷入异常后也不会正常返回,所有异常均无法正常结束。
五、心得体会
1.深刻理解了系统调用的原理和机制,掌握CPU访问设备控制器的方法。
2.掌握Arm体系结构的系统调用机制和规范,实现部分异常处理等。