A64指令集架构指南

116 阅读29分钟

1. 概述

指令集架构 (ISA) 是计算机抽象模型的一部分。 它定义了软件如何控制处理器。

Arm ISA 允许您编写符合 Arm 规范的软件和固件。如果您的软件或固件符合规范,任何基于 Arm 的处理器都会以相同的方式执行它

本指南介绍了64位 Armv8-A 架构中使用的 A64 指令集,也称为 AArch64

本指南不会详细介绍每一条指令,指令的详细说明见 Arm Architecture Reference Manual

我们将介绍指令的格式、指令的不同类型以及用汇编程序编写的代码如何与编译器生成的代码交互。

2. 为什么应该关心 ISA?

作为开发者,可能不需要在日常工作中直接编写汇编程序。然而汇编程序在某些领域仍然很重要,例如 “第一阶段引导软件” 或一些底层内核活动

虽然不直接写汇编,但是了解指令集可以做什么以及编译器如何使用这些指令可以帮助您编写更高效的代码。它还可以帮助您了解编译器的输出。这在调试时很有用

3. Arm 架构中的指令集

Armv8-A 支持三种指令集:A32T32A64

在 AArch64 执行状态下执行时使用 A64 指令集。 它是一个固定长度的 32 位指令集。名称中的 64 指的是 AArch64 执行状态对该指令的使用。不是指内存中指令的大小。

A32 和 T32 指令集也分别称为 Arm 和 Thumb。 这些指令集在 AArch32 执行状态下执行时使用。 在本指南中,不介绍 A32 和 T32 指令集

4. 关于指令集的相关资源

每个版本的 Arm 架构都有自己的 Arm 架构参考手册,可以在 Arm 开发者网站上找到。 每个Arm ARM( Arm Architecture Reference Manual)都提供了每条指令的详细说明,包括:

  • 编码 - 指令在内存中如何展示
  • 参数 - 指令的输入
  • 伪代码 - 指令的作用,以 Arm 伪代码语言表示
  • 限制 - 指令什么时候不能被使用,或者它可以触发的异常

A64 的指令描述也以 XML 和 HTML 形式提供。 如果您需要经常参考说明,则 XML 和 HTML 格式非常有用。 XML 和 HTML 格式可以在 Arm 开发者网站上找到。 您可以在相关信息(最后一小节)中找到链接。XML 可以作为压缩档案下载,HTML 可以使用 Web 浏览器查看和搜索

5. Simple sequential execution(简单顺序执行)

Arm 架构描述了指令要遵循简单顺序执行 (SSE) 模型。CPU 的行为是 取值-解码-执行,并且按照指令在内存中出现的顺序进行操作

实际上,现代处理器具有可以同时执行多个指令的管道,并且可能会乱序执行。下图显示了 Arm Cortex 处理器的示例管道:

要记住架构只是功能描述,并没有指定单个处理器的工作方式。每个处理器的行为必须与简单的顺序执行模型一致,即使它在内部重新排序指令

6. AArch64 中的寄存器 - 通用寄存器

大多数 A64 指令都在寄存器上运行。该架构提供 31 个通用寄存器。每个寄存器都可以用作 64 位 X 寄存器 (X0..X30),或用作 32 位 W 寄存器 (W0..W30)。

下图显示 W0 是 X0 的低 32 位,W1 是 X1 的低 32 位:

对于数据处理指令,选择使用 X 或 W 决定了操作的大小。使用 X 寄存器将导致 64 位计算,使用 W 寄存器将导致 32 位计算

ADD W0, W1, W2	; 执行32位整数加法
ADD X0, X1, X2	; 执行64位整数加法

当写入 W 寄存器时,64 位寄存器的高 32 位将被清零

有一组单独的 32 个寄存器用于浮点和向量运算。 这些寄存器是 128 位,但与通用寄存器一样,可以通过多种方式访问。 Bx 是 8 位,Hx 是 16 位,依此类推,Qx 是 128 位。

您使用的寄存器名称决定了计算的大小

FADD S0, S1, S2
FADD D0, D1, D2

这些寄存器也可以称为 V 寄存器。 当使用 V 形式时,寄存器被视为向量。 这意味着它被视为包含多个独立值,而不是单个值。此示例执行向量浮点加法:

FADD V0.2D, V1.2D, V2.2D

此示例执行向量整数加法:

ADD  V0.2D, V1.2D, V2.2D

7. AArch64 中的寄存器 - 其他寄存器

以下是一些应该了解的其他寄存器:

  • 零寄存器,XZR 和 WZR,始终读取为 0 并忽略写入
  • 您可以使用堆栈指针 (SP, stack pointer) 作为 loads 和 stores 的基地址。 您还可以将堆栈指针与一组有限的数据处理指令一起使用,但它不是常规的通用寄存器。Armv8-A 有多个堆栈指针,每个堆栈指针都与特定的异常级别相关联。当指令中使用 SP 时,表示当前堆栈指针。异常模型指南解释了如何选择堆栈指针
  • X30 用作链接寄存器,可以简称为 LR。 单独的寄存器 ELR_ELx 用于从异常中返回。异常模型指南对此进行了更详细的讨论
  • 程序计数器(PC) 不是 A64 中的通用寄存器,它不能与数据处理指令一起使用。可以使用以下方式读取 PC:
ADR Xd, .

ADR 指令返回根据当前位置计算的标签地址。 点(.)表示“这里”,因此显示的指令返回其自身的地址。这相当于读PC。某些分支指令和某些加载/存储操作隐式使用 PC 的值

在A32和T32指令集中,PC 和 SP 是通用寄存器。 A64 指令集并非如此。

8. AArch64 中的寄存器 - 系统寄存器

除了通用寄存器之外,该架构还定义了系统寄存器。系统寄存器用于配置处理器和控制系统,例如 MMU 和异常处理

系统寄存器不能直接被数据处理或加载/存储指令使用。 相反,系统寄存器的内容需要读入 X 寄存器,进行操作,然后写回系统寄存器

有两个专门的指令用于访问系统寄存器:

  1. 将系统寄存器读入 Xd
MRS        Xd, <system register>
  1. 将 Xn 写入系统寄存器
MSR        <system register>, Xn

系统寄存器按名称指定,例如 SCTLR_EL1:

MRS        X0, SCTLR_EL1  	; 读SCTLR_EL1到X0寄存器

系统寄存器名称以 _ELx 结尾。 _ELx 指定访问寄存器所需的最小权限。 例如:

SCTLR_EL1   ; 需要 EL1 或更高权限
SCTLR_EL2   ; 需要 EL2 或更高权限
SCTLR_EL3   ; 需要 EL3 权限

9. 数据处理 - 算术和逻辑运算

逻辑和整数算术指令的基本格式为:

  • Operation(操作符) 定义了指令的作用。 例如,ADD 执行加法,AND 执行逻辑与。可以将 S 添加到 Operation 中以设置标志位。 例如,ADD 变为 ADDS。该命令告诉处理器根据指令结果更新 ALU 标志。 我们在生成条件代码部分讨论 ALU 标志。
  • Destination 是指令的目的地,它始终是寄存器,并指定运算结果放置的位置。大多数指令都只有一个目标寄存器。一些指令有两个目标寄存器。 当目标是 W 寄存器时,相应 X 寄存器的高 32 位会被设置为 0
  • Operand(操作数)1 始终是寄存器。这是指令的第一个输入
  • Operand(操作数)2 可以是寄存器,也可以是常量,作为指令的第二个输入。当操作数 2 是寄存器时,它可能包括可选的移位。 当操作数 2 是常量时,它在指令本身内进行编码。 这意味着可用常数的范围是有限的

10. 数据处理 - 浮点

浮点运算遵循与整数数据处理指令相同的格式并使用浮点寄存器。与整数数据处理指令一样,操作的大小决定了所使用的寄存器的大小。浮点指令的运算部分始终以 F 开头。

例如,该指令以半精度设置 H0 = H1 / H2:

FDIV       H0, H1, H2

该指令以单精度设置 S0 = S1 + S2:

FADD       S0, S1, S2

该指令以双精度设置 D0 = D1 - D2:

FSUB       D0, D1, D2

10.1. 支持 8 位和 16 位浮点

Arm 架构支持 16 位和 8 位浮点格式

Armv8.2-A 添加了对 16 位浮点 FP16 的支持。 从 Armv8.2-A 开始,对 FP16 的支持是可选的,如果实现了 SVE 或 SVE2,那么对 FP16 的支持将成为强制性的,这意味着它在 Armv9-A 中实际上是强制性的。

BFloat16,通常缩写为 BF16,是另一种 16 位浮点存储格式。 BF16 从 Armv8.2-A 开始可选支持,但在 Arm8.6-A 和 Armv9.1-A 中成为强制支持。 BF16 最近成为一种专门针对神经网络 (NN) 高性能处理而定制的格式

FP16 和 BF16 格式之间的区别在于指数和分数之间的位划分方式,如上图所示。 BF16 具有与 FP32 相同的指数范围,但分数位数较少。 这使得 BF16 和 FP32 之间的转换更加简单,因为 BF16 实际上是 FP32 的较低精度版本。

2022 年,Arm、英特尔和 Nvidia 宣布合作开发 FP8,这是一种交换格式,允许软件生态系统轻松共享 NN 模型并支持 AI 计算能力的不断进步。 FP8 支持作为 Armv9.2-A 中的可选功能引入。 FP8 引入了一对格式:E5M2 和 E4M3。 这两种格式在精度和范围之间提供了不同的权衡。

使用 FP8 时,使用哪种格式(E5M2 和 E4M3)由 FPMR 寄存器中的字段选择。 可以为指令的不同输入选择不同的格式,从而可以有效地处理不同格式的数据集。

10.2. 浮点支持是可选的吗?

不可以。Armv8-A 中必须支持浮点。 该体系结构指定每当使用丰富的操作系统(例如 Linux)时都需要它。

如果您运行的是完全专有的软件堆栈,那么从技术上讲,您可以省略浮点支持。 大多数工具链,包括 GCC 和 Arm Compiler 6,都将支持浮点。

11. 数据处理 - 位操作

有一组用于操作寄存器内位的指令。 该图显示了一些示例:

BFI 指令将一个位字段插入到寄存器中。 在上图中,BFI 从源寄存器 (W0) 中取出一个 6 位字段,并将其插入到目标寄存器的第 9 位位置。

UBFX 提取一个位字段。 在上图中,UBFX 从源寄存器中的位位置 18 获取 7 位字段,并将其放入目标寄存器中。

其他指令可以反转字节或位顺序,如下图所示:

当您处理不同字节顺序的数据时,REV16 和 RBIT 特别有用。

12. 数据处理 - 扩展和饱和(extension and saturation)

有时需要将数据从一种尺寸转换为另一种尺寸。 SXTx(符号扩展)和 UXTx(无符号扩展)指令可用于此转换。 在这个转换中,x决定了被扩展的数据的大小,如下图所示:

在第一条指令 SXTB 中,B表示字节。 它采用 W0 的底部字节并将其符号扩展为 32 位。

UXTH 是半字 (H) 的无符号扩展。 它采用 W1 的底部 16 位,并将其零扩展为 32 位。

前两个示例将 W 寄存器作为目标,这意味着扩展至 32 位。 第三个示例有一个 X 寄存器,这意味着符号扩展为 64 位

12.1. Sub-register-sized integer data processing (子寄存器大小的整数数据处理)

一些指令执行饱和算术运算。这意味着如果结果大于或小于目标所能容纳的值,那么结果将被设置为目标整型范围内的最大值或最小值。

数据处理指令能够有效的处理32位和64位数据。实际上,在处理子寄存器计算时,经常会看到饱和指令。子寄存器计算是16位或8位的计算。如下表格展示了C语言中子寄存器计算的例子和生成的汇编代码:

表中第一个例子,32位加法映射到W寄存器,因此可以很容易的处理。对于表中的16位示例,需要额外的指令。表中第三个例子使用16位作为输入,将其扩展到32位,然后执行加法。该序列使用以下命令将 16 位输入转换为 32 位:

SXTH  W8,W1

然后,这条指令执行加法运算,并将结果饱和为有符号的16位:

ADD   W0,W8,W0,SXTH

添加, SXTH到ADD操作的操作数列表的末尾会导致结果使用饱和算术运算。由于目标是W寄存器,因此ADD将饱和到 16 位整数范围。

13. 数据处理 - 格式转换

我们已经看到 MOV 和 MVN 指令将值从一个寄存器复制到另一个寄存器。 同样,FMOV 可用于在浮点寄存器和通用寄存器之间进行复制。

但是,使用 FMOV 会复制寄存器之间的文字位模式。 还有一些指令可以转换为最接近的表示形式,如下图所示:

在此示例中,假设 X0 包含值 2(正整数 2):

X0 = 0x0000_0000_0000_0002

然后,执行以下序列:

FMOV D0, X0


SCVTF D1, X0

两条指令都将 X0“复制”到 D 寄存器中。 然而,结果却截然不同:

D0 = 0x0000_0000_0000_0002 = 9.88131e-324

D1 = 0x4000_0000_0000_0002 = 2.0

FMOV 复制了文字位模式,当解释为浮点值时,这是一个非常不同的值。 SCVTF 将 X0 中的值转换为最接近的浮点值。

类似地,FCVTxx 可用于将浮点值转换为其最接近的整数表示形式。 在本例中,xx 的不同值控制所使用的舍入模式。

14. 数据处理 - 向量和矩阵数据

A64架构还提供对矢量数据处理的支持。 可用的两种类型的矢量处理是:

  • 高级 SIMD,也称为 NEON。
  • 可扩展矢量扩展(SVE 和 SVE2)。 SVE 在 Armv8-A 中引入,并针对 HPC 工作负载进行了优化。 Armv9-A 引入了 SVE2,它扩展了基本 SVE 以支持更多用例。

Armv9-A 还引入了可选的可扩展矩阵扩展(SME 和 SME2)。 SME 基于 SVE2 构建,添加了新功能以高效处理矩阵。 主要特点包括:

  • Matrix tile storage
  • Load, store, insert, and extract tile vectors, including on-the-fly transposition
  • Outer product of SVE vectors
  • Streaming SVE mode

SME provides outer-product instructions to accelerate matrix operations. SME2 significantly extends the capabilities with instructions for multi-vector operations, multi-vector predicates, range prefetches and 2b/4b weight compression.

The new instructions enable SME2 to accelerate more workloads than the original SME. Including GEMV, Non-Linear Solvers, Small and Sparse Matrices, and Feature Extraction or tracking.

15. 加载和存储

基本的加载和存储操作是:LDR(加载)STR(存储) 。这些操作在内存和通用寄存器之间传输单个值。这些指令的语法是:

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

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

16. 加载和存储 - size

加载或存储的大小由寄存器类型 X 或 W 以及 Size 字段确定。 X 用于 64 位,W 用于 32 位。 例如,该指令将地址中的 32 位加载到 W0 中:

LDR        W0, [<address>]

该指令将 64 位从地址加载到 X0 中:

LDR        X0, [<address>]

size 字段允许您加载子寄存器大小的数据量。例如,该指令将 W0 的底部一个字节 (B) 存储到地址:

STRB       W0, [<address>]

该指令将 W0 的底部 halfword (H) 存储到地址:

STRH       W0, [<address>]

最后,该指令将 X0 的底部 word (W) 存储到地址:

STRW       X0, [<address>]

17. 加载和存储 - zero and sign extension

默认情况下,当加载子寄存器大小的数据量时,寄存器的其余部分被清零,如下图所示:

Note

请记住,每当写入 W 寄存器时,X 寄存器的上半部分就会被清零。

在运算中添加 S 会导致该值被符号扩展。大小扩展的程度取决于目标是 W 还是 X 寄存器,如下图所示:

18. 加载和存储 - 寻址

加载和存储指令的地址出现在方括号内,如下例所示:

LDR  W0,  [X1]

有几种寻址模式定义了地址的形成方式

  • 基址寄存器 - 最简单的寻址形式是单个寄存器。 基址寄存器是一个 X 寄存器,包含正在访问的数据的绝对或相对虚拟地址,如下图所示:

  • 偏移寻址模式 - 可以选择将偏移应用于基地址,如下图所示:

在上图中,X1 包含基地址,#12 是该地址的字节偏移量。这意味着访问的地址是 X1 + 12。偏移量可以是常量或另一个寄存器

这种寻址方式可能用于结构体 struct。编译器维护了一个指向 struct 基地址的指针,使用偏移量来选择不同的成员

  • 预索引寻址模式( Pre-index addressing modes - 在指令语法中,预索引通过在方括号后添加感叹号!来表示,如图所示:

预索引寻址类似于偏移寻址,不同之处在于基指针(sp)会随着指令的执行而更新。在上图中,指令完成后X1 的值为 X1 + 12

  • 后索引寻址模式( Post-index addressing modes - 使用后索引寻址时,从基指针中的地址加载值,然后更新指针,如下图所示:

后索引寻址对于弹出堆栈很有用。 该指令从堆栈指针指向的位置加载值,然后将堆栈指针移动到堆栈中的下一个完整位置

19. 加载和存储 - load pair and store pair

到目前为止,我们已经讨论了单个寄存器的加载和存储。A64 还具有加载对 (LDP,load pair) 和存储对 (STP,store pair) 指令

这些 LDP 和 STP 对指令将两个寄存器传入和传出存储器。寄存器按操作数顺序从左到右进行处理。 也就是说,首先加载或存储第一个寄存器操作数,然后加载或存储第二个寄存器操作数

考虑以下示例:

第一条指令将 [X0] 加载到 W3,并将 [X0 + 4] 加载到 W7:

LDP        W3, W7, [X0]

第二条指令将 D0 存储到 [X4],并将 D1 存储到 [X4 + 8]:

STP        D0, D1, [X4]

加载对 和 存储对 指令通常用于 push 堆栈和从堆栈中 pop。第一条指令将 X0 和 X1 push到堆栈:

STP        X0, X1, [SP, #-16]!

; 1. 把 X0 push到栈内:sp - 16
; 2. 把 X1 push到栈内:sp - 24

第二条指令从堆栈中 pop 出 X0 和 X1:

LDP        X0, X1, [SP], #16

; 1. 从栈中取出8个字节给x0
; 2. 再取出8个字节给x1
; 3. 移动sp位置:add sp, #16

在 AArch64 中,堆栈指针必须是 128 位(16 字节)对齐的

20. 加载和存储 - 使用浮点寄存器

正如我们将在此处看到的,加载和存储也可以使用浮点寄存器来执行。第一条指令将 64 位从 [X0] 加载到 D1 中:

LDR        D1, [X0]

第二条指令存储从 Q0 到 [X0 + X1] 的 128 位:

STR        Q0, [X0, X1]

最后,该指令从 X5 加载一对 128 位值,然后将 X5 增加 256:

LDP        Q1, Q3, [X5], #256

有以下限制:

  • 大小仅由寄存器类型指定
  • There is no option to sign extend loads.
  • 该地址必须是 X 寄存器

使用浮点寄存器进行加载和存储可能会出现意外情况。 memcpy() 类型例程通常使用它们。 这是因为更宽的寄存器意味着需要更少的迭代。不要仅仅因为您的代码不使用浮点值,就认为您不需要使用浮点寄存器。

21. 加载和存储 - specialist instructions (专业指令)

A64 指令集还包括一些用于更专业用例的加载和存储指令。

22. 程序流程(Program flow)

通常,处理器按程序顺序执行指令。这意味着处理器执行指令的顺序与它们在内存中设置的顺序相同。改变这种顺序的一种方法是使用分支(branch)指令。分支指令改变程序流程,用于循环、decisions(决策)和函数调用

A64指令集也有一些条件分支指令。这些指令根据前一个指令的结果改变它们的执行方式

23. 程序流程 - 循环和决策

在本节中,我们将研究循环和决策如何让您使用分支指令更改程序代码的流程。

分支指令有两种类型:无条件分支指令 和 条件分支指令

无条件分支指令

无条件分支指令 B 对标签执行一个直接的、与pc相关的分支。从当前PC到目标的偏移量被编码在指令中。该范围受限于指令中记录偏移量的可用空间,为+/-128MB。

当您使用 BR 时,BR 执行到 Xn 中指定的地址的间接或绝对分支

条件分支指令

条件分支指令 B. 是 B 指令的条件版本。 仅当条件 为 true 时才会采用分支。范围限制为 +/-1MB。

该条件针对存储在 PSTATE 中的 ALU 标志进行测试,并且需要由先前的指令(例如 CMP )生成。

CBZ <Xn> <label> and CBNZ <Xn> <label>

如果 Xn 包含 0 (CBZ),则该指令分支到 ;如果 Xn 不包含 0 (CBNZ),则分支到 label。

TBZ <Xn>, #<imm>, <label> and TBNZ <Xn>, #<imm>, <label>

TBZ 和 TBNZ 指令的工作方式与 CBZ 和 CBNZ 指令类似,但测试 指定的单个位而不是整个寄存器值。

将它们映射到您可能用 C 编写的内容,以下示例展示了如何将分支用于循环和决策

思考以下 C 代码:

if (a == 5)
  b = 5;

上述 C 代码的编译器的典型输出可能如下:

   CMP W0, #5
   B.NE skip
   MOV W8, #5
skip:

思考以下 C 代码:

while (a != 0)
{
  b = b + c;
  a = a - 1;
}

上述 C 代码的编译器的典型输出可能如下:

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

Note

输出中显示的标签不会由编译器创建。 它们包含在这里是为了提高可读性。

24. 程序流程 - 生成条件代码

在 "程序流程 - 循环和决策" 节中,我们了解到 是根据 PSTATE 中存储的 ALU 标志进行测试的。

数据处理指令可能导致 ALU 标志位被设置。回顾一下,结束操作时的 S 会导致 ALU 标志更新。这是 ALU 标志未更新的指令示例:

ADD        X0, X1, X2

这是使用 S 更新 ALU 标志的指令示例:

ADDS       X0, X1, X2

此方法允许软件控制何时更新或不更新标志。 这些标志可由后续条件指令使用。 我们以下面的代码为例:

SUBS       X0, X5, #1 

AND     X1, X7, X9 

B.EQ    label

SUBS 指令执行减法并更新 ALU 标志。 然后 AND 指令执行与运算,并且不更新 ALU 标志。最后,B.EQ 指令使用作为减法结果设置的标志来执行条件分支

这些标志是:

  • N - Negative
  • C - Carry
  • V - Overflow
  • Z - Zero

我们以 Z 标志为例。如果运算结果为零,则 Z 标志设置为 1。例如,这里如果 X5 为 1,则 Z 标志将被设置,否则将被清除:

SUBS       X0, X5, #1

条件代码映射到这些标志并成对出现。 我们以EQ(等于)和NE(不等于)为例,看看它们如何映射到Z标志:

EQ 代码检查 Z==1。 NE 代码检查 Z==0

以下面的代码为例:

SUBS       W0, W7, W9            // W0 = W7 - W9

B.EQ    label

在第一行中,我们有一个减法运算。因为我们使用了 S 后缀,所以如果结果为零,则该减法运算会设置 Z 标志。在最后一行中,如果 Z==1,则有一个 label 分支

如果 w7==w9,减法结果将为零,并且 Z 标志将被设置。因此,如果 w7 和 w9 相等,则将采用到 label 的分支

除了常规数据处理指令外,还可以使用其他仅更新 ALU 标志的指令:

  • CMP - 比较
  • TST - 测试

这些指令是其他指令的别名。例如:

CMP X0, X7     //an alias of SUBS XZR, X0, X7

TST W5, #1     //an alias of ANDS WZR, W5, #1

通过使用零寄存器作为目标,我们将丢弃操作结果并仅更新 ALU 标志

25. 程序流程 - 条件选择指令

到目前为止,我们已经看到了使用分支来处理决策的示例。 A64指令集还提供条件选择指令。 在许多情况下,这些指令可以用作分支的替代方法。

有很多变体,但基本形式是:

CSEL       Xd, Xn, Xm, cond
if cond then

  Xd = Xn

else

  Xd = Xm

举个例子:

CMP        W1, #0

CSEL    W5, W6, W7, EQ

等同于:

if (W1==0) then

  W5 = W6

else

  W5 = W7

有一些变体将另一个操作与条件选择相结合。 例如,CSINC 执行选择和加法:

CSINC      Xd, Xn, Xm, cond
 if cond then

  Xd = Xn

 else

  Xd = Xm + 1

要有条件地增加,您可以编写:

CSINC        X0, X0, X0, cond
if cond then

  X0 = X0

 else

  X0 = X0 + 1

该体系结构为此命令提供了一个别名 CINC。 但请注意,CINC 反转了 CSINC 的逻辑:

  • CSINC X0, X0, X0, cond 如果 cond 为 true,则保持 X0 不变;如果 cond 为 false,则递增 X0
  • CINC X0, X0, cond 如果 cond 为 true,则递增 X0;如果 cond 为 false,则保持 X0 不变

编译器选择最有效的方法来实现程序中的功能。 编译器通常会对执行简单操作的小型 if ... else 语句使用条件选择,因为条件选择比分支更有效

以下是一些简单的 if ... else 示例,它们将使用分支的实现与使用条件选择指令的等效实现进行比较:


在这些类型的示例中,条件选择具有一些优点。 无论结果如何,序列都更短并且采用相同数量的指令。

重要的是,条件选择还消除了分支的需要。 在现代处理器中,分支预测逻辑很难正确预测这种分支。 错误预测的分支可能会对性能产生负面影响,最好尽可能删除分支

26. 函数调用

当调用函数或子例程时,我们需要一种方法在完成后返回调用者。 在 B 或 BR 指令中添加 L 会将它们变成带链接的分支。 这意味着返回地址作为分支的一部分被写入 LR (X30)。

Note

LR 和 X30 名称可以互换。 汇编器(例如 GNU GAS 或 armclang)将接受两者。

有一个专门的函数返回指令 RET。 这将执行到链接寄存器中地址的间接分支。 总之,这意味着我们得到:

Note

该图显示了用 GAS 语法汇编器编写的函数 foo()。 关键字 .global 导出符号,.type 指示导出的符号是一个函数。

为什么我们需要一个特殊的函数返回指令? 从功能上来说,BR LR 的作用与 RET 相同。 使用 RET 告诉处理器这是一个函数返回。 大多数现代处理器和所有 Cortex-A 处理器都支持分支预测。 知道这是函数返回可以让处理器更准确地预测分支。

分支预测器猜测程序流程跨分支的方向。 猜测用于决定将哪些内容加载到等待处理的指令的管道中。 如果分支预测器猜测正确,则管道具有正确的指令,并且处理器不必等待从内存加载指令。

27. Procedure Call Standard (过程调用标准)

Arm 架构对通用寄存器的使用方式几乎没有限制。 回顾一下,整数寄存器和浮点寄存器是通用寄存器。 但是,如果您希望代码与其他人编写的代码或编译器生成的代码进行交互,那么您需要同意寄存器使用规则。 对于 Arm 架构,这些规则称为过程调用标准 (Procedure Call Standard),即 PCS。

PCS 规定:

  • 哪些寄存器用于将参数传递到函数中
  • 哪些寄存器用于向执行调用的函数(称为调用者)返回一个值
  • 被调用的函数(称为被调用者)可能会损坏哪些寄存器
  • 被调用者不能损坏哪些寄存器

思考一个从 main() 调用的函数 foo():

PCS 表示第一个参数在 X0 中传递,第二个参数在 X1 中传递,依此类推直至 X7。 更多的参数在堆栈上传递。我们的函数 foo() 有两个参数:b 和 c。 因此,b 将在 W0 中,c 将在 W1 中。

为什么是 W 而不是 X? 因为参数是 32 位类型,所以我们只需要一个 W 寄存器

Note

在C++中,X0 用于传递指向被调用函数的隐式this指针

OC 中,X0 代表 self、X1 代表 cmd

接下来,PCS 定义哪些寄存器可以被损坏,哪些寄存器不能被损坏。 如果寄存器可能被损坏,则被调用的函数可以覆盖而无需恢复,如 PCS 寄存器规则表所示:

例如,函数 foo() 可以使用寄存器 X0 到 X15,而无需保留它们的值。 但是,如果 foo() 想要使用 X19 到 X28,则必须先将它们保存到堆栈中,然后在返回之前从堆栈中恢复。

一些寄存器在 PCS 中具有特殊意义:

  • XR - 这是一个间接结果寄存器。 如果 foo() 返回一个结构体,那么该结构体的内存将由调用者(前面示例中的 main())分配。 XR 是指向调用者为返回结构体分配的内存的指针。
  • IP0IP1 - 这些寄存器是程序调用内易损坏的寄存器。这些寄存器可能在函数被调用和到达函数中的第一条指令之间被损坏。链接器使用这些寄存器在调用者和被调用者之间插入一小段代码(粘合代码) 。最常见的例子是分支范围扩展。A64中的分支指令范围有限。如果目标超出该范围,则链接器需要生成一小段代码(粘合代码) 以扩展分支的范围。
  • FP(X29) 帧指针。(等同于 x86 汇编中的 sp 寄存器)
  • LR (X30) 是函数调用的链接寄存器(LR)

Note

我们之前介绍了 ALU 标志,它们用于条件分支和条件选择。 PCS 表示 ALU 标志不需要在函数调用中保留

浮点寄存器有一组类似的规则:

28. 系统调用

有时,软件需要向更有特权的实体请求功能。 例如,当应用程序请求操作系统打开文件时,可能会发生这种情况

在 A64 中,有进行此类系统调用的特殊指令。这些指令会导致异常,从而允许受控地进入更高特权的异常级别。

  • SVC Supervisor 调用导致针对 EL1 的异常。由应用程序用来调用操作系统。
  • HVC Hypervisor 调用导致针对 EL2 的异常。由操作系统用来调用虚拟机管理程序,在 EL0 上不可用。
  • SMC 安全监视器调用导致针对 EL3 的异常。 由操作系统或虚拟机管理程序用来调用 EL3 固件,在 EL0 上不可用。

如果异常是从高于目标异常级别的异常级别执行的,则该异常将被带到当前异常级别。这意味着 EL2 处的 SVC 将导致异常进入 EL2。 类似地,EL3 处的 HVC 会导致异常进入 EL3。 这与异常永远不会导致处理器失去特权的规则是一致的

29. 相关信息

以下是与本指南中的材料相关的一些资源:

指令集资源

Procedure Call Standard(过程调用约定)

参考资料

developer.arm.com/documentati…