AArch64 指令集体系结构

本文参考:Learn the architecture: AArch64 Instruction Set Architecture (arm.com)

指令集体系结构(Instruction Set Architecture ,ISA)是对计算机的抽象模型描述。

本文基于 64-bit Armv8-A 架构,即 AArch64.

Arm 架构的指令集

Armv8-A 支持三种指令集:

  • A32、T32:在 AArch32 执行状态使用
  • A64:在 AArch64 执行状态使用。指令长固定是 32 位(而非 64 位)。

SSE 模型

Arm 架构通过简单序列执行(SSE)模型来描述指令,即取指,译码,执行。这是为了方便理解而采用的功能描述。而在实际处理器的设计中,使用的是流水线技术,并不是有序的。

权限模型(异常级别)

  • EL0 - 用于用户级应用程序。

  • EL1 - 用于运行像 Linux 这样的操作系统内核。

  • EL2 - 用于运行管理程序。

  • EL3 - 用于控制(和保护)对信任区的访问。

信任区

可信执行环境 (Trusted Execution Environment,TEE) 是主处理器内的一个安全区域,与操作系统并行运行于隔离的环境中,确保加载到 TEE 中的代码和数据受到保护。

寄存器

通用寄存器

通用寄存器有 31 个,分别是 X0~X31,每个都能存储 64 位的数据。

W0~W31 表示这些寄存器的低 32 位。

当写入 32 位寄存器的时候,该寄存器的高 32 位会清零。

还有 32 个单独的 128 位寄存器,用于浮点和向量运算。

  • 通过 Bx 可以访问低 8 位
  • 通过 Hx 可以访问低 16 位
  • 通过 Sx 可以访问低 32 位
  • 通过 Dx 可以访问低 64 位
  • 通过 Qx 可以访问低 128 位

其它寄存器

零寄存器

XZR 和 WZR 是零寄存器,内容恒为 0,不可写入。

栈指针寄存器:每个异常级别各有一个栈指针(Stack pointer)寄存器 SP_ELx

X30 用作链接寄存器(LR)

异常寄存器ELR_ELx 用于接受异常的返回

程序计数器(PC) 可以通过如下格式访问:

ADR Xd, .

. 表示当前地址,ADR 指令用于返回一个标签相对于 . 的地址。

在 A32/T32 中,PC 和 SP 归属通用寄存器

但是在 A64 中,不属于通用寄存器

系统寄存器

系统寄存器用于配置处理器、系统控制(如 MMU)以及异常处理,不允许直接访问或者通过 读 / 存 指令访问。但是可以将其读入一个 X 寄存器,或者从 X 寄存器写回。

读取系统寄存器:

MRS        Xd, <system register>

写回系统寄存器:

MSR        <system register>, Xn

系统寄存器的命名为 SCTLR_ELx

MRS        X0, SCTLR_EL1

_ELx 表示访问一个寄存器的最低权限级别。例如

SCTLR_EL1

表示需要 EL1 或更高权限。

指令

算术逻辑运算

格式如下:

ARM275 Branding of Enabling Content Graphics ST1 V4

指令格式:

  • 运算类型:如 ADD
  • 目标:算完放哪儿
  • 操作数 1:必须是寄存器
  • 操作数 2:可以是寄存器,可以是常量(立即数)

例子:

; 令 x0 = 1
mov		x0, #1

; 令 w0 = ~w1
mvn		w0, w1

浮点运算

浮点运算以 F 开头。

; h0 = h1 / h2
FDIV       H0, H1, H2
; s0 = s1 + s2
FADD       S0, S1, S2
; d0 = d1 - d2
FSUB       D0, D1, D2

位操作

image-20210713230114221

; 位段插入,将 W0 的低 6 位放到 W0 的第 9~9+6 位
BFI		W0, W0, #9, #6
; 位段取出,将 W0 的 18~18+7 位取出,放入 W1
UBFX	W1, W0, #18, #7

image-20210714092452563

REV16:每 16 字节对换

RBIT:整体镜像翻转

扩展指令和饱和度

扩展指令格式为:[u/s][格式 1] t[格式 2]

image-20210714093133894

; 有符号扩展
sxtb		w1,	w0
; 无符号扩展
uxth		w2,	w1

子寄存器大小的整数数据处理

有的指令执行 饱和运算,也即如果结果超出了目标寄存器的表示范围(太大或者太小),那么运算结果会设置为最大或最小值。

C Generated assembler
uint32_t add32(uint32_t a, uint32_t b)
{
return a+b;
}
add32:
ADD W0,W1,W0
RET
uint16_t uadd16(uint16_t a, uint16_t b)
{
return a+b;
}
uadd16:
AND W8,W1,#0xffff
ADD W0,W8,W0,UXTH
RET
int16_t sadd16(int16_t a, int16_t b)
{
return a+b;
}
sadd16:
SXTH W8,W1
ADD W0,W8,W0,SXTH
RET

第一个例子中,不需要额外处理。

后两个例子中,需要额外的指令。例如第三个例子,将 W1 的 16 位扩展到 32 位存放在 W8,然后执行运算,将结果饱和存放到 16 位寄存器。

格式转换

MOVMVN 可以在寄存器之间传递值,同样的 FMOV 用于浮点寄存器和通用寄存器传值。

; 将 X0 转为浮点放入 D0
scvtf		d0,	x0
; 将 D0 转为整数放入 X0(就近舍入)
fcvtns		x0, d0

FMOV 和 SCVTF 是不同的,区别在于前者只是机械地拷贝二进制,后者是拷贝和转换数值。

; X0 = 0x0000_0000_0000_0002
FMOV D0, X0
SCVTF D1, X0
; 结果:
; D0 = 0x0000_0000_0000_0002 = 9.88131e-324
; D1 = 0x4000_0000_0000_0002 = 2.0

向量数据

A64 提供两种向量处理:

  • 高级 SIMD,即 NEON
  • 可缩放向量扩展(Scalable Vector Extension, SVE)

SIMD 在 Armv6 的 32 位通用寄存器使用

NEON 在 Armv7 的 128 位向量寄存器使用

读 / 存指令

Arm 采用的是 Load/Store 模式

LDR<Sign><Size>        <Destination>, [<address>]

STR<Size>         <Destination>, [<address>]

大小

寄存器:

符号 位数
X 64
W 32

指令:

符号 字节数
B 1
H 2
W 4

零扩展和符号扩展

默认情况是零扩展:

image-20210714100000293

当指令中指明是符号扩展时:

image-20210714100018547

寻址

寄存器寻址
; 将 X1 的值作为地址,访存并读出到 W0 中
LDR  W0,  [X1]

X1 中存放的是绝对地址或虚拟地址。

偏移量寻址
; 最终地址是 X1 + 12
LDR, W0, [X1, #12]
预索引寻址模式
; 最终地址是 X1 + 12
; 并且指令执行后,X1 <- X1 + 12
LDR, W0, [X1, #12]!
索引后寻址

ARM275 Branding of Enabling Content Graphics ST1 V14

; 指令执行前,X1 <- X1 + 12
; 最终以 X1 寻址
LDR, W0, [X1], #12

加载对和存储对

可以一次性读取一对数据

; W3 <= [X0], W7 <= [X0 + 4]
LDP        W3, W7, [X0]
; D0 => [X4], D1 => [X4 + 8]
STP        D0, D1, [X4]
; 将 X0, X1 推入栈
STP        X0, X1, [SP, #-16]!
; 将 X0, X1 弹出栈
LDP        X0, X1, [SP], #16

栈指针必须 128 位对齐

使用浮点寄存器

; D1 <= [X0]
LDR        D1, [X0]
; Q0 => [X0 + X1]
STR        Q0, [X0, X1]
; Q1 <= [X5], Q3 <= [X5 + 16], X5 <= X5 + 256
LDP        Q1, Q3, [X5], #256

程序流控制

无条件分支

; 无条件跳转到 Xn 给定的地址
BR <Xn>

条件分支

格式:

B.<cond> <label>

例子:

; 如果 Xn 为 0 (CBZ),则该指令分支到 <label>,如果 Xn 不包含 0 (CBNZ),则分支到 label。
CBZ <Xn> <label> and CBNZ <Xn> <label>
TBZ <Xn>, #<imm>, <label> and TBNZ <Xn>, #<imm>, <label>

TBX 的工作方式与 CBZ/CBNZ 类似,但测试 <imm> 指定的位。

if (a == 5)
b = 5;

相当于:

CMP W0, #5
B.NE skip
MOV W8, #5
skip:
while (a != 0)
{
b = b + c;
a = a - 1;
}

相当于

loop:
CBZ W8, skip
ADD W9, W9, W10
SUB W8, W8, #1
B loop
skip:

条件码的产生

主要的标志位:

  • N - Negative 负数
  • C - Carry 进位
  • V - Overflow 溢出
  • Z - Zero 零

根据 SUB ADD 等指令的运算结果,将其设置为 0/1.

EQ 和 NE 指令
; 在 Z==1 时跳转到 label
B.EQ    label
CMP 和 TST 指令别名
CMP X0, X7 ; 相当于 SUBS XZR, X0, X7
TST W5, #1 ; 相当于 ANDS WZR, W5, #1

ZR:零寄存器

条件选择指令

格式:

; Xd = cond ? Xn : Xm
CSEL       Xd, Xn, Xm, cond

例子:

CMP        W1, #0

CSEL    W5, W6, W7, EQ
; Xd = cond ? Xn : Xm + 1
CSINC      Xd, Xn, Xm, cond

函数调用

image-20210714104227950

当调用函数的时候,我们需要把返回地址写入 LR(链路寄存器),这样当函数执行完之后,就可以返回到原来的地方。函数调用使用分支语句(BL)实现。

过程调用标准(PCS)

Arm 对通用寄存器的使用没有限制,但是为了方便还是约定了一个标准(就像 x86 汇编用 %eax 做返回值等,可看 此文)。规定如下:

寄存器 用途 谁来保存
X0-X7 参数和结果寄存器。X0 放返回值。 调用者
X8 XR,指向调用者为返回结构而分配的内存的指针。 调用者
X9-X15 Corruptible Registers(临时寄存器) 调用者
X16 IP0 子程序内部调用寄存器
X17 IP1 子程序内部调用寄存器
X18 PR 平台寄存器,它的使用与平台相关 调用者
X19-X28 被调用者保存的临时寄存器 被调用者
X29 FP 帧指针寄存器,用于连接栈帧
X30 LR

浮点寄存器:

寄存器 用途
D0-D7 参数和结果寄存器
D8-D23 被调用者保存寄存器
D24-D31 /

系统调用

有时候,处于 EL0 的用户程序需要调用 EL1 的函数(例如使用 OS 提供的函数读写文件),此时需要执行系统调用。类似的,其它 EL 也有系统调用。

  • SVC: Supervisor call,用于 EL0 访问 EL1
  • HVC:Hypervisor call,用于 EL1 访问 EL2
  • SMC:Secure monitor call,用于 EL1/2 访问 EL3