Arm64学习笔记
本文并不会对啥指令都一一记录,只是会记录一些关键点,仅供参考。
加载指令
LDR和STR
ldr和str指令分别对应着
从内存地址当中加载数据到寄存器当中。从寄存器中捞出数据存放到内存当中。
指令变种
LDR寻址模式
地址偏移模式
地址偏移模式常常使用寄存器的值来表示一个地址,或者基于寄存器的值做一些偏移来计算出内存地址,并且把这个内存地址的值加载到通用寄存器中。偏移量可以是正数也可以是负数。
格式:LDR Xd, [Xn, #offset]
变基模式
存在两种形式:
- 前变基模式
例如:1
LDR X1, [X2, #8]! // X2的地址先+8然后再取到X2此时的内存地址所保存的数据加载到X1,注意X2的地址先变成为X2=X2+8
- 后变基模式
例如
LDR X1, [X2], #8 // 取X2的地址对应的数据加载到X1寄存器,然后X2再+8
LDR伪指令
指令: 每一条指令对应一种CPU操作。
伪指令:对编译器发出的命令,它是在对源程序汇编期间由汇编程序处理的操作,它们可以完成如处理器的选择、定义程序模式、定义数据、分配存储区、指示程序结束等功能,可以理解成几条指令的集合。(LDR伪指令没有立即数范围限制)
例如:LDR X0, =0x11111111111111111111111111
MOV和MOVZ
MOV指令是最简单和原始的加载指令
只能加载两种类型
- 16位立即数
- 16位立即数左移16、32、48位
MOVZ指令实际上等同于MOV指令
MOV
多字节的加载和存储LDP和STP指令
在A32指令集中提供LDM和STM指令来实现多字节内存加载和存储,而A64指令集不再提供LDM和STM指令,而采用LDP和STP指令。LDP和STP可以一条指令加载和存储16个字节,效率提高一倍。
示例:LDP X3, X7, [X0] // 以X0的值为地址,加载此地址的值到X3寄存器,以X0+8为地址,加载此地址的值到X7寄存器。
STP X1, X2, [X4] // 存储X1的值到地址为X4的内存中,然后存储X2的值到地址为X4+8的内存中。
移位和算术
条件状态码
条件状态码:在pstate处理器状态中4个条件操作码NCZV
条件标志域:
条件标志位|描述
–|:–:|
N|负数标志(上一次运算结果为负数)
Z|零结果标志(上一次运算结果为0)
C|无符号溢出|
V|有符号溢出|
ADD加法指令
普通的加法指令
- 使用寄存器的加法
- 使用立即数的加法
- 使用移位操作的加法
adds指令,该指令会影响条件标志位,主要影响C标志位(无符号数溢出)
SUB减法指令
- 普通的减法指令SUB
- SUBS指令——影响条件标志位(C标志位)
带进位的加法指令
格式:ADC <Xd>, <Xn>, <Xm>
- adc指令,在做加法运算的时候,结果还会加上C标志位上的值(0或1)即Rd = Rn + Rm + C
带进位的减法指令
格式: SBC <Xd>, <Xn>, <Xm>
- sbc指令,
Rd = Rn - Rm - 1 + C
CMP比较指令
XZR寄存器、wzr寄存器
两者都是零寄存器,xzr和wzr值总是为零;XZR、wzr寄存器是访问零值操作数而无需加载和占用实际寄存器
比较两个数的大小, 内部使用subs指令来实现,影响标志位C。该指令等同于SUBZ XZR, <Xn>, #<imm>
。
例子:
1 | CMP X1, X2 |
当X1 >= X2 时, C = 1
当X1 < X2 时, C = 0
比如3 >= 1,实际上就为3 + (-1),即3 + 0xfffffffffffffff,再加哪怕是1都溢出了所以c=1
所以我就想到了如何来判断条件是否符合可以如下:
1 | CMP X1, X2 |
移位操作
LSL: 逻辑左移指令
LSR: 逻辑右移指令
ASR: 算术右移
ROR: 循环右移
注意:
- 逻辑左移 = 算术左移, arm64没有单独设置一个算术左移的指令。
- 逻辑右移和算术右移需要考虑符号问题。
例子:
1010101010,右移:
- 逻辑右移一位: 0101010101 (最高位永远补0)
- 算术右移一位: 1101010101 (算术右移,左边添加的数和符号相关,这里的符号看最高位,1则补1)
按位与操作
AND: 与操作
ANDS: 带条件标志位的与操作,影响Z标志位
AND Xd, Xn
结果为Xd = Xd & Xn
按位或操作
ORR: 或操作
EOR: 异或操作
按位或操作
ORR Xd, Xn
指令结果: Xd = Xd | Xn
按位异或操作
EOR Xd, Xn
指令结果: Xd = Xd ^ Xn
异或操作规律
- 0异或任何数都为任何数,如:
0 ^ 0 = 0;
0 ^ 1 = 1;
- 1异或任何数都为任何数取反,如:
1 ^ 0 = 1;
1 ^ 1 = 0;
- 任何数异或自己相当于把自己置0。
异或操作的几个小妙用
使得某些特定位翻转。
如:
想把10101010的【第1(即0)和第2位(即1),因为是小端序】翻转,则可以将其与10101011进行按位异或运算。
10101010 ^ 10101011 = 10101001交换两个数
在汇编里让变量设置为0
EOR X0, X0判断两个是否相等
(a ^ b) == 0
位段操作
插入操作
bfi: 位段(bitfield)插入操作
格式:BFI Xd, Xn, #lsb, #width
释义:用Xn中的Bit[0:width]替换Xd中的从lsb开始的width位,Xd其他位不变。
提取操作
- UBFX:无符号数的位段提取指令
- SBFX:有符号数的位段提取指令
格式两者相同:SBFX Xd, Xn, #lsb, #width
释义:
从Xn寄存器提取位段,位段从第lsb位开始,位宽为width,然后结果写入到Xd寄存器最低比特位中。
位域提取指令ubfx的妙用
- 直接提取寄存器的某些域
bfxil:
语法:BFXIL Xd, Xn, #lsb, #width
释义:从Xn寄存器的第lsb位开始,提取width位,替换Xd寄存器的最低width位,剩余高位不改变。
零计数指令CLZ
计算最高位为1的比特位前面有几个0
格式:CLZ, Xd, Xn
例子
X1 = 0xffffffffffffffff
CLZ X2, X1
结果为4
注意:
- UBFX:其他比特位是填充0
- SBFX:其他比特位全是填充f
比较与跳转指令
- CMP: 比较两个数
- CMN: 负向比较(把一个数跟另外一个数的二进制补码相比较)
CMP X1, X2
=> x1 - x2CMN X1, X2
=> x1 + x2
条件操作后缀
条件选择指令
CSEL: 条件选择指令
CSET: 条件置位指令
CSINC: 条件选择并增加指令
条件选择指令常搭配CMP指令使用
CSEL Xd, Xn, Xm, COND
指令结果:
Xd = 如果cond为真, 返回Xn, 否则返回Xm
CSET Xd, cond
指令结果:
Xd = 如果cond为真,返回1否则返回0
CSINC Xd, Xn, Xm, cond
指令结果:
Xd = 如果cond为真,返回Xn,否则返回(Xm + 1)
例子
上图的意思,先比较X0是不是等于0,等于0则返回X1+2, 否则返回X1-1
基本跳转指令
无条件的跳转指令
- b:跳转指令,无条件的跳转指令,不返回。
- 跳转范围:PC +/- 128MB
一般用于arm跳转到c/cpp代码
有条件的跳转指令
- b.cnd:有条件的跳转指令,不返回。
- cnd为条件操作后缀
- 跳转范围:PC+/-1MB
这个指令实际上是b.cc这类代码的抽象指令,b.条件的意思
跳转范围较小,一般在同一个函数范围内进行使用。
bx指令
- bx:跳转到寄存器指定的地址处,不返回。
跳转范围手册没写,估计能跳转的范围很大。
带返回地址的跳转指令
bl中的l代表的是link,也就是链接的地址,也就是arm32当中的lr寄存器里的地址,arm64中的x30里的地址
- bl:带返回地址(PC+4=>X30,即返回的地址保存在X30里),适用于call子函数
- 返回地址:保存到X30中,=>保存的是父函数的PC+4,PC+4实际上就是父函数调用子函数之后并返回之后的下一条指令的地址。
- 跳转范围:PC+/-128MB
存在的陷阱:bl指令:用于call子函数。它把返回地址写入到x30寄存器,返回地址为PC+4。在一个函数里调用bl来call子函数,有可能会把父函数的lr寄存器给冲走,然后父函数的ret返回就跑飞了。
解决方法:
- 在遇到嵌套调用bl的时候,需要在父函数里,先把x30寄存器保存到一个临时寄存器里(x30等价为arm32里的lr寄存器)。
- 在父函数ret返回时,先从临时寄存器中恢复x30寄存器的值,再ret返回。
blx指令:
- blx:跳转到寄存器指定的地址处,可以返回。
- 返回地址:保存到X30中,=>保存的是父函数的PC+4
比较并跳转指令
cbz:比较xt寄存器是否为0,为0则跳转到label标签处,跳转范围为+/-1MB
cbnz:比较xt寄存器是否为0,不为0则跳转到label标签处,跳转范围为+/-1MB
tbz:测试寄存器中某个比特位是否为0,为0则跳转,跳转范围为+/-32KB
tbnz:测试寄存器中某个比特位是否为0,不为0则跳转,跳转范围为+/-32KB
不返回的意思
不返回的意思是跳转到子函数之后不会再跳转回原调用处。
PC相对地址加载指令
- adr指令:加载PC相对地址的label的地址,范围为+/-1MB
- adrp指令:加载PC相对地址的label的地址,它只加载label所属的4kb对齐的地址,范围为+/-4GB
例子
adr x0, label // 读取label的地址,相对PC的
adrp x0, lable // 读取label所在的4kb的地址,相对pc的
陷阱与坑:adrp和ldr究竟有什么不同
adrp读取的是物理地址,ldr读取的是虚拟地址(链接地址),ldr读取的是虚拟空间全局的,而adrp只能读取最大4GB
返回指令
ret指令
- ret指令: 从子函数返回。通常X30寄存器保存了返回地址。
eret指令
- eret: 从当前的异常模式返回。
- 通常可以实现模式的切换,例如EL1切换到EL0。它会从SPSR中恢复PSTATE,从ELR中获取跳转地址,并返回到该地址。如果SPSR保存的是EL1则会切回到EL1
特殊的寄存器
XZR零寄存器
XZR寄存器的值恒为0
NZCV寄存器
保存条件标志,该寄存器可读可写。
内存独占加载和存储指令
往内存地址标记独占,别的寄存器将不能访问,跟加锁线程的意思相仿。Linux内核常常用来实现atomic的访问,例如atomic_write和atomic_set_bit。spinlock机制可以简单地使用ldxr和stxr指令来实现。
ldxr: 内存独占加载指令。从内存中以独占exclusive方式加载内存的地址到通用寄存器。
stxr: 内存独占存储指令。
ldxr是在加载内存的时候通过独占监视器来监视这个内存的访问,监视器会把这个内存地址标记为独占访问,保证其独占的方式来访问。
stxr是与ldxr是一般是成对出现的。是有条件地存储内存,刚才ldxr标记的内存地址被独占的方式存储了。stxr存储只有当独占监视器还处于独占状态才会成功。stxr存储成功后,状态编程open状态。
ldxr和stxr的指令格式
ldxr xd, [xn | sp]
, 独占的读取xn或者sp地址的内容到xd寄存器stxr wd, xt, [xn | sp]
, 独占地方式把xt的内容写入到xn或者sp地址。Wd为0表示成功,Wd为1表示失败。
Arm的独占监视器
Arm里有两个独占监视器。
- 本地监视器:用来监视non-sharable的地址访问。
- 全局监视器:用来监视sharable的地址访问。
- 对于sharable的地址,本地监视器和全局监视器都会检测。
异常处理指令
SVC: 系统调用指令;允许应用程序通过SVC指令来自陷到操作系统里,通常会进入到EL1(内核)异常等级。
格式:SVC #imm
HVC: 虚拟化系统调用指令;允许主机操作系统通过HVC指令自陷到虚拟机管理程序(Hypervisor)中,通常会进入到EL2异常等级。
格式:HVC #imm
SMC: 安全监控系统调用指令;允许主机操作系统或虚拟机管理程序通过SMC指令自陷到安全监管(Secure Monitor)中,通常会进入到EL3异常等级。
格式: SMC #imm
系统寄存器访问指令
MRS: 读取系统寄存器到通用寄存器
MSR: 更新系统寄存器
内存屏障指令
数据存储屏障(Data Memory Barrier, DMV)指令
保证的是内存屏障前后的内存访问指令的执行顺序,不会保证内存访问指令在内存屏障指令之前必须完成。
数据同步屏障(Data synchronization Barrier, DSB)指令
任何指令都要等待DSB前面的存储访问完成
指令同步屏障(Instruction synchronization Barrier, ISB)指令
冲洗流水线(Flush Pipeline)和预取buffers后,才会从高速缓存或者内存中预期ISB指令之后的指令,该指令用于保证上下文切换、更改msid等。
对齐伪指令
.align对齐,填充数据来实现对齐。可填充0或者nop指令。
- 告诉汇编程序,align后面的汇编必须能被2^n整除的地址开始分配
- Arm64系统中,第一个参数表示2^n大小。
数据定义伪指令
.byte: 把8位数当成数据插入到汇编中
.hword:把16位数当成数据插入到汇编中
.long和.int: 把32位数当成数据插入到汇编中
.quad:把64位数当成数据插入到汇编中
.float:把浮点数当成数据插入到汇编中
.ascii “string” -> 把string当做数据插入到汇编中,ascii伪操作定义的字符串需要自行添加到结尾字符’\0’。
.asciz “string” -> 类似ascii,在string后面插入一个结尾字符’\0’。
.rept: 重复定义
例如:
.rept 3 .long 0
.long 0 => .long 0
.endr .long 0
.equ:赋值操作
.set:赋值操作
上面两条指令是相等的。
例子:
.equ abcd, 0x45 // 让abcd等于0x45
函数相关伪操作
.global: 定义一个全局的符号
.include: 引用头文件
.if , .else , .endif 控制语句
.if语句
.ifdef symbol: 判断symbol是否定义
.ifndef symbol: 判断symbol是否没有定义
.ifc string1, string2: 字符串string1和string2是否相等
.ifeq expression: 判断expression的值是否为0
.ifeqs string1, string2: 等同于.ifc
.ifge expression: 判断expression的值是否大于等于0
.ifle expression: 判断expression的值是否小于等于0
.ifne expression: 判断expression的值是否不为0
与段相关的伪操作
.section:
- 表示接下来的汇编会链接到哪个段里,例如代码段,数据段等
- 每一个段以段名开始,以下一个段名或文件结尾为结束
.section name, “flags”
后面可以添加flags, 表示段的属性。
.pushsection: 把下面的代码push到指定的section中
.popsection: 结束push
上面两者是成对使用,仅仅是把pushsection和popsection的圈出来的代码加入到指定section中,其他代码还是在原来的section
宏(难点)
.macro和.endm组成一个宏
.macro后面跟着的是宏的名称,在后面是宏的参数
在宏里使用参数,需要添加前缀”",例如:
.macro plus1 p, p1
定义了一个名为plus1的宏,有两个参数p和p1。在宏里使用参数需要前缀,”\p”表示第一个参数,”\p1”表示第二个参数宏参数定义的时候可以设置一个初始化值,例如:
.macro reserve_str p1=0 p2
第一个参数p1有一个初始化值0,这时候可以使用reserve_str a,b或者reserve_str, b来调用这个宏
示例:
生成一个以参数l为名称的符号
1 | .macro labal l |
或
1 | .altmacro |
把两个参数以及”.”连接在一起
1 | .macro opcode base length |
链接器
基本概念
输入段(input section)
gcc编译之后产生的.o文件,这些.o文件包含了代码段(.text)、数据段(.data)、.bss段以及一些自定义数据段,这些段统称输入段。
输出段(output section)
通过链接程序把编译生成的.o文件进行链接成最终成一个可执行文件,这个可执行文件也包含了代码段、数据段、.bss段等段,这些段统称为输出段
每个段包括name和链接大小。
段的属性:
loadable: 运行时会加载这些段的内容到内存中。
allocatable: 运行时不会加载段的内容。
段的地址:
- VMA (virtual memory address): 虚拟地址,就是运行时的运行地址。
- LMA (load memory address): 加载地址
通常ROM的地址为加载地址,而RAM的地址为VMA
ld命令(链接器知识)
linux中采用gnu的aarch64-linux-gnu-ld
这个程序进行链接器程序的调用,搭配参数使用
常用参数
-T 指定链接脚本
-Map 输出一个符号表文件
-o 输出最终可执行二进制文件
链接脚本命令
ENTRY(symbol): 设置程序的入口函数。
- 链接程序有如下几种方式来确定入口点:
- 使用-e参数
- 使用ENTRY(symbol)
- 在.text的最开始的地方
- 0地址
例如:ENTRY(_text)指定本链接脚本的入口函数为”_text”
INCLUDE filename
引入filename的链接脚本
OUTPUT filename
输出二进制文件,类似在命令行里使用”-o filename”
OUTPUT_FORMAT(bfd)
输出BFD格式
OUTPUT_ARCH(bfdarch)
输出处理器体系结构格式,例如:OUTPUT_ARCH(aarch64),用于指定本链接脚本针对arm64来进行链接
符号赋值
- 符号也可以像c语言一样赋值
例如:
symbol = expression;
symbol += expression;
symbol -= expression;
symbol *= expression;
symbol /= expression;
symbol <<= expression;
symbol >>= expression;
symbol &= expression;
symbol |= expression;
- “.” 表示location counter, 表示当前位置
例子:
1 | floating_point = 0; // 给符号floating_point赋值为0 |
符号的引用
高级语言(C语言)常常需要引用链接脚本定义的符号
在C语言里,定义一个变量并初始化变量。例如: int foo = 100;
- 编译器会在符号表中定义了一个符号foo
- 编译器会在内存中为符号foo存储100
链接脚本与C语言定义符号存在差异。在链接脚本中定义一个变量:
- 链接器仅仅在符号表里定义这个符号,没有分配内存来存储变量的值。
- 访问链接脚本定义的变量访问的是变量的地址,不能访问变量的值。
示例:
1 | 链接脚本: C语言: |
可以在每个段中设置一些符号,以方便C语言访问每个段的起始地址和结束地址。示例:
1 | SECTIONS |
上面这一段链接脚本通过设置start_of_text和end_of_text两个符号就可以通过这两个符号简单算出.text段的大小,以及很简单的获取.text段的起始地址和结束地址
SECTIONS命令
SECTIONS命令:告诉链接器如何把输入段(input sections)映射到输出段(output sections),以及如何在内存中摆放这些输出段。
格式:
1 | SECTIONS |
输出section的描述符
1 | section [address] [(type)]: // section段的名字 address(VMA,运行地址) type输出段的属性 |
LMA加载地址
- 每个段有VMA(虚拟地址,运行地址)以及LMA(加载地址)
- 在输出段描述符中使用”AT”来指定LMA
- 如果没有通过”AT”来指定LMA,通常LMA=VMA
- 构建一个基于ROM的映像文件常常会设置输出段的虚拟地址和加载地址不一致
示例:
1 | SECTIONS |
text段的虚拟地址和加载地址为0x1000(即.text段的VMA=LMA),mdata段的虚拟地址设置为0x2000,但是通过AT符号指定了加载地址是在text段的结束地址(即.mdata段的VMA!=LMA),_data指定了data段的虚拟地址为0x2000。bss段的虚拟地址是在0x3000.
常见的内建函数(builtin functions)
ADDR(section): 返回前面已经定义过的段的VMA地址
ALIGN(n): 返回下一个与n字节对齐的地址,它是基于当前的位置(location counter)来计算对齐地址的。(n是n个字节,不是2^n个字节)
示例:
1 | SECTIONS { |
SIZEOF(section): 返回一个段的大小
MAX(exp1, exp2) / MIN(exp1, exp2): 返回两个表达式的最大值回最小值
内嵌汇编
基础内嵌汇编 asm asm-qualifiers(AssemblerInstructions)
- 格式
- asm关键字: 表明这是一个GNU扩展
- 修饰词(qualifiers)
- volatile: 在基础内嵌汇编中通常不需要这个修饰词。用来关闭gcc指令优化
- inline: 内联,asm汇编的代码会尽可能小
- goto: 在内嵌汇编里会跳转到C语言的标签里
1 | asm volatile( |
指令部:
在内嵌汇编代码中,使用%0对应输出部和输入部的第一个参数,使用%1表示第二个参数。
输出部:用于描述在指令部中可以被修改的C语言变量以及约束条件
- 每个输出约束(constraint)通常以"="号开头,接着的字母表示对操作数类型的说明,然后是关于变量结合的约束。
"=/+" + 约束修饰符 + 变量
- 输出部通常使用"="或者"+"作为输出约束,其中"="表示被**修饰的操作数只具有可写属性**,"+"表示被修饰的操作数**只具有可读可写属性**
- 输出部可以是空的
输入部:用来描述在指令部只能被读取访问的C语言变量以及约束条件
- 输入部描述的参数是只有只读属性,不要试图去修改输入部的参数的内容,因为GCC编译器假定,输入部的参数的内容在内嵌汇编之前和之后都是一致的。
- 在输入部中不能使用"="或者"+"约束条件,否则编译器会报错。
- 输入部可以是空的。
损坏部(Clobbers):
- “memory”告诉GCC编译器内联汇编指令改变了内存中的值,强迫编译器在执行该汇编代码前存储所有缓存的值,在执行完汇编代码之后重新加载该值,目的是防止编译乱序。
- “cc”表示内嵌汇编代码修改了状态寄存器相关的标志位。
- 汇编代码块
- GCC汇编器把内嵌汇编当成一个字符串
- GCC编译器不会去解析和分析内嵌汇编。
- 多条汇编指令,需要使用”\n\t”来换行
- GCC的优化器,可以移动汇编指令的前后位置。如果你需要保持汇编指令的顺序,最后使用多个内嵌汇编的方式。
GCC内联操作符和修饰符
= : 被修饰的操作数只写
+ : 被修饰的操作数具有可读可写的属性
& : 被修饰的操作数只能作为输出
输出部和输入部的约束修饰符 - 通用
m: 内存变量
o: 操作数为内存变量,但是其寻址方式是偏移量类型
V: 操作数为内存变量,但寻址方式不是偏移量类型
r: 通用寄存器
i: 立即数
n: 立即数
p: 操作数是一个合法的内存地址(指针)
gcc内联汇编示例:
1 | static inline unsigned long array_index_mask_nospec(unsigned long idex, unsigned long sz) |
上图中r表示寄存器,%n是从输出部开始从0开始算起, I是约束修饰符的一种,下面有介绍
通过汇编符号名字来替代以前前缀%
1 | static void my_memcpy_asm_test(unsigned long src, unsigned long dst, unsigned long counter) |
输出部和输入部的约束修饰符 - ARM64
k: 栈指针寄存器(SP)
w: 浮点寄存器、SIMD、SVE寄存器
Up1: 使用p0~p7其中一个SVE寄存器
Upa: 使用p0~p15当中任意一个SVE寄存器
I: 整数常量,常常用于add指令
J: 整数常量,常常用于sub指令
K: 整数常量,常常用于32位逻辑指令
L: 整数常量,常常用于64位逻辑指令
M: 整数常量,常常用于32位的mov指令
N: 整数常量,常常用于64位的mov指令
S:绝对符号地址或标签引用
Y: 浮点常数,其值为0
Z:整数常数,其值为0
Ush: 在4GB内的PC相对地址的符号中的高地址部分
Q: 表示没有使用偏移量的单一寄存器的内存地址
Ump: 一个试用于SI、DI、SF和DF模式下的加载存储指令的内存地址
异常处理
arm64的异常等级
- EL0: 非特权模式,例如应用程序
- EL1: 特权模式,例如OS内核
- EL2: 虚拟化监控程序,例如hypervisor、kvm
- EL3: 安全模式, 例如secure monitor
arm64异常处理的一些基本概念
- Talking an exception: 正在处理一个异常
- Returning from an exception: 从一个异常中返回
- Exception levels: 异常等级
- Pricise exception: 精准异常
- Synchronous and asynchronous exception: 同步异常与异步异常
当异常发生时需要从低的EL切换到高的EL,例如EL0切EL1;当El1处理完成之后通过eret返回到el0。
同步异常与异步异常
同步异常
处理器需要等待异常处理的结果然后在继续处理下一条指令的异常处理方式就是同步异常。比如要访问一个数据,但是数据还没有准备好,此时会发生数据中止异常,此时处理器会返回发生异常的地址在哪里,此时操作系统里的异常处理的函数就要对其进行修复,比如mmu没有映射,触发异常,然后嵌入到操作系统的确认异常里面把地址进行修复,重新进行映射,然后application就可以访问该地址继而继续运行。
同步异常大致分为:
- 系统调用: svc、hvc、smc等
- MMU引发的异常
- SP和PC对齐检查
- 未分配的指令
异步异常
操作系统是无法感知接下来将要发生异常,所以异常处理对于异步异常返回的地址是不能说明是因为哪一条指令导致异常的发生。通常,异步异常指的是中断
中断发生时,处理器正在执行的指令和中断并没有关系,它们直接不存在依赖关系。
异步异常大致分为:
- IRQ中断
- FIQ中断
- SError
异常的入口
当异常发生时,CPU硬件做了什么事:
- PSTATE保持到SPSR_ELx。当异常发生在EL0则“x”就为1,因为异常会在el1里进行处理。
- 返回地址保持ELR_ELx,x同上
- PSTATE寄存器里的DAIF域都设置为1,相当于把调试异常、系统错误(SError)、IRQ中断以及FIQ中断都关闭了。
- 更新了ESR_ELx寄存器(该寄存器会告诉操作系统软件捕获到什么异常,包含同步异常的原因),里面包含了同步异常发生的原因
- SP执行SP_ELx
- 切换到对应的EL,然后跳转到异常向量表里执行。
当异常发生后,操作系统需要做哪些事情?
根据异常发生的类型,跳转到合适的异常向量表。异常向量表的每个表项会保存一个异常处理的跳转函数,然后跳转到恰当的异常处理函数并处理异常。
异常的返回
操作系统执行一条eret语句
- 从ELR_ELx寄存器中恢复PC指针
- 从SPSR_ELx寄存器恢复处理器的状态
异常返回地址
返回地址两个寄存器
- X30: 子函数的返回地址。使用ret指令来返回
- ELR_ELx: 异常返回地址。使用eret指令来返回
ELR_ELx寄存器保存了异常返回地址:
- 对于异步异常,它的返回地址是中断发生时的下一条指令,或者没有执行的第一条指令。
- 对于不是system call的同步异常,返回的是触发同步异常的那一条指令。
- 对于system call,它是返回svc指令的下一条指令。
异常处理的路由
- 异常发生的时候,异常处理可以在当前EL也可以在更高的EL。
- EL0不能用来处理异常
- 同步异常是可以在当前的EL里处理的,比如在EL1里发生了同步异常
- 对于异步异常,可以路由到EL1,EL2,EL3处理,需要配置HCR以及SCR相关寄存器。
栈的选择
- 每个异常等级EL都有对应栈指针寄存器SP
- SP_EL0, SP_EL1, SP_EL2, SP_EL3
- 栈必须16字节对齐。硬件可以检测栈指针是否对齐。
- 当异常发生时,并跳转到目标异常等级时,硬件会自动选择SP_ELx。
- 操作系统负责分配和保证每个异常等级EL对应的栈,是可用的。
异常处理的执行模式
当异常发生时,切换到高级别的EL,这个EL运行在哪个模式?Aarch64 or Aarch32?
- HCR_EL2.RW记录了EL1要运行在哪个模式?
- 1: 表示aarch64
- 0: 表示aarch32
- HCR_EL2.RW记录了EL1要运行在哪个模式?
当异常发生后,执行模式可以发生改变。
一个aarch32位的应用程序正在运行,这时候来了一个中断,它可能会跑到aarch64执行状态下的EL1里处理这个中断。
异常返回的执行模式
- 从一个异常返回时,SPSR寄存器记录了:
- 返回到哪个EL?SPSR.M[3:0]
- 返回目标EL的执行模式?SPSR.M[4]
- M[4] = 0, 表示aarch64; M[4] = 1, 表示aarch32;
M4记录了异常现场时的执行模式,保存了PSTATE.nRW的值
练习——从EL2切换到EL1
步骤如下:
- 设置HCR_EL2寄存器,最重要的是Bit 31的RW域,表示EL1要运行在哪个执行环境里?
- 设置SCTLR_EL1寄存器,需要设置大小端和关闭MMU。
- 设置SPSR_EL2寄存器,设置模式M域为EL1h,另外需要关闭所有的DAIF。
- 设置异常返回寄存器elr_el2,让其返回到el1_entry汇编函数里
- 执行eret
第三步-第四步实际上是一个异常返回的经典步骤,从EL2返回到EL1