本文主要从ARM汇编的使用者角度,首先介绍各种通用和系统寄存器,接着介绍发生异常时对各种系统寄存器的使用。随后介绍了ARM的指令、调用惯例和汇编宏的使用。最后以对一个裸机启动汇编代码的分析结束了本文。
1. ARMv8编程模型总览
1.1. 寄存器
r0
…r30
:64位通用寄存器x0
…x30
是这些寄存器64位的别名w0
…w30
是这些寄存器32位的别名
lr
:64位link register;是x30
的别名bl <addr>
指令会将下一条指令的地址存储在lr
中,然后跳转到addr
。ret
指令将PC设置为lr
中的值。
sp
:栈指针- 栈指针的低32位能够被通过
wsp
访问,栈指针必须总是16字节对齐的
- 栈指针的低32位能够被通过
pc
:程序计数器- 只读,
pc
会在分支指令和异常进入和返回时被更新
- 只读,
v0
…v31
:128位SIMD和FP浮点寄存器- 这些寄存器被用于向量化SIMD指令和浮点数操作。这些寄存器通过别名访问。
q0
…q31
是这些寄存器128位的别名。d0
…d31
是低64位的别名。此外,低32位、16位和8位分别通过s
,h
和b
进行别名化。
- 这些寄存器被用于向量化SIMD指令和浮点数操作。这些寄存器通过别名访问。
xzr
:只读0寄存器- 这是一个伪寄存器,不一定存在对应的硬件寄存器,总是0.
1.1.1. PSTATE
PSTATE是一个伪寄存器。无法直接对它执行读/写操作。然而,存在能够被用于读/写PSTATE各个域的特殊用途寄存器,如下:
NZCV
: condition flagsDAIF
: exception mask bits, used to prevent exceptions from being issuedCurrentEL
: the current exception levelSPSel
: stack pointer selector
这些寄存器属于系统寄存器(system registers)或是特殊寄存器(spectial registers)。必须使用mrs
和msr
读写。例如,读NZCV
到x1
需要mrs x1, NZCV
1.2. 异常相关
Raspberry Pi的CPU上电后处于EL3。自带的固件固件会在EL3运行,切换到EL2,然后运行kernel8.img
文件。
1.2.1. ELx寄存器
一些系统寄存器例如ELR
,SPSR
和SP
在每个异常级别都存在备份。这些寄存器带有_ELn
的后缀。
- 例如
ELR_ELx
,其中x
后缀表示目标异常级别(target exception level)。目标异常级别是CPU将要切换进入并执行异常向量的异常级别。 - 例如
SP_ELs
,其中s
后缀表示源异常级别(souce exception level)。源异常级别是异常发生时,CPU处于的异常级别。
1.2.2. 异常级别切换
异常级别提升和下降都仅存在一种机制。
- 下降:必须使用
eret
返回,执行eret
指令时当前异常级别为ELx
,则CPU会- 设置PC为
ELR_ELx
中的值 - 设置PSTATE为
SPSR_ELx
中的值。SPSR_ELx
寄存器也包含了将返回的异常级别。注意到改变异常级别也会有下面的影响:- 当返回到
ELs
时,若SPSR_ELx[0] == 1
,sp
被设置为SP_ELs
;若SPSR_ELx[0] == 0
,sp
被设置为SP_EL0
.
- 当返回到
- 设置PC为
- 提升:仅可能作为异常发生的结果。当到
ELx
的切换发生时,CPU会- 设置
PSTATE.DAIF = 0b1111
掩盖所有异常和中断 - 保存
PSTATE
和其他域到SPSR_ELx
- 保存preferred exception link address到
ELR_ELx
- 若
SPSel
被设为1,则设置sp
到SP_ELx
- 保存exception syndrome到
ESR_ELx
- 设置
pc
到异常向量对应项的地址
- 设置
1.2.3. 异常向量
一共存在4种类型的异常,每个对应4种可能的异常源,因而总共由16种异常向量。四种异常类型是:
- 同步
- IRQ
- FIQ
- SError
四种来源是
- 当
SP = SP_EL0
,相同的异常级别 - 当
SP = SP_ELx
,相同的异常级别 - 运行AArch64的低异常级别
- 运行AArch32的低异常级别
“When an exception occurs, the processor must execute handler code which corresponds to the exception. The location in memory where [an exception] handler is stored is called the exception vector. In the ARM architecture, exception vectors are stored in a table, called the exception vector table. Each exception level has its own vector table, that is, there is one for each of EL3, EL2 and EL1. The table contains instructions to be executed, rather than a set of addresses [as in x86]. Each entry in the vector table is 16 instructions long. Vectors for individual exceptions are located at fixed offsets from the beginning of the table. The virtual address of each table base is set by the [special-purpose] Vector Based Address Registers VBAR_EL3, VBAR_EL2 and VBAR_EL1.”
如上,EL3,EL2,EL1均有自己的异常向量表。向量表中每项即异常处理器,包含最长16条指令。每个向量表的基地址的虚拟地址存储在Vector Based Address Registers中,即VBAR_EL3, VBAR_EL2, VBAR_EL1
。
向量在物理上的布局如下:
1.3. 小结
- 别名
x30
的寄存器?w30
和lr
- 使用
ret
设置PC为地址A
?- 用
mov
写lr
为A
然后用ret
- 用
- 使用
eret
设置PC为地址A
?- 用
mrs
写ELR_ELx
- 用
- 决定当前异常级别?
- 读
CurrentEL[3:2]
,数值代表异常级别
- 读
- 在异常返回时改变源栈指针?
- 使用
msr
改变SP
,然后设置SPSR_ELx[0] == 1
- 使用
EL0
进程执行svc
指令,CPU会跳转到哪个地址?VBAR_EL1
加0x400
EL0
发生了timer中断,CPU会跳转到哪个地址?VBAR_EL1
加0x480
- 如何仅掩盖IRQ异常?
- 写
PSTATE.DAIF = 0b0010
- 写
- 源异常级别位于AArch64,如何
eret
进入AArch32执行状态?- 写
SPRSR.M
域为1
- 写
2. 指令
2.1. 访存
ldr <ra> [<rb>]
str <ra> [<rb>]
其中<rb>
为base register。
ldr r0, [r3, #64] // r0 = *(r3 + 64)
str r0, [r3, #-12] // *(r3 - 12) = r0
post-index访存,基地址在指令执行完后改变
ldr r0, [r3], #30 // r0 = *r3; r3 += 30
str r0, [r3], #-12 // *r3 = r0; r3 -= 12
pre-index访存,基地址在指令执行前改变
ldr r0, [r3, #30]! // r3 += 30; r0 = *r3
str r0, [r3, #-12]! // r3 -= 12; *r3 = r0
上述post-index和pre-index都是寻址模式。
一次性load,store两个寄存器可以使用ldp
和stp
(load pair, store pair)。
1 |
|
2.2. 装载立即数
- movk(move/keep):load a 16-bit immediate shifted by left some number of bits without replacing any of the other bits
1 |
|
立即数需要带#
前缀,LSL
表示逻辑左移。
仅16位立即数带可选的移位能够被装载进寄存器。编译器一般能够识别右移的值,例如能够自动将mov x12, #(1 << 21)
自动转换为mov x12, 0x20, LSL #16
2.3. 从符号装载地址
1 |
|
为了装载符号后第一条指令的地址,可以使用adr
或ldr
指令
1 |
|
若符号和指令不在同一个linker节则必须使用ldr
。如果在同一个节,可以使用adr
。
2.4. 寄存器间移动
mov
msr
和mrs
2.5. 算术
add <dest> <a> <b> // dest = a + b
sub <dest> <a> <b> // dest = a - b
参数<b>
也能为一个立即数
1 |
|
2.6. 逻辑指令
and
,orr
1 |
|
2.7. 分支
无条件跳转
b label
1 |
|
br
blr
1 |
|
2.7.1. 条件分支
cmp
指令比较两个寄存器/一个寄存器一个立即数的值,并为之后的条件分支指令例如bne
,beq
和blt
设置flag
1 |
|
- bne
- neq
- blt: less than
- ble: less than or equal
- bgt: greater than
- bge: greater than or equal
cbz <ra> <label>
: compare, branch on zerocbnz <ra> <label>
: compare, branch if not zero
2.8. 小结
2.8.1. 实现memcpy
源地址为x0
,目标地址为x1
,拷贝的字节数为x2
,保证非0,且为8的整数倍。
1 |
|
2.8.2. 将0xABCED
写入ELR_EL1
1 |
|
3. 调用惯例(Calling Convention)
AArch64 requires the SP register to be 16-byte aligned whenever it is used as part of a load or store.
在通过异常向量表调用rust编写的hanle_exception
函数的时候,我们必须遵守调用惯例(calling convention, or procedure call standard)。调用惯例指的是下面一系列规则:
- 如何向一个函数传参
- 在AArch64中,前8个参数通过
r0
…r7
从左至右传递。
- 在AArch64中,前8个参数通过
- 如何从一个函数返回值
- 在AArch64中,前八个返回值通过
r0
…r7
传递。
- 在AArch64中,前八个返回值通过
- 函数必须保存哪些状态(registers,stack,etc)
- calleed-saved寄存器:
r19
…r29
和SP
。其余通用意图寄存器都是caller-saved,包括lr
(x30)。SIMD/FP较为复杂。。。
- calleed-saved寄存器:
- 如何返回调用者
- 在AArch64中,
ret
通过lr
(link address)返回
- 在AArch64中,
注意:严格遵守调用惯例会阻止所有函数调用和函数体内的优化。因此,Rust的函数默认情况下并不保证遵守惯例。为了强迫Rust根据目标平台惯例编译,需要使用extern
关键字
4. 汇编宏
1 |
|
5. 一个实例:bare metal启动
1 |
|
总的来说,首先除了核0外的其它核都低功耗循环,核0从EL3到EL2到EL1,然后执行kinit
。中间过程中没有虚拟内存、数据/指令cache。但使用AArch64,小端法。