基本概念
ARM 体系结构是一种硬件规范,主要用来约定指令集,为了降低客户基于 ARM 体系结构开发处理器的难度,ARM 根据不同的应用开发需求开发出箭筒体系结构的处理器 IP,然后授权给客户,比如 ARMv8 体系结构开发出的处理器 IP 有:Cortex-A53, Cortex-A55, Cortex-A72, Cortex-A73,.
ARMv8 体系结构特性
- 提供超过 4G 物理内存空间
- 具有 64 位宽的虚拟地址空间,32 位宽的虚拟地址空间只能供 4GB 大小的虚拟空间访问,限制了桌面操作系统和服务器的应用。
ARMv8 体系结构包含多少个通用寄存器?
- 提供 31 个 64 位宽的通用寄存器。分别是 X0~X30 。可以减少对栈的访问,从而提高性能。
- 提供 16KB 和 64KB 的页面,有助于降低 TLB 的未命中率。
- 具有全新的异常处理模型,降低操作系统和虚拟化的实现复杂度。
A64 指令集和 A32 指令集是不兼容的,它们是完全不一样的指令集,它们的指令编码时不一样的,另外 A64 指令集的指令宽度是 32 位,而不是 64 位。
ARMv8 处理器支持两种执行状态, AArch64 和 AArch32 ,当处理器在 AArch64 状态时,运行 A64 指令集,而处理器在 AArch32 状态时,运行 A32 和 T32 指令集,
AArch64 执行状态包含多少个异常等级?分页有什么作用?
AArch64 执行状态的异常等级确定了处理器当前运行的特权级别,类似特权等级。
- EL0: 用户特权,用于运行普通用户程序
- EL1: 系统特权,通常用于操作系统内核,如果系统使能了虚拟化扩展,运行虚拟机操作系统内核。
- EL2: 运行虚拟化扩展的虚拟机监控器
- EL3: 运行安全世界中的安全监控器
ARMv8 支持的数据宽度
- 字节(byte): 8位
- 半字(halfword): 16位
- 字(word):32位
- 双字(doubleword): 64位
- 四字(quadword): 128位
ARMv8 寄存器
31个通用寄存器
在 AArch64 状态下,使用 X(X0、X30) 表示 64 位通用寄存器,另外可以使用 W 来表示低 32 位的数据,如 W0 表示 X0 寄存器的低 32 位数据, W1 表示 X1 寄存器的低 32 位数据。
处理器的状态
AArch64 体系结构使用 PSTATE 寄存器来保存当前处理器状态
条件标志位: N(负位) Z(零位) C(进位) V(溢出标志位)
特殊寄存器
- 零寄存器
ARMv8 有两个零寄存器,这些寄存器的内容全是 0 ,可以用作源寄存器,也可以用作目标寄存器。WZR 是32位的零寄存器,XZR 是64位的零寄存器。
- PC 指针寄存器
通常用来指向当前运行指令的下一条指令的地址,用于控制程序的运行顺序。
- SP 寄存器
ARMv8 支持 4 个异常等级,每个异常等级都有一个 SP 寄存器
- SP_EL0: EL0 下的 SP 寄存器(最低等级)
- SP_EL1: EL1 下的 SP 寄存器
- SP_EL2: EL2 下的 SP 寄存器
- SP_EL3: EL3 下的 SP 寄存器
当处理器运行在 EL0 时,它只能访问 SP_EL0 ,不能访问其他高级的 SP 寄存器。
- 备份程序状态寄存器
当我们运行一个异常处理程序时,处理器的备份程序会保存到备份程序状态寄存器(SPSR)里面,当异常要发生时,处理器会把 PSTATE 寄存器的值暂时保存到 SPSR 里,当异常处理完成返回时,再把 SPSR 寄存器的值恢复到 PSTATE 寄存器。
- ELR
存放异常返回地址
- CurrentEL 寄存器
表示 PSTATE 寄存器中的 EL 字段(当前异常等级),该寄存器保存了当前的异常等级,可以使用 MRS 指令读取当前异常等级。
- DAIF 寄存器
表示 PSTATE 寄存器中的{D, A, I, F}字段(异常掩码标志位)。
- SPSel 寄存器
表示 PSTATE 寄存器中的 SP 字段,用于选择某个 SP_ELn 寄存器。
- PAN 寄存器
表示 PSTATE 寄存器中 PAN(特权禁止访问)字段。可以通过 MSR 和 MRS 指令来设置 PAN 寄存器,主要是为了防止内核态恶意访问用户态内存而新增的,所以需要调用内核提供的结构,比如 copy_from_user()
或者 copy_to_user()
函数。
- 0: 表示在内核态可以访问用户态内存
- 1: 表示在内核态访问用户态内存会触发一个访问权限异常
- UAO 寄存器
表示 PSTAYE 寄存器中的 UAO (用户访问覆盖) 字段。同样可以使用 MSR 和 MRS 指令来设置,
UAO=1 时,表示在 EL1 和 EL2 执行非特权指令(例如 LDTR 和 STTR)的效果和特权指令(例如 LDR、STR)是一样的。
- NACV 寄存器
表示 PSTATE 寄存器中的{N, Z, C, V}字段 (条件标志位)。
系统寄存器
系统寄存器支持不同的异常等级的访问,通常系统寄存器会使用“Reg_ELn"的方式来表示
- Reg_EL1: 处理器处于 EL1、EL2 和 EL3 时可以访问该寄存器
- Reg_EL2: 处理器处于 EL2 和 EL3 时可以访问该寄存器
除了 CTR_EL0 ,大部分系统寄存器不支持处理器处于 EL0 时范访问。
加载与存储指令
在 ARMv8 体系结构下,所以的数据都需要在通用寄存器中完成,而不能直接在内存中完成,因此,首先把待处理的数据从内存加载到通用寄存器,再进行数据处理,最后再把结果写入到内存中。
- LDR 内存加载指令
- STR 存储指令
LDR 目标基础漆,<存储器地址> //把存储器地址中的数据加载到目标寄存器中
STR 源寄存器, <存储器地址> //把源寄存器的数据存储到存储器中
基于基地址的寻址模式
基地址模式
- 使用寄存器的值来表示一个地址
- 把这个内存地址的内容加载到通用寄存器中
LDR Xt, [Xn] //把 Xn 寄存器中的内容作为内存地址,并把这个内存地址的内容加载到 Xt 寄存器中
STR xt, [Xn] //把 Xt 寄存器中的内容存储到 Xn 寄存器的内存地址中
基地址+偏移地址模式
- 基地址 + 偏移地址 = 内存地址
- 把这个内存地址的值加载到通用寄存器中
LDR Xt, [Xn. #offset] //Xn寄存器中的内容加一个偏移量(offset 必须是 8 的倍数),以相加的结果作为内存地址,加载这个内存地址的内容到 Xt 寄存器中。
STR Xt,[Xn. #offset] //把 Xt 寄存器的值存储到以 Xn 寄存器的值加一个偏移量表示的内存地址中
基地址扩展模式
LDR <Xt>, [<Xn>, (<Xm>) {, <extend> {<amount>}}]
STR <Xt>, [<Xn>, (<Xm>) {, <extend> {<amount>}}]
- Xt: 目标寄存器
- Xn:基地址寄存器
- Xm,偏移的寄存器
- extend:扩展/移位 指示符,默认是 LSL(逻辑左移),UXTW(从寄存器中提取32位,其余高位填充0),SXTW(从寄存器中提取32位,其余高位须有符号扩展),SXTX(从寄存器中提取64位数据)
- amount:索引偏移量,当 extend 不是 LSL 时有效
LDR X0, [X1, X2 LSL #3] //内存地址为 X1 寄存器的值(X2 寄存器的值<<3),加载这个内存地址的值到 X0 寄存器
LDR X0, [X1, W2, SXTW, #3] //先对 W2 的值做有效符号扩展,然后左移 3 位,和 X1 寄存器的值相加后得到内存地址,加载这个内存地址的值到 X0 寄存器
变基模式
- 前变基模式:先更新偏移量地址,后访问内存地址
- 后变基模式:先访问内存地址,后更新偏移量地址
前变基模式
内存加载指令
LDR Xt, [<Xn|SP>, #<simm>]
- 首先,Xn/SP 寄存器的值 = Xn/SP 寄存器的值 + simm
- 以新的 Xn/SP 寄存器的值作为内存地址,并加载这个内存地址的值到 Xt 寄存器
存储指令
STR Xt, [<Xn|SP>, #<simm>]
- 首先,Xn/SP 寄存器的值 = Xn/SP 寄存器的值 + simm
- 以新的 Xn/SP 寄存器的值作为内存地址,然后把 Xt 寄存器的值存储到这个内存单元中
后变基模式
内存加载指令
LDR Xt, [<Xn|SP>, #<simm>]
- 首先,加载 Xn/SP 寄存器的值到 Xt 寄存器
- 然后更新,Xn/SP 寄存器的值 = Xn/SP 寄存器的值 + simm
存储指令
STR Xt, [<Xn|SP>, $<simm>]
- 首先,将 Xt 寄存器的值加载到 Xn/SP 寄存器的值为内存地址的内存单元中
- 然后更新,Xn/SP 寄存器的值 = Xn/SP 寄存器的值 + simm
易混例子
//(X1的值不变)
LDR X0, [X1, #8] //内存地址为 X1的值+8,加载此内存地址的值到 X0 寄存器
//(X1的值改变)
LDR X0, [X1, #8]! //前变基模式,先更新 X1 寄存器的值 = X1 寄存器的值 + 8,然后将 X0 的值加载到新的 X1 寄存器值对应的内存单元中
//(X1的值改变)
LDR X0, [X1], #8 //后变基模式,以 X1 的值作为内存地址,加载该内存地址的值到 X0 中,然后更新 X1 寄存器的值 = X1 寄存器的值 + 8
STP X0, X1, [SP, #-16]! //把 X0 和 X1 寄存器的值压回栈中
LDP X0, X1, [SP], #16 //把 X0 和 X1 寄存器的值抬出栈
PC相对地址模式
可以使用 lable 标签来标记代码片段,LDR 指令可以访问标签的地址
LDR Xt <label>
读取 label 所在内存地址的内存到 Xt 寄存器中,但是这个 label 必须在当前 PC地址后 1MB 的范围内,否则会报错。
my_data:
.word 0x40
ldr x0, my_data // 最终 X0 寄存器的值为 0x40
假设当前 PC 的地址是 0x806E4, 那么这条 LDR 指令读取 0x806E4 + 0x20 地址的内容到 X6 寄存器中。
#define MY_LABEL 0x20
ldr x6, MY_LABEL
// error 0x100000 的偏移量超超出 1MB 范围
#define MY_LABEL_1 0x100000
ldr x6, MY_LABEL_1
LDR 伪指令
伪指令是对汇编器发出的指令,伪指令可以分解为几条指令的集合。
LDR 指令既可以在大范围内加载地址的伪指令,也可以是普通的内存访问指令。当第二个参数前面有 “=” 时,表示伪指令,否则表示普通的内存访问指令。
LDR Xt, =<label> //把label标记的地址加载到 Xt 寄存器
#define MY_LABEL 0x20
ldr x6, =MY_LABEL
//这里的 ldr 是一条伪指令,它会把 MY_LABEL 宏的值加载到 X6 寄存器中
my_data1:
.quad 0x8
ldr x5, =my_data1
ldr x6, [x5]
//my_data1 定义了一个数据为 0x8,第一条 LDR 是伪指令,它把 my_data1 对应的地址加载到 X5 寄存器中
//第二条 LDR 是普通的内存访问指令,以 X5 寄存器的值为内存地址,加载这个地址的内容到 X6 中
简而言之,伪指令是把地址给目标寄存器,而普通指令是把地址的值给目标指令
linux 内核的 head.S 中,启动 MMU 之后就使用这个特性实现从运行地址定位到链接地址。
secondary_startup:
bl __cpu_secondary_check52bitva
bl __cpu_setup // initialise processor
bl __enable_mmu
ldr x8, =__secondary_switched //跳转到__secondary_switched函数,该函数的地址是链接地址(内核空间的虚拟地址),
// 在这之前CPU运行在实际物理地址上,实现了地址重定位功能
br x8
ENDPROC(secondary_startup)