一、比较指令
1.1、CMN指令
我们已经介绍了 CMP 指令,CMP 指令还有另外一个变种——CMN,CMN 指令用来将一个数与另一个数的相反数进行比较。 指令的基本格式如下。
CMN <Xn|SP>, #<imm>{, <shift>}
CMN <Xn|SP>, <R><m>{, <extend> {#<amount>}}
上述两条 CMN 指令分别等同于如下的 ADDS 指令。
CMN 指令的计算过程就是把第一个操作数加上第二个操作数,计算结果会影响 PSTATE 寄存器的 N、 C、 V、Z 标志位。如果 CMN 后面的跳转指令使用与标志位相关的条件后缀,例如 CS 或者 CC 等,那么可以根据 N、 C、 V、Z 标志位来进行跳转。
1.2、CSEL 指令
CSEL 指令的格式如下。
CSEL <Xd>, <Xn>, <Xm>, <cond>
CSEL 指令的作用是判断 cond 是否为真。如果为真,则返回 Xn;否则,返回 Xm,把结果写入 Xd 寄存器中。
CSEL 指令常常需要和 CMP 比较指令搭配使用。
使用 CSEL 指令来实现下面的 C 语言函数。
unsigned long csel_test(unsigned long a, unsigned long b)
{
if (a >= b)
return b + 2;
else
return b - 1;
}
上述 C 语言代码可改成如下汇编代码。
.global csel_test
csel_test:
cmp x0, x1
add x2, x1, #2
sub x3, x1, #1
csel x0, x2, x3, ge
ret
汇编函数采用 X0 寄存器的值作为第一个形参,以 X1 寄存器的值作为第二个形参。CSEL 指令里采用 GE 这个条件判断码,它会根据前面的 CMP 比较指令的结果进行判断,GE 表示无符号大于或者等于。
1.3、CSET 指令
CSET 指令的格式如下。
CSET <Xd>, <cond>
CSET 指令的意思是当 cond 条件为真时设置 Xd 寄存器为 1,否则设置为 0。
使用 CSET 指令来实现下面的 C 语言函数。
boot cset_test(unsigned long a, unsigned long b)
{
return (a > b) ? true : false;
}
上述 C 语言代码改成汇编代码如下。
.global cset_test
cset_test:
cmp x0, x1
cset x0, hi
ret
CSET 指令里采用 HI 这个条件判断码,它会根据前面的 CMP 比较指令的结果进行判断,HI 表示无符号大于。当满足条件时,cset_test 函数返回 1;否则,返回 0。
1.4、CSINC 指令
CSINC 指令的格式如下。
CSINC <Xd>, <Xn>, <Xm>, <cond>
CSINC 指令的意思是当 cond 为真时, 返回 Xn 寄存器的值; 否则, 返回 Xm寄存器的值加 1。
二、跳转与返回指令
2.1、跳转指令
编写汇编代码常常会使用跳转指令, A64 指令集提供了多种不同功能的跳转指令,如下表所示:
指令 |
描述 |
B |
跳转指令。指令的格式如下。 B label 该跳转指令可以在当前 PC 偏移量 ±128 MB 的范围内无条件地跳转到 label 处 |
B.cond |
有条件的跳转指令。指令的格式如下。 B.cond label 如 B.EQ,该跳转指令可以在当前 PC 偏移量 ±1 MB 的范围内有条件地跳转到 label 处 |
BL |
带返回地址的跳转指令。指令的格式如下。 BL label |
BR |
跳转到寄存器指定的地址。指令的格式如下。 BR Xn |
BLR |
跳转到寄存器指定的地址。指令的格式如下。 BLR Xn 和 BR 指令类似,不同的地方是,BLR 指令将返回地址设置到 LR(X30 寄存器)中 |
2.2、返回指令
A64 指令集提供了两条返回指令。
- RET 指令:通常用于子函数的返回,其返回地址保存在 LR 里。
- ERET 指令:从当前的异常模式返回。它会把 SPSR 的内容恢复到 PSTATE 寄存器中,从 ELR 中获取跳转地址并返回到该地址。ERET 指令可以实现处理器模式的切换,比如从 EL1 切换到 EL0。
2.3、比较并跳转指令
A64 指令集还提供了几个比较并跳转指令,如下表所示:
指令 |
描述 |
CBZ |
比较并跳转指令。指令的格式如下。 CBZ Xt, label 判断 Xt 寄存器是否为 0,若为 0,则跳转到 label 处,跳转范围是当前 PC 相对偏移量 ±1 MB |
CBNZ |
比较并跳转指令。指令的格式如下。 CBNZ Xt, label 判断 Xt 寄存器是否不为 0,若不为 0,则跳转到 label 处,跳转范围是当前 PC 相对偏移量 ±1 MB |
TBZ |
测试位并跳转指令。指令的格式如下。 TBZ R<t>, #imm, label 判断 Rt 寄存器中第 imm 位是否为 0,若为 0,则跳转到 label 处,跳转范围是当前 PC 相对偏移量 ±32 KB |
TBNZ |
测试位并跳转指令。指令的格式如下。 TBNZ R<t>, #imm, label 判断 Rt 寄存器中第 imm 位是否不为 0,若不为 0,则跳转到 label 处,跳转范围是当前 PC 相对偏移量 ±32 KB |
三、其他重要指令
3.1、PC相对地址加载指令
A64 指令集提供了 PC 相对地址加载指令——ADR 和 ADRP 指令。
ADR 指令的格式如下。
ADR <Xd>, <label>
ADR 指令加载一个在当前 PC 值±1 MB 范围内的 label 地址到 Xd 寄存器中。
ADRP 指令的格式如下。
ADRP <Xd>, <label>
ADRP 指令加载一个在当前 PC 值一定范围内的 label 地址到 Xd 寄存器中, 这个地址与 label 所在的地址按 4 KB 对齐,偏移量的范围为 -4 GB~4 GB。
既然 LDR 伪指令和 ADRP 指令都可以加载 label 的地址,而且 LDR 伪指令可以寻址 64 位的地址空间,而 ADRP 指令的寻址范围为当前 PC 地址 ±4 GB,那么有了 LDR 伪指令为什么还需要 ADRP 指令呢?
本文借助AI工具回答这个问题,仅供参考。
1、代码效率方面
ADRP
指令在生成页面对齐地址时,指令格式更简洁,执行效率更高。编译器在处理ADRP
指令时,相对更容易进行优化。而LDR
伪指令在某些情况下,编译器需要额外的处理来生成合适的机器码。
示例:
假设要加载一个距离当前PC
地址不远的全局变量地址。
.global my_variable
my_variable:.word 0x12345678
ADRP X0, my_variable
ADD X0, X0, :lo12:my_variable
在上述代码中,ADRP
指令快速生成my_variable
所在页面的基地址到X0
寄存器,然后通过ADD
指令加上低 12 位偏移量获取准确地址。
如果使用LDR
伪指令:
.global my_variable
my_variable:.word 0x12345678
LDR X0, =my_variable
编译器可能会将其转换为更复杂的指令序列,比如先将地址存储在文字池(literal pool),再从文字池加载到寄存器,相比之下会多一些额外的操作,在代码执行效率上可能不如ADRP
。
2、位置无关代码(PIC)编写方面
在编写共享库等需要位置无关代码的场景下,ADRP
指令有着天然的优势。它基于PC
相对寻址,使得代码在不同的内存位置都能正确执行。而LDR
伪指令虽然也能加载地址,但在位置无关性的处理上没有ADRP
指令直接。
示例:
在共享库中定义一个函数来访问全局变量:
// 共享库中的代码
.global shared_variable
shared_variable:.word 0x98765432
.section ".text"
.global my_shared_function
my_shared_function:
ADRP X1, shared_variable
ADD X1, X1, :lo12:shared_variable
LDR W2, [X1]
// 对加载到的变量值进行处理
//...
RET
当这个共享库被加载到不同进程的不同内存地址时,ADRP
基于PC
相对计算地址的方式,能保证正确获取到shared_variable
的地址。
如果使用LDR
伪指令:
// 尝试使用LDR伪指令的方式
.global shared_variable
shared_variable:.word 0x98765432
.section ".text"
.global my_shared_function
my_shared_function:
LDR X1, =shared_variable
LDR W2, [X1]
// 对加载到的变量值进行处理
//...
RET
在共享库被加载到不同位置时,编译器处理LDR
伪指令生成的代码可能无法正确适应地址的变化,导致获取到错误的地址。
3.2、内存独占访问指令
ARMv8 体系结构都提供独占内存访问(exclusive memory access)的指令。在 A64 指令集中, LDXR 指令尝试在内存总线中申请一个独占访问的锁,然后访问一个内存地址。 STXR 指令会往刚才 LDXR 指令已经申请独占访问的内存地址中写入新内容。通常组合使用 LDXR 和 STXR 指令来完成一些同步操作。
另外,ARMv8 还提供多字节( 字节)独占访问的指令,即 LDXP 和 STXP 指令。独占内存访问指令如下表所示:
指令 |
描述 |
LDXR |
独占内存访问指令。指令的格式如下。 LDXR Xt, [Xn|SP{,#0}] |
STXR |
独占内存访问指令。指令的格式如下。 STXR Ws, Xt, [Xn|SP{,#0}] |
LDXP |
多字节独占内存访问指令。指令的格式如下。 LDXP Xt1, Xt2, [Xn|SP{,#0}] |
STXP |
多字节独占内存访问指令。指令的格式如下。 STXP Ws, Xt1, Xt2, [Xn|SP{,#0}] |
3.3、异常处理指令
A64 指令集支持多个异常处理指令,如下表所示:
指令 |
描述 |
SVC |
系统调用指令。指令的格式如下。 SVC #imm 允许应用程序通过 指令自陷到操作系统中,通常会陷入 EL1 |
HVC |
虚拟化系统调用指令。指令的格式如下。 允许主机操作系统通过 HVC 指令自陷到虚拟机管理程序(hypervisor)中,通常会陷入 EL2 |
SMC |
安全监控系统调用指令。指令的格式如下。 SMC #imm |