A64 指令集提供了许多不同种类的分支指令。对于简单的相对分支,即那些从当前地址偏移的分支,使用 B 指令。无条件简单相对分支可以从当前程序计数器位置向后或向前分支最多 128MB。 有条件的简单相对分支,其中条件代码附加到 B,具有 ±1MB 的较小范围。
调用子程序时,需要将返回地址存储在链接寄存器(X30)中,使用 BL 指令。这没有条件版本。BL 的行为类似于 B 指令,具有将返回地址存储在寄存器 X30 中的附加作用,返回地址是 BL 之后指令的地址。
除了这些 PC 相关指令外,A64 指令集还包括两个绝对分支。 BR Xn 指令对 Xn 中的地址执行绝对分支,而 BLR Xn 具有相同的效果,但还将返回地址存储在 X30(链接寄存器)中。 RET 指令的行为类似于 BR Xn,但它向分支预测逻辑提示它是一个函数返回。RET 默认分支到 X30 中的地址,但可以指定其他寄存器。
A64 指令集包括一些特殊的条件分支。在某些情况下,这些允许提高代码密度,因为不需要显式比较。
CBZ Rt, label // 如果为零则比较并分支
CBNZ Rt, label // 如果不为零则比较并分支
这些指令将 32 位或 64 位源寄存器与零进行比较,然后有条件地执行分支。分支偏移量的范围为 ± 1MB。这些指令不读取或写入条件代码标志(NZCV)。
有两个类似的测试和分支指令
TBZ Rt, bit, label // 如果 Rt<bit> 为零,则测试并分支
TBNZ Rt, bit, label // 如果 Rt<bit> 不为零,则测试并分支
这些指令在立即和条件分支指定的位位置测试源寄存器中的位,具体取决于该位是设置还是清除。分支偏移量的范围为 ±32kB。与 CBZ/CBNZ 一样,这些指令不读取或写入条件代码标志(NZCV)。
一、分支指令
1.1 B
分支指令导致无条件分支到 PC 相对偏移处的标签,并提示这不是子例程调用或返回。
B <label>
<label>
是要无条件跳转到的程序标签。 它与该指令地址的偏移量在 +/-128MB 范围内,编码为“imm26”乘以 4。
下面是使用 B 指令的例子。
long long int x = 0;
long long int y = 0;
long long int z = 0;
LOGD("x=%llx y=%llx z=%llx", x, y, z);
asm volatile(
"MOV %x[x], #1\n"
"B skipy\n"
"MOV %x[y], #2\n"
"skipy:\n"
"MOV %x[z], #3\n"
:[x] "+r"(x),
[y] "+r"(y),
[z] "+r"(z)
:
: "cc", "memory");
LOGD("-----------------------------");
LOGD("x=%llx y=%llx z=%llx", x, y, z);
由于 B skipy
跳过了 MOV %x[y], #2
这条指令,因此 y 的值还是初始值。
运行结果如下:
2023-05-05 07:59:28.672 12642-12704/com.demo.myapplication D/NativeCore: x=0 y=0 z=0
2023-05-05 07:59:28.672 12642-12704/com.demo.myapplication D/NativeCore: -----------------------------
2023-05-05 07:59:28.672 12642-12704/com.demo.myapplication D/NativeCore: x=1 y=0 z=3
1.2 B.cond
有条件地分支到 PC 相对偏移处的标签,并提示这不是子例程调用或返回。
B.<cond> <label>
<cond>
是标准条件之一,以标准方式编码在“cond”字段中。
<label>
是有条件跳转到的程序标签。它与该指令地址的偏移量在 +/-1MB 范围内,编码为“imm19”乘以 4。
下面是使用 B.cond 指令的例子。
long long int x = 0;
long long int y = 0;
long long int z = 0;
LOGD("x=%llx y=%llx z=%llx", x, y, z);
asm volatile(
"MOV X0, #2\n"
"SUBS X0, X0, #1\n"
"B.EQ skipy\n"
"MOV %x[x], #1\n"
"SUBS X0, X0, #1\n"
"B.EQ skipy\n"
"MOV %x[y], #2\n"
"skipy:\n"
"MOV %x[z], #3\n"
:[x] "+r"(x),
[y] "+r"(y),
[z] "+r"(z)
:
: "cc", "memory");
LOGD("-----------------------------");
LOGD("x=%llx y=%llx z=%llx", x, y, z);
由于第二条 B.EQ skipy
跳过了 MOV %x[y], #2
这条指令,因此 y 的值还是初始值。第一条 B.EQ skipy
指令因为 第一条 SUBS X0, X0, #1
指令并没有设置 Z 标志位,所以没有跳转,否则 x 的值最终也不会改变。
运行结果如下:
2023-05-05 08:08:27.579 16848-16909/com.demo.myapplication D/NativeCore: x=0 y=0 z=0
2023-05-05 08:08:27.579 16848-16909/com.demo.myapplication D/NativeCore: -----------------------------
2023-05-05 08:08:27.579 16848-16909/com.demo.myapplication D/NativeCore: x=1 y=0 z=3
1.3 BL
BL(Branch with Link)指令分支到 PC 相对偏移,将寄存器 X30 设置为 PC + 4。它提供了这是一个子例程调用的提示。
BL <label>
<label>
是要无条件跳转到的程序标签。它与该指令地址的偏移量在 +/-128MB 范围内,编码为“imm26”乘以 4。
下面是使用 BL 指令的例子。
long long int x = 0;
long long int y = 0;
long long int z = 0;
LOGD("x=%llx y=%llx z=%llx", x, y, z);
asm volatile(
"BL mod_y\n"
"MOV %x[x], #1\n"
"B mod_z\n"
"MOV %x[x], #4\n"
"mod_y:\n"
"MOV %x[y], #2\n"
"BR LR\n"
"mod_z:\n"
"MOV %x[z], #3\n"
:[x] "+r"(x),
[y] "+r"(y),
[z] "+r"(z)
:
: "cc", "memory");
LOGD("-----------------------------");
LOGD("x=%llx y=%llx z=%llx", x, y, z);
运行结果如下:
2023-05-05 09:13:40.038 15433-15580/com.demo.myapplication D/NativeCore: x=0 y=0 z=0
2023-05-05 09:13:40.038 15433-15580/com.demo.myapplication D/NativeCore: -----------------------------
2023-05-05 09:13:40.038 15433-15580/com.demo.myapplication D/NativeCore: x=1 y=2 z=3
1.4 BR
BR (Branch to Register)指令无条件地分支到寄存器中的地址,并提示这不是子程序返回。
BR <Xn>
<Xn>
是保存要分支到的地址的通用寄存器的 64 位名称,在“Rn”字段中编码。
BR 指令的例子参考 BL 指令的例子即可。
1.5 BLR
BLR(Branch with Link to Register)指令在寄存器中的地址调用子程序,将寄存器 X30 设置为 PC+4。
BLR <Xn>
<Xn>
是保存要分支到的地址的通用寄存器的 64 位名称,在“Rn”字段中编码。
下面是使用 BLR 指令的例子。
long long int x = 0;
long long int y = 0;
long long int z = 0;
LOGD("x=%llx y=%llx z=%llx", x, y, z);
asm volatile(
"BL mod_y\n"
"MOV %x[x], #1\n"
"BR LR\n"
"MOV %x[x], #4\n"
"mod_y:\n"
"MOV %x[y], #2\n"
"BLR LR\n"
"MOV %x[y], #5\n"
"mod_z:\n"
"MOV %x[z], #3\n"
:[x] "+r"(x),
[y] "+r"(y),
[z] "+r"(z)
:
: "cc", "memory");
LOGD("-----------------------------");
LOGD("x=%llx y=%llx z=%llx", x, y, z);
BLR 指令执行会设置 LR 寄存器的值为 PC + 4,BR 指令则只分支!
运行结果如下:
2023-05-07 15:58:22.939 4243-4243/com.example.myapplication D/native-armv8a: x=0 y=0 z=0
2023-05-07 15:58:22.939 4243-4243/com.example.myapplication D/native-armv8a: -----------------------------
2023-05-07 15:58:22.939 4243-4243/com.example.myapplication D/native-armv8a: x=1 y=5 z=3
1.6 RET
无条件地从子程序分支返回到寄存器中的地址,并提示这是子程序返回。
RET {<Xn>}
<Xn>
是保存要分支到的地址的通用寄存器的 64 位名称,在“Rn”字段中编码。 如果不存在则默认为 X30。
下面是使用 RET 指令的例子。将上面例子中 BR LR 改为 RET,x、y、z 最终的值和上面的例子保持一致。
long long int x = 0;
long long int y = 0;
long long int z = 0;
LOGD("x=%llx y=%llx z=%llx", x, y, z);
asm volatile(
"BL mod_y\n"
"MOV %x[x], #1\n"
"RET\n"
"MOV %x[x], #4\n"
"mod_y:\n"
"MOV %x[y], #2\n"
"BLR LR\n"
"MOV %x[y], #5\n"
"mod_z:\n"
"MOV %x[z], #3\n"
:[x] "+r"(x),
[y] "+r"(y),
[z] "+r"(z)
:
: "cc", "memory");
LOGD("-----------------------------");
LOGD("x=%llx y=%llx z=%llx", x, y, z);
运行结果如下:
2023-05-07 16:21:41.276 19720-19720/com.example.myapplication D/native-armv8a: x=0 y=0 z=0
2023-05-07 16:21:41.276 19720-19720/com.example.myapplication D/native-armv8a: -----------------------------
2023-05-07 16:21:41.276 19720-19720/com.example.myapplication D/native-armv8a: x=1 y=5 z=3
二、条件分支指令
2.1 CBZ
CBZ(Compare and Branch on Zero)指令将寄存器中的值与零进行比较,如果比较相等,则有条件地分支到 PC 相对偏移量处的标签。它提供了这不是子例程调用或返回的提示。该指令不影响条件标志。
32-bit (sf == 0)
CBZ <Wt>, <label>
64-bit (sf == 1)
CBZ <Xt>, <label>
<Wt>
是要测试的通用寄存器的 32 位名称,编码在“Rt”字段中。
<Xt>
是要测试的通用寄存器的 64 位名称,编码在“Rt”字段中。
<label>
是有条件跳转到的程序标签。它与该指令地址的偏移量在 +/-1MB 范围内,编码为“imm19”乘以 4。
下面是使用 CBZ 指令的例子。
long long int x = 0;
long long int y = 0;
LOGD("x=%llx y=%llx", x, y);
asm volatile(
"MOV X0, #1\n"
"CBZ X0, mod_y\n"
"MOV %x[x], #1\n"
"SUB X0, X0, #1\n"
"CBZ X0, mod_y\n"
"MOV %x[x], #3\n"
"mod_y:\n"
"MOV %x[y], #2\n"
:[x] "+r"(x),
[y] "+r"(y)
:
: "cc", "memory");
LOGD("-----------------------------");
LOGD("x=%llx y=%llx", x, y);
第一次执行 CBZ X0, mod_y
,X0 的值不为 0,因此会继续向下执行到 MOV %x[x], #1
,接着 SUB X0, X0, #1
对 X0 寄存器减一操作,X0 的值已经为 0,第二次执行 CBZ X0, mod_y
已经满足分支到 mod_y 标签处,所以直接跳到 mod_y 处执行(跳过了 MOV %x[x], #3
)。也就是最终 x 的值为 1,y 的值为 2。
运行结果如下:
2023-05-07 16:29:49.401 22423-22423/com.example.myapplication D/native-armv8a: x=0 y=0
2023-05-07 16:29:49.401 22423-22423/com.example.myapplication D/native-armv8a: -----------------------------
2023-05-07 16:29:49.401 22423-22423/com.example.myapplication D/native-armv8a: x=1 y=2
2.2 CBNZ
CBNZ(Compare and Branch on Nonzero)指令将寄存器中的值与零进行比较,如果比较不相等,则有条件地分支到 PC 相对偏移处的标签。它提供了这不是子例程调用或返回的提示。该指令不影响条件标志。
32-bit (sf == 0)
CBNZ <Wt>, <label>
64-bit (sf == 1)
CBNZ <Xt>, <label>
<Wt>
是要测试的通用寄存器的 32 位名称,编码在“Rt”字段中。
<Xt>
是要测试的通用寄存器的 64 位名称,编码在“Rt”字段中。
<label>
是有条件跳转到的程序标签。它与该指令地址的偏移量在 +/-1MB 范围内,编码为“imm19”乘以 4。
下面是使用 CBNZ 指令的例子。修改 CBZ 指令例子中的第一条 CBZ 指令为 CBNZ 即可。
long long int x = 0;
long long int y = 0;
LOGD("x=%llx y=%llx", x, y);
asm volatile(
"MOV X0, #1\n"
"CBNZ X0, mod_y\n"
"MOV %x[x], #1\n"
"SUB X0, X0, #1\n"
"CBZ X0, mod_y\n"
"MOV %x[x], #3\n"
"mod_y:\n"
"MOV %x[y], #2\n"
:[x] "+r"(x),
[y] "+r"(y)
:
: "cc", "memory");
LOGD("-----------------------------");
LOGD("x=%llx y=%llx", x, y);
第一次执行 CBNZ X0, mod_y
,X0 的值不为 0,因此直接分支到 mod_y 标签处,所以直接跳到 mod_y 处执行(跳过了 中间的其他指令)。也就是最终 x 的值还为 0,但 y 的值已经被修改为 2。
运行结果如下:
2023-05-07 16:36:26.884 31211-31211/com.example.myapplication D/native-armv8a: x=0 y=0
2023-05-07 16:36:26.884 31211-31211/com.example.myapplication D/native-armv8a: -----------------------------
2023-05-07 16:36:26.884 31211-31211/com.example.myapplication D/native-armv8a: x=0 y=2
2.3 TBZ
TBZ(Test bit and Branch if Zero)指令将测试位的值与零进行比较,如果比较相等,则有条件地分支到 PC 相对偏移处的标签。它提供了这不是子例程调用或返回的提示。该指令不影响条件标志。
TBZ <R><t>, #<imm>, <label>
<R>
是宽度说明符,在“b5”字段中编码。它可以具有以下值:
R | b5 |
---|---|
W | 0 |
X | 1 |
在汇编程序源代码中,始终允许使用“X”说明符,但仅当位数小于 32 时才允许使用“W”说明符。
<t>
为待测通用寄存器的编号 [0-30] 或名称 ZR(31),编码在“Rt”字段中。
<imm>
为要测试的位数,取值范围为 0~63,编码为“b5:b40”。
<label>
是有条件跳转到的程序标签。它与该指令地址的偏移量在 +/-32KB 范围内,被编码为“imm14”乘以 4。
下面是使用 TBZ 指令的例子。
long long int x = 0;
long long int y = 0;
LOGD("x=%llx y=%llx", x, y);
asm volatile(
"MOV X0, #0b100\n"
"TBZ X0, 2, mod_y\n"
"MOV %x[x], #1\n"
"TBZ X0, 1, mod_y\n"
"MOV %x[x], #3\n"
"mod_y:\n"
"MOV %x[y], #2\n"
:[x] "+r"(x),
[y] "+r"(y)
:
: "cc", "memory");
LOGD("-----------------------------");
LOGD("x=%llx y=%llx", x, y);
执行 MOV X0, #0b100
后,X0 的值为 0b100(也就是从低到高第三位不为 0,从 0 开始计数的话即位的 index 为 2),TBZ X0, 2, mod_y
测试并比较位 index 为 2 处 X0 对应的位,此位为 1,所以不满足 TBZ 分支到 mod_y 处的情况,这会继续执行 MOV %x[x], #1
,第二次执行 TBZ 指令 TBZ X0, 1, mod\_y
,由于满足分支条件,直接跳到 mod_y 处执行(跳过了 MOV %x[x], #3
),最终 x 的值为 1,但 y 的值已经被修改为 2。
运行结果如下:
2023-05-07 17:16:43.303 13323-13323/com.example.myapplication D/native-armv8a: x=0 y=0
2023-05-07 17:16:43.303 13323-13323/com.example.myapplication D/native-armv8a: -----------------------------
2023-05-07 17:16:43.303 13323-13323/com.example.myapplication D/native-armv8a: x=1 y=2
2.4 TBNZ
TBNZ(Test bit and Branch if Nonzero)指令将通用寄存器中的位值与零进行比较,如果比较不相等,则有条件地分支到 PC 相对偏移量处的标签。它提供了这不是子例程调用或返回的提示。该指令不影响条件标志。
TBNZ <R><t>, #<imm>, <label>
<R>
是宽度说明符,在“b5”字段中编码。它可以具有以下值:
R | b5 |
---|---|
W | 0 |
X | 1 |
在汇编程序源代码中,始终允许使用“X”说明符,但仅当位数小于 32 时才允许使用“W”说明符。
<t>
为待测通用寄存器的编号 [0-30] 或名称 ZR(31),编码在“Rt”字段中。
<imm>
为要测试的位数,取值范围为 0~63,编码为“b5:b40”。
<label>
是有条件跳转到的程序标签。它与该指令地址的偏移量在 +/-32KB 范围内,被编码为“imm14”乘以 4。
下面是使用 TBNZ 指令的例子。
long long int x = 0;
long long int y = 0;
LOGD("x=%llx y=%llx", x, y);
asm volatile(
"MOV X0, #0b100\n"
"TBNZ X0, 2, mod_y\n"
"MOV %x[x], #1\n"
"mod_y:\n"
"MOV %x[y], #2\n"
:[x] "+r"(x),
[y] "+r"(y)
:
: "cc", "memory");
LOGD("-----------------------------");
LOGD("x=%llx y=%llx", x, y);
执行 MOV X0, #0b100
后,X0 的值为 0b100(也就是从低到高第三位不为 0,从 0 开始计数的话即位的 index 为 2),TBNZ X0, 2, mod_y
测试并比较位 index 为 2 处 X0 对应的位,此位为 1,所以满足 TBNZ 分支到 mod_y 处的情况,这会跳过 MOV %x[x], #1
执行,最终 x 的值还为 0,但 y 的值已经被修改为 2。
运行结果如下:
2023-05-07 17:07:33.926 10241-10241/com.example.myapplication D/native-armv8a: x=0 y=0
2023-05-07 17:07:33.926 10241-10241/com.example.myapplication D/native-armv8a: -----------------------------
2023-05-07 17:07:33.926 10241-10241/com.example.myapplication D/native-armv8a: x=0 y=2
参考资料
1.《ARMv8-A-Programmer-Guide》
2.《Arm® A64 Instruction Set Architecture Armv8, for Armv8-A architecture profile》