ARM架构(三)ARMv8 Programm Model Overview

本文主要从ARM汇编的使用者角度,首先介绍各种通用和系统寄存器,接着介绍发生异常时对各种系统寄存器的使用。随后介绍了ARM的指令、调用惯例和汇编宏的使用。最后以对一个裸机启动汇编代码的分析结束了本文。

1. ARMv8编程模型总览

1.1. 寄存器

  • r0r30:64位通用寄存器
    • x0x30是这些寄存器64位的别名
    • w0w30是这些寄存器32位的别名
  • lr:64位link register;是x30的别名
    • bl <addr>指令会将下一条指令的地址存储在lr中,然后跳转到addrret指令将PC设置为lr中的值。
  • sp:栈指针
    • 栈指针的低32位能够被通过wsp访问,栈指针必须总是16字节对齐的
  • pc:程序计数器
    • 只读,pc会在分支指令和异常进入和返回时被更新
  • v0v31:128位SIMD和FP浮点寄存器
    • 这些寄存器被用于向量化SIMD指令和浮点数操作。这些寄存器通过别名访问。q0q31是这些寄存器128位的别名。d0d31是低64位的别名。此外,低32位、16位和8位分别通过shb进行别名化。
  • xzr:只读0寄存器
    • 这是一个伪寄存器,不一定存在对应的硬件寄存器,总是0.

1.1.1. PSTATE

PSTATE是一个伪寄存器。无法直接对它执行读/写操作。然而,存在能够被用于读/写PSTATE各个域的特殊用途寄存器,如下:

  • NZCV: condition flags
  • DAIF: exception mask bits, used to prevent exceptions from being issued
  • CurrentEL: the current exception level
  • SPSel: stack pointer selector

这些寄存器属于系统寄存器(system registers)或是特殊寄存器(spectial registers)。必须使用mrsmsr读写。例如,读NZCVx1需要mrs x1, NZCV

1.2. 异常相关

Raspberry Pi的CPU上电后处于EL3。自带的固件固件会在EL3运行,切换到EL2,然后运行kernel8.img文件。

1.2.1. ELx寄存器

一些系统寄存器例如ELRSPSRSP在每个异常级别都存在备份。这些寄存器带有_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] == 1sp被设置为SP_ELs;若SPSR_ELx[0] == 0sp被设置为SP_EL0.
  • 提升:仅可能作为异常发生的结果。当到ELx的切换发生时,CPU会
    • 设置PSTATE.DAIF = 0b1111掩盖所有异常和中断
    • 保存PSTATE和其他域到SPSR_ELx
    • 保存preferred exception link address到ELR_ELx
    • SPSel被设为1,则设置spSP_ELx
    • 保存exception syndromeESR_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

向量在物理上的布局如下:

arm-vector-1.png arm-vector2.png

1.3. 小结

  • 别名x30的寄存器?
    • w30lr
  • 使用ret设置PC为地址A
    • movlrA然后用ret
  • 使用eret设置PC为地址A
    • mrsELR_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两个寄存器可以使用ldpstp(load pair, store pair)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// push `x0` and `x1` onto the stack. after this operation the stack is:
//
//   |------| <x (original SP)
//   |  x1  |
//   |------|
//   |  x0  |
//   |------| <- SP
//
stp x0, x1, [SP, #-16]!

// pop `x0` and `x1` from the stack. after this operation, the stack is:
//
//   |------| <- SP
//   |  x1  |
//   |------|
//   |  x0  |
//   |------| <x (original SP)
//
ldp x0, x1, [SP], #16

// these four operations perform the same thing as the previous two
sub SP, SP, #16
stp x0, x1, [SP]
ldp x0, x1, [SP]
add SP, SP, #16

// same as before, but we are saving and restoring all of x0, x1, x2, and x3.
sub SP, SP, #32
stp x0, x1, [SP]
stp x2, x3, [SP, #16]

ldp x0, x1, [SP]
ldp x2, x3, [SP, #16]
add SP, SP, #32

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
2
3
4
5
6
7
mov   x0, #0xABCD, LSL #32  // x0 = 0xABCD00000000
mov   x0, #0x1234, LSL #16  // x0 = 0x12340000

mov   x1, #0xBEEF           // x1 = 0xBEEF
movk  x1, #0xDEAD, LSL #16  // x1 = 0xDEADBEEF
movk  x1, #0xF00D, LSL #32  // x1 = 0xF00DDEADBEEF
movk  x1, #0xFEED, LSL #48  // x1 = 0xFEEDF00DDEADBEEF

立即数需要带#前缀,LSL表示逻辑左移。

仅16位立即数带可选的移位能够被装载进寄存器。编译器一般能够识别右移的值,例如能够自动将mov x12, #(1 << 21)自动转换为mov x12, 0x20, LSL #16

2.3. 从符号装载地址

1
2
3
add_30:
    add x1, x1, #10
    add x1, x1, #20

为了装载符号后第一条指令的地址,可以使用adrldr指令

1
2
adr x0, add_30    // x0 = address of first instruction of add_30
ldr x0, =add_30   // x0 = address of first instruction of add_30

若符号和指令不在同一个linker节则必须使用ldr。如果在同一个节,可以使用adr

2.4. 寄存器间移动

  • mov
  • msrmrs

2.5. 算术

  • add <dest> <a> <b> // dest = a + b
  • sub <dest> <a> <b> // dest = a - b

参数<b>也能为一个立即数

1
2
3
sub sp, sp, #120 // sp -= 120
add x3, x1, #120 // x3 = x1 + 120
add x3, x3, #88  // x3 += 88

2.6. 逻辑指令

  • and,orr
1
2
3
4
5
6
7
8
9
10
mov x1, 0b11001
mov x2, 0b10101

and x3, x1, x2  // x3 = x1 & x2 = 0b10001
orr x3, x1, x2  // x3 = x1 | x2 = 0b11101
orr x1, x1, x2  // x1 |= x2
and x2, x2, x1  // x2 &= x1

and x1, x1, #0b110  // x1 &= 0b110
orr x1, x1, #0b101  // x1 |= 0b101

2.7. 分支

无条件跳转

  • b label
1
2
3
4
5
6
7
8
my_function:
    add x0, x0, x1
    ret

mov  x0, #4
mov  x1, #30
bl   my_function  // lr = address of `mov x3, x0`
mov  x3, x0       // x3 = x0 = 4 + 30 = 34
  • br
  • blr
1
2
3
ldr  x0, =label
blr  x0          // identical to bl label
br   x0          // identical to b  label

2.7.1. 条件分支

cmp指令比较两个寄存器/一个寄存器一个立即数的值,并为之后的条件分支指令例如bnebeqblt设置flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// add 1 to x0 until it equals x1, then call `function_when_eq`, then exit
not_equal:
    add  x0, x0, #1
    cmp  x0, x1
    bne  not_equal
    bl   function_when_eq
    cmp  x0, #0
    beq  x0_is_eq_to_zero

exit:
    ...

// called when x0 == x1
function_when_eq:
    ret
  • bne
  • neq
  • blt: less than
  • ble: less than or equal
  • bgt: greater than
  • bge: greater than or equal
  • cbz <ra> <label>: compare, branch on zero
  • cbnz <ra> <label>: compare, branch if not zero

2.8. 小结

2.8.1. 实现memcpy

源地址为x0,目标地址为x1,拷贝的字节数为x2,保证非0,且为8的整数倍。

1
2
3
4
5
6
memcpy:
cbz x2, _memcpy_end
mov x3, [x0], #8 // x3<-*x0, x0+=8
str x3, [x1], #8 // *x1<-x3, x1+=8
sub x2, x2, 1 // x2-=1
_memcpy_end: ret

2.8.2. 将0xABCED写入ELR_EL1

1
2
3
mov x1, 0xD // x1 = 0xD
movk x1, 0xABCE, LSL #4 // x1 = 0xABCED
msr ELR_EL1, x1 

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个参数通过r0r7从左至右传递。
  • 如何从一个函数返回值
    • 在AArch64中,前八个返回值通过r0r7传递。
  • 函数必须保存哪些状态(registers,stack,etc)
    • calleed-saved寄存器:r19r29SP。其余通用意图寄存器都是caller-saved,包括lr(x30)。SIMD/FP较为复杂。。。
  • 如何返回调用者
    • 在AArch64中,ret通过lr(link address)返回

注意:严格遵守调用惯例会阻止所有函数调用和函数体内的优化。因此,Rust的函数默认情况下并不保证遵守惯例。为了强迫Rust根据目标平台惯例编译,需要使用extern关键字

4. 汇编宏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.macro HANDLER source, kind
    .align 7
    // 保存lr,xzr: 调用者保存
    stp     lr, xzr, [SP, #-16]!
    // 保存x28,x29:callee(使用的话)保存
    stp     x28, x29, [SP, #-16]!
    
    // x29 <- [source:kind]
    mov     x29, \source
    movk    x29, \kind, LSL #16
    // jump to context_save
    bl      context_save
    
    // restore regs
    ldp     x28, x29, [SP], #16
    ldp     lr, xzr, [SP], #16
    eret
.end

5. 一个实例:bare metal启动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
// #define EL0 0b00
// #define EL1 0b01
// #define EL2 0b10
// #define EL3 0b11

.section .text.init

.global _start
_start:
    // read cpu affinity, start core 0, halt rest
    mrs     x1, MPIDR_EL1
    and     x1, x1, #3
    cbz     x1, setup

halt:
    // core affinity != 0, halt it
    wfe
    b       halt

setup:
    // store the desired EL1 stack pointer in x1
    adr     x1, _start

    // read the current exception level into x0 (ref: C5.2.1)
    mrs     x0, CurrentEL
    and     x0, x0, #0b1100 // 获取当前异常级别
    lsr     x0, x0, #2 // 逻辑右移

switch_to_el2:
    // switch to EL2 if we're in EL3. otherwise switch to EL1
    cmp     x0, 0b11            // EL3
    bne     switch_to_el1

    // 当前为EL3,写入0101_1011_0001
    // 自左向右:
    // 1. 下一级为AArch64
    // 2. HVC指令为EL1即以上级别使能
    // 3. SMC指令在EL1即以上未定义
    // 4. [5:4]保留
    // 5. [2]: 物理FIQ路由,低于EL3的FIQ不会被带入EL3
    // 5. [1]: 物理IRQ路由:中断不会被带入EL3
    // 6. [0]:non-secure bit:EL0和EL1处于Non-secure状态
    // set-up SCR_EL3 (bits 0, 4, 5, 7, 8, 10) (A53: 4.3.42)
    mov     x2, #0x5b1
    msr     SCR_EL3, x2

    // [9:6]: 禁中断
    // 3: 异常级别为2(0b10)
    // 0: 选择SP0
    // set-up SPSR and PL switch! (bits 0, 3, 6, 7, 8, 9) (ref: C5.2.20)
    mov     x2, #0x3c9
    msr     SPSR_EL3, x2
    adr     x2, switch_to_el1
    msr     ELR_EL3, x2
    eret

switch_to_el1:
    // switch to EL1 if we're not already in EL1. otherwise continue with start
    cmp     x0, 0b01    // EL1
    beq     set_stack

    // set the stack-pointer for EL1
    msr     SP_EL1, x1

    // [0]: 使能EL0和EL1对EL2物理计数器寄存器的访问
    // [1]: 使能EL0和EL1对EL2物理定时器寄存器的访问
    // enable CNTP for EL1/EL0 (ref: D7.5.2, D7.5.13)
    // NOTE: This doesn't actually enable the counter stream.
    mrs     x0, CNTHCTL_EL2
    orr     x0, x0, #0b11
    msr     CNTHCTL_EL2, x0
    msr     CNTVOFF_EL2, xzr

    // HCR[31]位默认0表示EL1及以下均为AArch32,这里置1
    // HCR[1] Set/Way Invalidation Override,禁止cache?
    // enable AArch64 in EL1 (A53: 4.3.36)
    mov     x0, #(1 << 31)      // Enable AArch64 for EL1
    orr     x0, x0, #(1 << 1)   // RES1 on A-53
    msr     HCR_EL2, x0
    mrs     x0, HCR_EL2

    // enable floating point and SVE (SIMD) (A53: 4.3.38, 4.3.34)
    msr     CPTR_EL2, xzr     // don't trap accessing SVE registers
    mrs     x0, CPACR_EL1
    orr     x0, x0, #(0b11 << 20)
    msr     CPACR_EL1, x0

    // System control register: top level control of the system
    // 禁用指令cache
    // 小端法
    // EL0禁止访问DC,IC相关指令
    // Set SCTLR to known state (RES1: 11, 20, 22, 23, 28, 29) (A53: 4.3.30)
    mov     x2, #0x0800
    movk    x2, #0x30d0, lsl #16
    msr     SCTLR_EL1, x2

    // set up exception handlers
    // FIXME: load `_vectors` addr into appropriate register (guide: 10.4)

    // 同禁中断FIDA
    // M[3:0] 设置为EL1h,即0b0101
    // top两位表示异常级别为1,低两位表示使用使用target EL的SP
    // change execution level to EL1 (ref: C5.2.19)
    mov     x2, #0x3c5
    msr     SPSR_EL2, x2
    
    // 这里没有eret,因而其实没有转到EL1,仍处于EL2
    // FIXME: Return to EL1 at `set_stack`.

set_stack:
    // set the current stack pointer
    mov     sp, x1

go_kmain:
    // jump to kmain, which shouldn't return. halt if it does
    bl      kinit
    b       halt

context_save:
    // FIXME: Save the remaining context to the stack.

.global context_restore
context_restore:
    // FIXME: Restore the context from the stack.

    ret

.align 11
_vectors:
    // FIXME: Setup the 16 exception vectors.

总的来说,首先除了核0外的其它核都低功耗循环,核0从EL3到EL2到EL1,然后执行kinit。中间过程中没有虚拟内存、数据/指令cache。但使用AArch64,小端法。

6. 参考