A64 内存访问其他指令包括浮点和 NEON 标量加载存储指令、访问多个内存位置指令、非特权访问指令、预取内存指令、非临时加载存储对指令、内存屏障和栅栏指令、同步原语等。
一、浮点和 NEON 标量加载存储指令
加载和存储指令也可以访问浮点/NEON 寄存器。这里,大小仅由加载或存储的寄存器决定,可以是 B、H、S、D 或 Q 寄存器中的任何一个。
加载指令
存储指令
没有符号扩展选项可用于加载到 FP/SIMD 寄存器。此类加载的地址仍使用通用寄存器指定。
例如:
LDR D0, [X0, X1]
—— 将 X0 加 X1 指向的内存地址处的双字加载到寄存器 D0。
下面是的实例是实际执行 LDR D0, [%x[a], X0]
指令的效果。
long long int len = 2;
auto *arr = new long long int[len]{0};
LOGD("+++++++++++++++++++++++++++++");
for (int i = 0; i < len; i++) {
arr[i] = 0x0102030410203040 * (i + 1);
LOGD("arr[%d]=0x%llx", i, arr[i]);
}
char *a = (char *) arr;
asm volatile(
"MOV X0, #8\n"
"LDR D0, [%x[a], X0]\n"
:[a] "+r"(a)
:
: "cc", "memory");
LOGD("+++++++++++++++++++++++++++++");
delete[] arr;
我们看到 V0 寄存器在断点 1 处时,内部都为 0,当运行到断点 2 处时,已经被修改为了 arr[1]=0x204060820406080 的值了。因为 D0 实际上就是 V0 寄存器的下半部分。
浮点和标量 NEON 加载和存储使用与整数加载和存储相同的寻址模式。
二、访问多个内存位置指令
A64 不包括可用于 A32 和 T32 代码的多重加载 (LDM) 或存储 (STM) 指令。在 A64 代码中,有加载对 (LDP) 和存储对 (STP) 指令。与 A32 LDRD 和 STRD 指令不同,可以读取或写入任意两个整数寄存器。数据从相邻的内存位置读取或写入。为这些指令提供的寻址模式选项比其他内存访问指令更受限制。LDP 和 STP 指令只能使用具有缩放的 7 位带符号立即值的基址寄存器,并具有可选的前置或后置增量。与 32 位 LDRD 和 STRD 不同,LDP 和 STP 可以进行未对齐访问。
加载和存储对指令 | 描述 |
---|---|
LDP W3, W7, [X0] | 将地址 X0 处的字加载到 W3,并将地址 X0 + 4 处的字加载到 W7。 |
LDP X8, X2, [X0, #0x10]! | 将地址 X0 + 0x10 处的双字加载到 X8,将地址 X0 + 0x10 + 8 处的双字加载到 X2,并将 0x10 加到 X0。 |
LDPSW X3, X4, [X0] | 将地址 X0 处的字加载到 X3 并将地址 X0 + 4 处的字加载到 X4,并将两者符号扩展到双字大小。 |
LDP D8, D2, [X11], #0x10 | 将地址 X11 处的双字加载到 D8,将地址 X11 + 8 处的双字加载到 D2,并将 0x10 加到 X11。 |
STP X9, X8, [X4] | 将 X9 中的双字存储到地址 X4,并将 X8 中的双字存储到地址 X4 + 8。 |
LDP W3, W7, [X0]
指令图示:
LDP X8, X2, [X0, #0x10]!
指令图示:
2.1 LDP
加载寄存器对指令根据基址寄存器值和立即偏移计算地址,从内存加载两个 32 位字或两个 64 位双字,并将它们写入两个寄存器。
Post-index
32-bit (opc == 00)
LDP <Wt1>, <Wt2>, [<Xn|SP>], #<imm>
64-bit (opc == 10)
LDP <Xt1>, <Xt2>, [<Xn|SP>], #<imm>
Pre-index
32-bit (opc == 00)
LDP <Wt1>, <Wt2>, [<Xn|SP>, #<imm>]!
64-bit (opc == 10)
LDP <Xt1>, <Xt2>, [<Xn|SP>, #<imm>]!
Signed offset
32-bit (opc == 00)
LDP <Wt1>, <Wt2>, [<Xn|SP>{, #<imm>}]
64-bit (opc == 10)
LDP <Xt1>, <Xt2>, [<Xn|SP>{, #<imm>}]
<Wt1>
是要传输的第一个通用寄存器的 32 位名称,在“Rt”字段中编码。
<Wt2>
是要传输的第二个通用寄存器的 32 位名称,在“Rt2”字段中编码。
<Xt1>
是要传输的第一个通用寄存器的 64 位名称,在“Rt”字段中编码。
<Xt2>
是要传输的第二个通用寄存器的 64 位名称,在“Rt2”字段中编码。
<Xn|SP>
是通用基址寄存器或堆栈指针的 64 位名称,在“Rn”字段中编码。
<imm>
对于 32 位后索引和 32 位前索引变体:是带符号的立即字节偏移量,是 -256 到 252 范围内的 4 的倍数,在“imm7”字段中编码为 <imm>/4
。对于 32 位有符号偏移变体:是可选的有符号立即字节偏移,是 -256 到 252 范围内的 4 的倍数,默认为 0 并在“imm7”字段中编码为 <imm>/4
。
对于 64 位后索引和 64 位前索引变体:是带符号的立即字节偏移量,是 -512 到 504 范围内 8 的倍数,在“imm7”字段中编码为 <imm>/8
。对于 64 位有符号偏移变体:是可选的有符号立即字节偏移,是 -512 到 504 范围内 8 的倍数,默认为 0 并在“imm7”字段中编码为 <imm>/8
。
LDP Post-index
下面是使用 LDP Post-index 指令的例子。
long long int len = 3;
long long int x = 0;
long long int y = 0;
long long int z = 0;
long long int w = 0;
auto *arr = new long long int[len]{0};
LOGD("+++++++++++++++++++++++++++++");
LOGD("x=0x%llx y=0x%llx z=0x%llx w=0x%llx", x, y, z, w);
for (int i = 0; i < len; i++) {
arr[i] = 0x0102030410203040 * (i + 1);
LOGD("arr[%d]=0x%llx", i, arr[i]);
}
char *a = (char *) arr;
asm volatile(
"LDP %x[x], %x[y], [%x[a]], #8\n"
"LDP %x[z], %x[w], [%x[a]], #8\n"
:[a] "+r"(a),
[x] "+r"(x),
[y] "+r"(y),
[z] "+r"(z),
[w] "+r"(w)
:
: "cc", "memory");
LOGD("-----------------------------");
LOGD("x=0x%llx y=0x%llx z=0x%llx w=0x%llx", x, y, z, w);
delete[] arr;
观察结果不难发现,LDP %x[x], %x[y], [%x[a]], #8
指令先将 arr[0] 加载到 %x[x] 寄存器,接着将 arr + 8 处的值(也就是 arr[1])加载到 %x[y] 寄存器。LDP %x[z], %x[w], [%x[a]], #8
指令接着从 arr + 8 处的值(也就是 arr[1])加载到 %x[z] 寄存器,arr + 16 加载到 %x[w]。我们看到 LDP %x[x], %x[y], [%x[a]], #8
将 arr + 8 同时存储在了 %x[a] 中,地址只加了一次,而不是两次!
运行结果如下:
2023-05-02 07:40:26.204 24915-24915/com.example.myapplication D/native-armv8a: +++++++++++++++++++++++++++++
2023-05-02 07:40:26.204 24915-24915/com.example.myapplication D/native-armv8a: x=0x0 y=0x0 z=0x0 w=0x0
2023-05-02 07:40:26.204 24915-24915/com.example.myapplication D/native-armv8a: arr[0]=0x102030410203040
2023-05-02 07:40:26.204 24915-24915/com.example.myapplication D/native-armv8a: arr[1]=0x204060820406080
2023-05-02 07:40:26.204 24915-24915/com.example.myapplication D/native-armv8a: arr[2]=0x306090c306090c0
2023-05-02 07:40:26.204 24915-24915/com.example.myapplication D/native-armv8a: -----------------------------
2023-05-02 07:40:26.204 24915-24915/com.example.myapplication D/native-armv8a: x=0x102030410203040 y=0x204060820406080 z=0x204060820406080 w=0x306090c306090c0
LDP Pre-index
下面是使用 LDP Pre-index 指令的例子。将 LDP Post-index 的例子稍做修改(len 改为 4,并把 LDP 指令改为 Pre-index 的格式)。
long long int len = 4;
long long int x = 0;
long long int y = 0;
long long int z = 0;
long long int w = 0;
auto *arr = new long long int[len]{0};
LOGD("+++++++++++++++++++++++++++++");
LOGD("x=0x%llx y=0x%llx z=0x%llx w=0x%llx", x, y, z, w);
for (int i = 0; i < len; i++) {
arr[i] = 0x0102030410203040 * (i + 1);
LOGD("arr[%d]=0x%llx", i, arr[i]);
}
char *a = (char *) arr;
asm volatile(
"LDP %x[x], %x[y], [%x[a], #8]!\n"
"LDP %x[z], %x[w], [%x[a], #8]!\n"
:[a] "+r"(a),
[x] "+r"(x),
[y] "+r"(y),
[z] "+r"(z),
[w] "+r"(w)
:
: "cc", "memory");
LOGD("-----------------------------");
LOGD("x=0x%llx y=0x%llx z=0x%llx w=0x%llx", x, y, z, w);
delete[] arr;
观察结果不难发现,LDP Pre-index 首先会将地址加 8 并保存,再去加载数据到寄存器。
运行结果如下:
2023-05-02 07:47:37.285 26563-26563/com.example.myapplication D/native-armv8a: +++++++++++++++++++++++++++++
2023-05-02 07:47:37.285 26563-26563/com.example.myapplication D/native-armv8a: x=0x0 y=0x0 z=0x0 w=0x0
2023-05-02 07:47:37.285 26563-26563/com.example.myapplication D/native-armv8a: arr[0]=0x102030410203040
2023-05-02 07:47:37.285 26563-26563/com.example.myapplication D/native-armv8a: arr[1]=0x204060820406080
2023-05-02 07:47:37.285 26563-26563/com.example.myapplication D/native-armv8a: arr[2]=0x306090c306090c0
2023-05-02 07:47:37.285 26563-26563/com.example.myapplication D/native-armv8a: arr[3]=0x4080c104080c100
2023-05-02 07:47:37.285 26563-26563/com.example.myapplication D/native-armv8a: -----------------------------
2023-05-02 07:47:37.285 26563-26563/com.example.myapplication D/native-armv8a: x=0x204060820406080 y=0x306090c306090c0 z=0x306090c306090c0 w=0x4080c104080c100
LDP Signed offset
下面是使用 LDP Signed offset 指令的例子。两次 LDP 指令加载的位置都是固定的。
long long int len = 4;
long long int x = 0;
long long int y = 0;
long long int z = 0;
long long int w = 0;
auto *arr = new long long int[len]{0};
LOGD("+++++++++++++++++++++++++++++");
LOGD("x=0x%llx y=0x%llx z=0x%llx w=0x%llx", x, y, z, w);
for (int i = 0; i < len; i++) {
arr[i] = 0x0102030410203040 * (i + 1);
LOGD("arr[%d]=0x%llx", i, arr[i]);
}
char *a = (char *) arr;
asm volatile(
"LDP %x[x], %x[y], [%x[a], #8]\n"
"LDP %x[z], %x[w], [%x[a], #8]\n"
:[a] "+r"(a),
[x] "+r"(x),
[y] "+r"(y),
[z] "+r"(z),
[w] "+r"(w)
:
: "cc", "memory");
LOGD("-----------------------------");
LOGD("x=0x%llx y=0x%llx z=0x%llx w=0x%llx", x, y, z, w);
delete[] arr;
运行结果如下:
2023-05-02 07:59:42.287 19495-19495/com.example.myapplication D/native-armv8a: +++++++++++++++++++++++++++++
2023-05-02 07:59:42.287 19495-19495/com.example.myapplication D/native-armv8a: x=0x0 y=0x0 z=0x0 w=0x0
2023-05-02 07:59:42.287 19495-19495/com.example.myapplication D/native-armv8a: arr[0]=0x102030410203040
2023-05-02 07:59:42.287 19495-19495/com.example.myapplication D/native-armv8a: arr[1]=0x204060820406080
2023-05-02 07:59:42.287 19495-19495/com.example.myapplication D/native-armv8a: arr[2]=0x306090c306090c0
2023-05-02 07:59:42.287 19495-19495/com.example.myapplication D/native-armv8a: arr[3]=0x4080c104080c100
2023-05-02 07:59:42.287 19495-19495/com.example.myapplication D/native-armv8a: -----------------------------
2023-05-02 07:59:42.287 19495-19495/com.example.myapplication D/native-armv8a: x=0x204060820406080 y=0x306090c306090c0 z=0x204060820406080 w=0x306090c306090c0
2.2 LDPSW
Load Pair of Registers Signed Word 指令根据基址寄存器值和立即偏移量计算地址,从内存中加载两个 32 位字,对它们进行符号扩展,并将它们写入两个寄存器。
Post-index
LDPSW <Xt1>, <Xt2>, [<Xn|SP>], #<imm>
Pre-index
LDPSW <Xt1>, <Xt2>, [<Xn|SP>, #<imm>]!
Signed offset
LDPSW <Xt1>, <Xt2>, [<Xn|SP>{, #<imm>}]
<Xt1>
是要传输的第一个通用寄存器的 64 位名称,在“Rt”字段中编码。
<Xt2>
是要传输的第二个通用寄存器的 64 位名称,在“Rt2”字段中编码。
<Xn|SP>
是通用基址寄存器或堆栈指针的 64 位名称,在“Rn”字段中编码。
<imm>
对于后索引和前索引变体:是带符号的立即字节偏移量,是 -256 到 252 范围内的 4 的倍数,在“imm7”字段中编码为 <imm>/4
。对于带符号的偏移量变体:是可选的带符号立即字节偏移量,是 -256 到 252 范围内的 4 的倍数,默认为 0 并在“imm7”字段中编码为 <imm>/4
。
LDPSW Post-index
下面是使用 LDPSW Post-index 指令的例子。
long long int len = 4;
long long int x = 0;
long long int y = 0;
long long int z = 0;
long long int w = 0;
auto *arr = new long long int[len]{0};
LOGD("+++++++++++++++++++++++++++++");
LOGD("x=0x%llx y=0x%llx z=0x%llx w=0x%llx", x, y, z, w);
for (int i = 0; i < len; i++) {
arr[i] = 0x0102030450203040 * (i + 1);
LOGD("arr[%d]=0x%llx", i, arr[i]);
}
char *a = (char *) arr;
asm volatile(
"LDPSW %x[x], %x[y], [%x[a]], #8\n"
"LDPSW %x[z], %x[w], [%x[a]], #8\n"
:[a] "+r"(a),
[x] "+r"(x),
[y] "+r"(y),
[z] "+r"(z),
[w] "+r"(w)
:
: "cc", "memory");
LOGD("-----------------------------");
LOGD("x=0x%llx y=0x%llx z=0x%llx w=0x%llx", x, y, z, w);
delete[] arr;
LDPSW %x[x], %x[y], [%x[a]], #8
从 arr[0] 处加载 4 个字节(一个字长)到 %x[x],接着 arr + 4 处加载 4 个字节到 %x[y],最后要将 arr + 8 地址保存起来。LDPSW %x[z], %x[w], [%x[a]], #8
类似的,先从 arr + 8 处直接加载,但因为 0xa0406080 符号位为 1,因此要扩展符号位(均为 1),所以 z 的值最终为 0xffffffffa0406080。
运行结果如下:
2023-05-02 16:30:20.260 14000-14000/com.example.myapplication D/native-armv8a: +++++++++++++++++++++++++++++
2023-05-02 16:30:20.260 14000-14000/com.example.myapplication D/native-armv8a: x=0x0 y=0x0 z=0x0 w=0x0
2023-05-02 16:30:20.260 14000-14000/com.example.myapplication D/native-armv8a: arr[0]=0x102030450203040
2023-05-02 16:30:20.260 14000-14000/com.example.myapplication D/native-armv8a: arr[1]=0x2040608a0406080
2023-05-02 16:30:20.260 14000-14000/com.example.myapplication D/native-armv8a: arr[2]=0x306090cf06090c0
2023-05-02 16:30:20.260 14000-14000/com.example.myapplication D/native-armv8a: arr[3]=0x4080c114080c100
2023-05-02 16:30:20.260 14000-14000/com.example.myapplication D/native-armv8a: -----------------------------
2023-05-02 16:30:20.260 14000-14000/com.example.myapplication D/native-armv8a: x=0x50203040 y=0x1020304 z=0xffffffffa0406080 w=0x2040608
LDPSW Pre-index
下面是使用 LDPSW Pre-index 指令的例子。LDPSW Pre-index 和 LDPSW Post-index 区别在于先要将地址加上立即数后再去加载数据到寄存器。
long long int len = 4;
long long int x = 0;
long long int y = 0;
long long int z = 0;
long long int w = 0;
auto *arr = new long long int[len]{0};
LOGD("+++++++++++++++++++++++++++++");
LOGD("x=0x%llx y=0x%llx z=0x%llx w=0x%llx", x, y, z, w);
for (int i = 0; i < len; i++) {
arr[i] = 0x0102030450203040 * (i + 1);
LOGD("arr[%d]=0x%llx", i, arr[i]);
}
char *a = (char *) arr;
asm volatile(
"LDPSW %x[x], %x[y], [%x[a], #8]!\n"
"LDPSW %x[z], %x[w], [%x[a], #8]!\n"
:[a] "+r"(a),
[x] "+r"(x),
[y] "+r"(y),
[z] "+r"(z),
[w] "+r"(w)
:
: "cc", "memory");
LOGD("-----------------------------");
LOGD("x=0x%llx y=0x%llx z=0x%llx w=0x%llx", x, y, z, w);
delete[] arr;
运行结果如下:
2023-05-02 16:39:17.475 16127-16127/com.example.myapplication D/native-armv8a: +++++++++++++++++++++++++++++
2023-05-02 16:39:17.475 16127-16127/com.example.myapplication D/native-armv8a: x=0x0 y=0x0 z=0x0 w=0x0
2023-05-02 16:39:17.475 16127-16127/com.example.myapplication D/native-armv8a: arr[0]=0x102030450203040
2023-05-02 16:39:17.475 16127-16127/com.example.myapplication D/native-armv8a: arr[1]=0x2040608a0406080
2023-05-02 16:39:17.475 16127-16127/com.example.myapplication D/native-armv8a: arr[2]=0x306090cf06090c0
2023-05-02 16:39:17.475 16127-16127/com.example.myapplication D/native-armv8a: arr[3]=0x4080c114080c100
2023-05-02 16:39:17.475 16127-16127/com.example.myapplication D/native-armv8a: -----------------------------
2023-05-02 16:39:17.475 16127-16127/com.example.myapplication D/native-armv8a: x=0xffffffffa0406080 y=0x2040608 z=0xfffffffff06090c0 w=0x306090c
LDPSW Signed offset
下面是使用 LDPSW Signed offset 指令的例子。把 LDPSW Pre-index 例子中的 LDPSW 指令感叹号去掉。每次都是从固定的 arr + 8 处开始加载,也就是 arr[1]。
long long int len = 4;
long long int x = 0;
long long int y = 0;
long long int z = 0;
long long int w = 0;
auto *arr = new long long int[len]{0};
LOGD("+++++++++++++++++++++++++++++");
LOGD("x=0x%llx y=0x%llx z=0x%llx w=0x%llx", x, y, z, w);
for (int i = 0; i < len; i++) {
arr[i] = 0x0102030450203040 * (i + 1);
LOGD("arr[%d]=0x%llx", i, arr[i]);
}
char *a = (char *) arr;
asm volatile(
"LDPSW %x[x], %x[y], [%x[a], #8]\n"
"LDPSW %x[z], %x[w], [%x[a], #8]\n"
:[a] "+r"(a),
[x] "+r"(x),
[y] "+r"(y),
[z] "+r"(z),
[w] "+r"(w)
:
: "cc", "memory");
LOGD("-----------------------------");
LOGD("x=0x%llx y=0x%llx z=0x%llx w=0x%llx", x, y, z, w);
delete[] arr;
运行结果如下:
2023-05-02 16:43:12.228 17276-17276/com.example.myapplication D/native-armv8a: +++++++++++++++++++++++++++++
2023-05-02 16:43:12.228 17276-17276/com.example.myapplication D/native-armv8a: x=0x0 y=0x0 z=0x0 w=0x0
2023-05-02 16:43:12.228 17276-17276/com.example.myapplication D/native-armv8a: arr[0]=0x102030450203040
2023-05-02 16:43:12.228 17276-17276/com.example.myapplication D/native-armv8a: arr[1]=0x2040608a0406080
2023-05-02 16:43:12.228 17276-17276/com.example.myapplication D/native-armv8a: arr[2]=0x306090cf06090c0
2023-05-02 16:43:12.228 17276-17276/com.example.myapplication D/native-armv8a: arr[3]=0x4080c114080c100
2023-05-02 16:43:12.228 17276-17276/com.example.myapplication D/native-armv8a: -----------------------------
2023-05-02 16:43:12.228 17276-17276/com.example.myapplication D/native-armv8a: x=0xffffffffa0406080 y=0x2040608 z=0xffffffffa0406080 w=0x2040608
2.3 STP
Store Pair of Registers 指令根据基址寄存器值和立即偏移量计算地址,并将两个 32 位字或两个 64 位双字从两个寄存器存储到计算出的地址。
Post-index
32-bit (opc == 00)
STP <Wt1>, <Wt2>, [<Xn|SP>], #<imm>
64-bit (opc == 10)
STP <Xt1>, <Xt2>, [<Xn|SP>], #<imm>
Pre-index
32-bit (opc == 00)
STP <Wt1>, <Wt2>, [<Xn|SP>, #<imm>]!
64-bit (opc == 10)
STP <Xt1>, <Xt2>, [<Xn|SP>, #<imm>]!
Signed offset
32-bit (opc == 00)
STP <Wt1>, <Wt2>, [<Xn|SP>{, #<imm>}]
64-bit (opc == 10)
STP <Xt1>, <Xt2>, [<Xn|SP>{, #<imm>}]
<Wt1>
是要传输的第一个通用寄存器的 32 位名称,在“Rt”字段中编码。
<Wt2>
是要传输的第二个通用寄存器的 32 位名称,在“Rt2”字段中编码。
<Xt1>
是要传输的第一个通用寄存器的 64 位名称,在“Rt”字段中编码。
<Xt2>
是要传输的第二个通用寄存器的 64 位名称,在“Rt2”字段中编码。
<Xn|SP>
是通用基址寄存器或堆栈指针的 64 位名称,在“Rn”字段中编码。
<imm>
对于 32 位后索引和 32 位前索引变体:是带符号的立即字节偏移量,是 -256 到 252 范围内的 4 的倍数,在“imm7”字段中编码为 <imm>/4
。对于 32 位有符号偏移变体:是可选的有符号立即字节偏移,是 -256 到 252 范围内的 4 的倍数,默认为 0 并在“imm7”字段中编码为 <imm>/4
。
对于 64 位后索引和 64 位前索引变体:是带符号的立即字节偏移量,是 -512 到 504 范围内 8 的倍数,在“imm7”字段中编码为 <imm>/8
。对于 64 位有符号偏移变体:是可选的有符号立即字节偏移,是 -512 到 504 范围内的 8 的倍数,默认为 0 并在“imm7”字段中编码为 <imm>/8
。
STP Post-index
下面是使用 STP Post-index 指令的例子。
long long int len = 4;
long long int x = 0x102030450203040;
long long int y = 0x2040608a0406080;
long long int z = 0x306090cf06090c0;
long long int w = 0x4080c114080c100;
auto *arr = new long long int[len]{0};
LOGD("+++++++++++++++++++++++++++++");
LOGD("x=0x%llx y=0x%llx z=0x%llx w=0x%llx", x, y, z, w);
char *a = (char *) arr;
asm volatile(
"STP %x[x], %x[y], [%x[a]], #8\n"
"STP %x[z], %x[w], [%x[a]], #8\n"
:[a] "+r"(a),
[x] "+r"(x),
[y] "+r"(y),
[z] "+r"(z),
[w] "+r"(w)
:
: "cc", "memory");
LOGD("-----------------------------");
for (int i = 0; i < len; i++) {
LOGD("arr[%d]=0x%llx", i, arr[i]);
}
delete[] arr;
STP %x[x], %x[y], [%x[a]], #8
首先将 %x[x] 存储到 arr 处,%x[y] 存储到 arr + 8 处,并将 arr + 8 地址保存。同样 STP %x[z], %x[w], [%x[a]], #8
先将 %x[z] 存储到 arr + 8 处,%x[w] 存储到 arr + 16 处,,并将 arr + 16 地址保存。
运行结果如下:
2023-05-02 17:19:09.208 1431-1431/com.example.myapplication D/native-armv8a: +++++++++++++++++++++++++++++
2023-05-02 17:19:09.208 1431-1431/com.example.myapplication D/native-armv8a: x=0x102030450203040 y=0x2040608a0406080 z=0x306090cf06090c0 w=0x4080c114080c100
2023-05-02 17:19:09.208 1431-1431/com.example.myapplication D/native-armv8a: -----------------------------
2023-05-02 17:19:09.208 1431-1431/com.example.myapplication D/native-armv8a: arr[0]=0x102030450203040
2023-05-02 17:19:09.208 1431-1431/com.example.myapplication D/native-armv8a: arr[1]=0x306090cf06090c0
2023-05-02 17:19:09.208 1431-1431/com.example.myapplication D/native-armv8a: arr[2]=0x4080c114080c100
2023-05-02 17:19:09.208 1431-1431/com.example.myapplication D/native-armv8a: arr[3]=0x0
STP Pre-index
下面是使用 STP Pre-index 指令的例子。
long long int len = 4;
long long int x = 0x102030450203040;
long long int y = 0x2040608a0406080;
long long int z = 0x306090cf06090c0;
long long int w = 0x4080c114080c100;
auto *arr = new long long int[len]{0};
LOGD("+++++++++++++++++++++++++++++");
LOGD("x=0x%llx y=0x%llx z=0x%llx w=0x%llx", x, y, z, w);
char *a = (char *) arr;
asm volatile(
"STP %x[x], %x[y], [%x[a], #8]!\n"
"STP %x[z], %x[w], [%x[a], #8]!\n"
:[a] "+r"(a),
[x] "+r"(x),
[y] "+r"(y),
[z] "+r"(z),
[w] "+r"(w)
:
: "cc", "memory");
LOGD("-----------------------------");
for (int i = 0; i < len; i++) {
LOGD("arr[%d]=0x%llx", i, arr[i]);
}
delete[] arr;
和 STP Post-index 指令的唯一区别在于地址先增加。
运行结果如下:
2023-05-02 17:24:00.376 15988-15988/com.example.myapplication D/native-armv8a: +++++++++++++++++++++++++++++
2023-05-02 17:24:00.376 15988-15988/com.example.myapplication D/native-armv8a: x=0x102030450203040 y=0x2040608a0406080 z=0x306090cf06090c0 w=0x4080c114080c100
2023-05-02 17:24:00.376 15988-15988/com.example.myapplication D/native-armv8a: -----------------------------
2023-05-02 17:24:00.376 15988-15988/com.example.myapplication D/native-armv8a: arr[0]=0x0
2023-05-02 17:24:00.376 15988-15988/com.example.myapplication D/native-armv8a: arr[1]=0x102030450203040
2023-05-02 17:24:00.376 15988-15988/com.example.myapplication D/native-armv8a: arr[2]=0x306090cf06090c0
2023-05-02 17:24:00.376 15988-15988/com.example.myapplication D/native-armv8a: arr[3]=0x4080c114080c100
STP Signed offset
下面是使用 STP Signed offset 指令的例子,去除上面 STP Pre-index 指令中的感叹号,每次都从固定的地址处存储。
long long int len = 4;
long long int x = 0x102030450203040;
long long int y = 0x2040608a0406080;
long long int z = 0x306090cf06090c0;
long long int w = 0x4080c114080c100;
auto *arr = new long long int[len]{0};
LOGD("+++++++++++++++++++++++++++++");
LOGD("x=0x%llx y=0x%llx z=0x%llx w=0x%llx", x, y, z, w);
char *a = (char *) arr;
asm volatile(
"STP %x[x], %x[y], [%x[a], #8]\n"
"STP %x[z], %x[w], [%x[a], #8]\n"
:[a] "+r"(a),
[x] "+r"(x),
[y] "+r"(y),
[z] "+r"(z),
[w] "+r"(w)
:
: "cc", "memory");
LOGD("-----------------------------");
for (int i = 0; i < len; i++) {
LOGD("arr[%d]=0x%llx", i, arr[i]);
}
delete[] arr;
运行结果如下:
2023-05-02 17:24:49.807 16666-16666/com.example.myapplication D/native-armv8a: +++++++++++++++++++++++++++++
2023-05-02 17:24:49.807 16666-16666/com.example.myapplication D/native-armv8a: x=0x102030450203040 y=0x2040608a0406080 z=0x306090cf06090c0 w=0x4080c114080c100
2023-05-02 17:24:49.807 16666-16666/com.example.myapplication D/native-armv8a: -----------------------------
2023-05-02 17:24:49.807 16666-16666/com.example.myapplication D/native-armv8a: arr[0]=0x0
2023-05-02 17:24:49.807 16666-16666/com.example.myapplication D/native-armv8a: arr[1]=0x306090cf06090c0
2023-05-02 17:24:49.807 16666-16666/com.example.myapplication D/native-armv8a: arr[2]=0x4080c114080c100
2023-05-02 17:24:49.807 16666-16666/com.example.myapplication D/native-armv8a: arr[3]=0x0
三、非特权访问指令
A64 LDTR 和 STTR 指令执行非特权加载或存储:
- 在 EL0(普通用户应用程序)、EL2(Hypervisor) 或 EL3(底层固件,包括安全监视器),它们的行为与正常的加载或存储相同。
- 当在 EL1(操作系统内核通常被描述为具有特权) 执行时,它们的行为就好像它们是在特权级别 EL0 上执行的一样。这些指令等同于 A32 LDRT 和 STRT 指令。
3.1 LDTR
加载寄存器(非特权)指令从内存中加载一个字或双字,并将其写入寄存器。用于加载的地址是根据基址寄存器和立即偏移量计算得出的。
如果 PSTATE.UAO 的 Effective 值为 0 并且满足以下任一条件,则指令进行的内存访问就像指令在 EL0 处执行一样:
- 指令在 EL1 处执行。
- 当 HCR_EL2.{E2H, TGE} 的 Effective 值为 {1, 1} 时,指令在 EL2 处执行。
否则,内存访问将根据执行指令的异常级别确定的限制进行操作。
32-bit (size == 10)
LDTR <Wt>, [<Xn|SP>{, #<simm>}]
64-bit (size == 11)
LDTR <Xt>, [<Xn|SP>{, #<simm>}]
<Wt>
是要传输的通用寄存器的 32 位名称,在“Rt”字段中编码。
<Xt>
是要传输的通用寄存器的 64 位名称,在“Rt”字段中编码。
<Xn|SP>
是通用基址寄存器或堆栈指针的 64 位名称,在“Rn”字段中编码。
<simm>
是可选的有符号立即字节偏移量,范围为 -256 到 255,默认为 0 并在“imm9”字段中编码。
下面是使用 LDTR 指令的例子。
long long int len = 4;
long long int x = 0;
long long int y = 0;
auto *arr = new long long int[len]{0};
LOGD("+++++++++++++++++++++++++++++");
for (int i = 0; i < len; i++) {
arr[i] = 0x0102030405060708 * (i + 1);
LOGD("arr[%d]=0x%llx", i, arr[i]);
}
char *a = (char *) arr;
asm volatile(
"LDTR %x[x], [%x[a], #8]\n"
"LDTR %x[y], [%x[a], #8]\n"
:[a] "+r"(a),
[x] "+r"(x),
[y] "+r"(y)
:
: "cc", "memory");
LOGD("-----------------------------");
LOGD("x=0x%llx y=0x%llx", x, y);
delete[] arr;
LDTR 每次从 a + 8 处读取数据加载到想要的寄存器。
运行结果如下:
2023-05-03 06:41:19.391 2097-2097/com.example.myapplication D/native-armv8a: +++++++++++++++++++++++++++++
2023-05-03 06:41:19.391 2097-2097/com.example.myapplication D/native-armv8a: arr[0]=0x102030405060708
2023-05-03 06:41:19.391 2097-2097/com.example.myapplication D/native-armv8a: arr[1]=0x20406080a0c0e10
2023-05-03 06:41:19.391 2097-2097/com.example.myapplication D/native-armv8a: arr[2]=0x306090c0f121518
2023-05-03 06:41:19.391 2097-2097/com.example.myapplication D/native-armv8a: arr[3]=0x4080c1014181c20
2023-05-03 06:41:19.391 2097-2097/com.example.myapplication D/native-armv8a: -----------------------------
2023-05-03 06:41:19.391 2097-2097/com.example.myapplication D/native-armv8a: x=0x20406080a0c0e10 y=0x20406080a0c0e10
3.2 LDTRB
加载寄存器字节(非特权)指令从内存中加载一个字节,对其进行零扩展,并将结果写入寄存器。用于加载的地址是根据基址寄存器和立即偏移量计算得出的。
如果 PSTATE.UAO 的 Effective 值为 0 并且满足以下任一条件,则指令进行的内存访问就像指令在 EL0 处执行一样:
- 指令在 EL1 处执行。
- 当 HCR_EL2.{E2H, TGE} 的 Effective 值为 {1, 1} 时,指令在 EL2 处执行。
否则,内存访问将根据执行指令的异常级别确定的限制进行操作。
LDTRB <Wt>, [<Xn|SP>{, #<simm>}]
<Wt>
是要传输的通用寄存器的 32 位名称,在“Rt”字段中编码。
<Xn|SP>
是通用基址寄存器或堆栈指针的 64 位名称,在“Rn”字段中编码。
<simm>
是可选的有符号立即字节偏移量,范围为 -256 到 255,默认为 0 并在“imm9”字段中编码。
下面是使用 LDTRB 指令的例子。
long long int len = 2;
long long int x = 0;
long long int y = 0;
auto *arr = new long long int[len]{0};
LOGD("+++++++++++++++++++++++++++++");
for (int i = 0; i < len; i++) {
arr[i] = 0x0102030450607080 * (i + 1);
LOGD("arr[%d]=0x%llx", i, arr[i]);
}
char *a = (char *) arr;
asm volatile(
"LDTRB %w[x], [%x[a], #0]\n"
"LDTRB %w[y], [%x[a], #0]\n"
:[a] "+r"(a),
[x] "+r"(x),
[y] "+r"(y)
:
: "cc", "memory");
LOGD("-----------------------------");
LOGD("x=0x%llx y=0x%llx", x, y);
delete[] arr;
LDTRB 的目的寄存器是 w 寄存器,另外将对应字节进行 0 扩展!
运行结果如下:
2023-05-03 06:51:13.186 7353-7353/com.example.myapplication D/native-armv8a: +++++++++++++++++++++++++++++
2023-05-03 06:51:13.186 7353-7353/com.example.myapplication D/native-armv8a: arr[0]=0x102030450607080
2023-05-03 06:51:13.186 7353-7353/com.example.myapplication D/native-armv8a: arr[1]=0x2040608a0c0e100
2023-05-03 06:51:13.186 7353-7353/com.example.myapplication D/native-armv8a: -----------------------------
2023-05-03 06:51:13.186 7353-7353/com.example.myapplication D/native-armv8a: x=0x80 y=0x80
3.3 LDTRH
加载寄存器半字(非特权)指令从内存中加载一个半字,对其进行零扩展,并将结果写入寄存器。用于加载的地址是根据基址寄存器和立即偏移量计算得出的。
如果 PSTATE.UAO 的 Effective 值为 0 并且满足以下任一条件,则指令进行的内存访问就像指令在 EL0 处执行一样:
- 指令在 EL1 处执行。
-当 HCR_EL2.{E2H, TGE} 的 Effective 值为 {1, 1} 时,指令在 EL2 处执行。
否则,内存访问将根据执行指令的异常级别确定的限制进行操作。
LDTRH <Wt>, [<Xn|SP>{, #<simm>}]
<Wt>
是要传输的通用寄存器的 32 位名称,在“Rt”字段中编码。
<Xn|SP>
是通用基址寄存器或堆栈指针的 64 位名称,在“Rn”字段中编码。
<simm>
是可选的有符号立即字节偏移量,范围为 -256 到 255,默认为 0 并在“imm9”字段中编码。
下面是使用 LDTRH 指令的例子。
long long int len = 2;
long long int x = 0;
long long int y = 0;
auto *arr = new long long int[len]{0};
LOGD("+++++++++++++++++++++++++++++");
for (int i = 0; i < len; i++) {
arr[i] = 0x0102030450608070 * (i + 1);
LOGD("arr[%d]=0x%llx", i, arr[i]);
}
char *a = (char *) arr;
asm volatile(
"LDTRH %w[x], [%x[a], #0]\n"
"LDTRH %w[y], [%x[a], #0]\n"
:[a] "+r"(a),
[x] "+r"(x),
[y] "+r"(y)
:
: "cc", "memory");
LOGD("-----------------------------");
LOGD("x=0x%llx y=0x%llx", x, y);
delete[] arr;
LDTRB 的目的寄存器是 w 寄存器,另外将对应半字进行 0 扩展!
运行结果如下:
2023-05-03 06:58:49.589 10072-10072/com.example.myapplication D/native-armv8a: +++++++++++++++++++++++++++++
2023-05-03 06:58:49.589 10072-10072/com.example.myapplication D/native-armv8a: arr[0]=0x102030450608070
2023-05-03 06:58:49.589 10072-10072/com.example.myapplication D/native-armv8a: arr[1]=0x2040608a0c100e0
2023-05-03 06:58:49.589 10072-10072/com.example.myapplication D/native-armv8a: -----------------------------
2023-05-03 06:58:49.589 10072-10072/com.example.myapplication D/native-armv8a: x=0x8070 y=0x8070
3.4 LDTRSB
Load Register Signed Byte(非特权)指令从内存中加载一个字节,将其符号扩展为 32 位或 64 位,并将结果写入寄存器。 用于加载的地址是根据基址寄存器和立即偏移量计算得出的。
如果 PSTATE.UAO 的 Effective 值为 0 并且满足以下任一条件,则指令进行的内存访问就像指令在 EL0 处执行一样:
- 指令在 EL1 处执行。
- 当 HCR_EL2.{E2H, TGE} 的 Effective 值为 {1, 1} 时,指令在 EL2 处执行。
否则,内存访问将根据执行指令的异常级别确定的限制进行操作。
32-bit (opc == 11)
LDTRSB <Wt>, [<Xn|SP>{, #<simm>}]
64-bit (opc == 10)
LDTRSB <Xt>, [<Xn|SP>{, #<simm>}]
<Wt>
是要传输的通用寄存器的 32 位名称,在“Rt”字段中编码。
<Xt>
是要传输的通用寄存器的 64 位名称,在“Rt”字段中编码。
<Xn|SP>
是通用基址寄存器或堆栈指针的 64 位名称,在“Rn”字段中编码。
<simm>
是可选的有符号立即字节偏移量,范围为 -256 到 255,默认为 0 并在“imm9”字段中编码。
下面是使用 LDTRSB 指令的例子。
long long int len = 2;
long long int x = 0;
long long int y = 0;
auto *arr = new long long int[len]{0};
LOGD("+++++++++++++++++++++++++++++");
for (int i = 0; i < len; i++) {
arr[i] = 0x0102030450608070 * (i + 1);
LOGD("arr[%d]=0x%llx", i, arr[i]);
}
char *a = (char *) arr;
asm volatile(
"LDTRSB %x[x], [%x[a], #1]\n"
"LDTRSB %x[y], [%x[a], #1]\n"
:[a] "+r"(a),
[x] "+r"(x),
[y] "+r"(y)
:
: "cc", "memory");
LOGD("-----------------------------");
LOGD("x=0x%llx y=0x%llx", x, y);
delete[] arr;
LDTRSB 加载地址的对应字节,并进行了符号位扩展!
运行结果如下:
2023-05-03 07:06:41.981 12820-12820/com.example.myapplication D/native-armv8a: +++++++++++++++++++++++++++++
2023-05-03 07:06:41.981 12820-12820/com.example.myapplication D/native-armv8a: arr[0]=0x102030450608070
2023-05-03 07:06:41.981 12820-12820/com.example.myapplication D/native-armv8a: arr[1]=0x2040608a0c100e0
2023-05-03 07:06:41.981 12820-12820/com.example.myapplication D/native-armv8a: -----------------------------
2023-05-03 07:06:41.981 12820-12820/com.example.myapplication D/native-armv8a: x=0xffffffffffffff80 y=0xffffffffffffff80
3.5 LDTRSH
加载寄存器带符号半字(非特权)指令从内存中加载半字,将其符号扩展为 32 位或 64 位,并将结果写入寄存器。用于加载的地址是根据基址寄存器和立即偏移量计算得出的。
如果 PSTATE.UAO 的 Effective 值为 0 并且满足以下任一条件,则指令进行的内存访问就像指令在 EL0 处执行一样:
- 指令在 EL1 处执行。
- 当 HCR_EL2.{E2H, TGE} 的 Effective 值为 {1, 1} 时,指令在 EL2 处执行。
否则,内存访问将根据执行指令的异常级别确定的限制进行操作。
32-bit (opc == 11)
LDTRSH <Wt>, [<Xn|SP>{, #<simm>}]
64-bit (opc == 10)
LDTRSH <Xt>, [<Xn|SP>{, #<simm>}]
<Wt>
是要传输的通用寄存器的 32 位名称,在“Rt”字段中编码。
<Xt>
是要传输的通用寄存器的 64 位名称,在“Rt”字段中编码。
<Xn|SP>
是通用基址寄存器或堆栈指针的 64 位名称,在“Rn”字段中编码。
<simm>
是可选的有符号立即字节偏移量,范围为 -256 到 255,默认为 0 并在“imm9”字段中编码。
下面是使用 LDTRSH 指令的例子。
long long int len = 2;
long long int x = 0;
long long int y = 0;
auto *arr = new long long int[len]{0};
LOGD("+++++++++++++++++++++++++++++");
for (int i = 0; i < len; i++) {
arr[i] = 0x0102030450608070 * (i + 1);
LOGD("arr[%d]=0x%llx", i, arr[i]);
}
char *a = (char *) arr;
asm volatile(
"LDTRSH %x[x], [%x[a], #0]\n"
"LDTRSH %x[y], [%x[a], #0]\n"
:[a] "+r"(a),
[x] "+r"(x),
[y] "+r"(y)
:
: "cc", "memory");
LOGD("-----------------------------");
LOGD("x=0x%llx y=0x%llx", x, y);
delete[] arr;
LDTRSH 加载地址的对应半字,并进行了符号位扩展!
运行结果如下:
2023-05-03 07:11:41.995 14111-14111/com.example.myapplication D/native-armv8a: +++++++++++++++++++++++++++++
2023-05-03 07:11:41.995 14111-14111/com.example.myapplication D/native-armv8a: arr[0]=0x102030450608070
2023-05-03 07:11:41.995 14111-14111/com.example.myapplication D/native-armv8a: arr[1]=0x2040608a0c100e0
2023-05-03 07:11:41.995 14111-14111/com.example.myapplication D/native-armv8a: -----------------------------
2023-05-03 07:11:41.995 14111-14111/com.example.myapplication D/native-armv8a: x=0xffffffffffff8070 y=0xffffffffffff8070
3.6 LDTRSW
加载寄存器有符号字(非特权)指令从内存中加载一个字,将其符号扩展为 64 位,并将结果写入寄存器。用于加载的地址是根据基址寄存器和立即偏移量计算得出的。
如果 PSTATE.UAO 的 Effective 值为 0 并且满足以下任一条件,则指令进行的内存访问就像指令在 EL0 处执行一样:
- 指令在 EL1 处执行。
- 当 HCR_EL2.{E2H, TGE} 的 Effective 值为 {1, 1} 时,指令在 EL2 处执行。
否则,内存访问将根据执行指令的异常级别确定的限制进行操作。
LDTRSW <Xt>, [<Xn|SP>{, #<simm>}]
<Xt>
是要传输的通用寄存器的 64 位名称,在“Rt”字段中编码。
<Xn|SP>
是通用基址寄存器或堆栈指针的 64 位名称,在“Rn”字段中编码。
<simm>
是可选的有符号立即字节偏移量,范围为 -256 到 255,默认为 0 并在“imm9”字段中编码。
下面是使用 LDTRSW 指令的例子。
long long int len = 2;
long long int x = 0;
long long int y = 0;
auto *arr = new long long int[len]{0};
LOGD("+++++++++++++++++++++++++++++");
for (int i = 0; i < len; i++) {
arr[i] = 0x0102030450608070 * (i + 1);
LOGD("arr[%d]=0x%llx", i, arr[i]);
}
char *a = (char *) arr;
asm volatile(
"LDTRSW %x[x], [%x[a], #8]\n"
"LDTRSW %x[y], [%x[a], #8]\n"
:[a] "+r"(a),
[x] "+r"(x),
[y] "+r"(y)
:
: "cc", "memory");
LOGD("-----------------------------");
LOGD("x=0x%llx y=0x%llx", x, y);
delete[] arr;
LDTRSW 加载地址(arr + 8)的对应字,并进行了符号位扩展!
运行结果如下:
2023-05-03 07:18:33.563 16309-16309/com.example.myapplication D/native-armv8a: +++++++++++++++++++++++++++++
2023-05-03 07:18:33.563 16309-16309/com.example.myapplication D/native-armv8a: arr[0]=0x102030450608070
2023-05-03 07:18:33.563 16309-16309/com.example.myapplication D/native-armv8a: arr[1]=0x2040608a0c100e0
2023-05-03 07:18:33.563 16309-16309/com.example.myapplication D/native-armv8a: -----------------------------
2023-05-03 07:18:33.563 16309-16309/com.example.myapplication D/native-armv8a: x=0xffffffffa0c100e0 y=0xffffffffa0c100e0
3.7 STTR
存储寄存器(非特权)指令将一个字或双字从寄存器存储到内存中。用于存储的地址是根据基址寄存器和立即偏移量计算得出的。
如果 PSTATE.UAO 的 Effective 值为 0 并且满足以下任一条件,则指令进行的内存访问就像指令在 EL0 处执行一样:
- 指令在 EL1 处执行。
- 当 HCR_EL2.{E2H, TGE} 的 Effective 值为 {1, 1} 时,指令在 EL2 处执行。
否则,内存访问将根据执行指令的异常级别确定的限制进行操作。
32-bit (size == 10)
STTR <Wt>, [<Xn|SP>{, #<simm>}]
64-bit (size == 11)
STTR <Xt>, [<Xn|SP>{, #<simm>}]
<Wt>
是要传输的通用寄存器的 32 位名称,在“Rt”字段中编码。
<Xt>
是要传输的通用寄存器的 64 位名称,在“Rt”字段中编码。
<Xn|SP>
是通用基址寄存器或堆栈指针的 64 位名称,在“Rn”字段中编码。
<simm>
是可选的有符号立即字节偏移量,范围为 -256 到 255,默认为 0 并在“imm9”字段中编码。
下面是使用 STTR 指令的例子。
long long int len = 2;
long long int x = 0x102030450608070;
long long int y = 0x2040608a0c100e0;
auto *arr = new long long int[len]{0};
LOGD("+++++++++++++++++++++++++++++");
LOGD("x=0x%llx y=0x%llx", x, y);
char *a = (char *) arr;
asm volatile(
"STTR %x[x], [%x[a], #0]\n"
"STTR %x[y], [%x[a], #8]\n"
:[a] "+r"(a),
[x] "+r"(x),
[y] "+r"(y)
:
: "cc", "memory");
LOGD("-----------------------------");
for (int i = 0; i < len; i++) {
LOGD("arr[%d]=0x%llx", i, arr[i]);
}
delete[] arr;
运行结果如下:
2023-05-03 07:26:24.480 17849-17849/com.example.myapplication D/native-armv8a: +++++++++++++++++++++++++++++
2023-05-03 07:26:24.480 17849-17849/com.example.myapplication D/native-armv8a: x=0x102030450608070 y=0x2040608a0c100e0
2023-05-03 07:26:24.480 17849-17849/com.example.myapplication D/native-armv8a: -----------------------------
2023-05-03 07:26:24.480 17849-17849/com.example.myapplication D/native-armv8a: arr[0]=0x102030450608070
2023-05-03 07:26:24.480 17849-17849/com.example.myapplication D/native-armv8a: arr[1]=0x2040608a0c100e0
3.8 STTRB
Store Register Byte(非特权)指令将一个字节从 32 位寄存器存储到内存中。用于存储的地址是根据基址寄存器和立即偏移量计算得出的。
如果 PSTATE.UAO 的 Effective 值为 0 并且满足以下任一条件,则指令进行的内存访问就像指令在 EL0 处执行一样:
- 指令在 EL1 处执行。
- 当 HCR_EL2.{E2H, TGE} 的 Effective 值为 {1, 1} 时,指令在 EL2 处执行。
否则,内存访问将根据执行指令的异常级别确定的限制进行操作。
STTRB <Wt>, [<Xn|SP>{, #<simm>}]
<Wt>
是要传输的通用寄存器的 32 位名称,在“Rt”字段中编码。
<Xn|SP>
是通用基址寄存器或堆栈指针的 64 位名称,在“Rn”字段中编码。
<simm>
是可选的有符号立即字节偏移量,范围为 -256 到 255,默认为 0 并在“imm9”字段中编码。
下面是使用 STTRB 指令的例子。
long long int len = 2;
long long int x = 0x102030450608070;
long long int y = 0x2040608a0c100e0;
auto *arr = new long long int[len]{0};
LOGD("+++++++++++++++++++++++++++++");
LOGD("x=0x%llx y=0x%llx", x, y);
for (int i = 0; i < len; i++) {
arr[i] = 0xffffffffffffffff;
LOGD("arr[%d]=0x%llx", i, arr[i]);
}
char *a = (char *) arr;
asm volatile(
"STTRB %w[x], [%x[a], #1]\n"
"STTRB %w[y], [%x[a], #9]\n"
:[a] "+r"(a),
[x] "+r"(x),
[y] "+r"(y)
:
: "cc", "memory");
LOGD("-----------------------------");
for (int i = 0; i < len; i++) {
LOGD("arr[%d]=0x%llx", i, arr[i]);
}
delete[] arr;
从结果可以看出要将 w 寄存器的最低有效字节写入相应的地址!
运行结果如下:
2023-05-03 14:42:39.016 29839-29839/com.example.myapplication D/native-armv8a: +++++++++++++++++++++++++++++
2023-05-03 14:42:39.016 29839-29839/com.example.myapplication D/native-armv8a: x=0x102030450608070 y=0x2040608a0c100e0
2023-05-03 14:42:39.016 29839-29839/com.example.myapplication D/native-armv8a: arr[0]=0xffffffffffffffff
2023-05-03 14:42:39.016 29839-29839/com.example.myapplication D/native-armv8a: arr[1]=0xffffffffffffffff
2023-05-03 14:42:39.016 29839-29839/com.example.myapplication D/native-armv8a: -----------------------------
2023-05-03 14:42:39.016 29839-29839/com.example.myapplication D/native-armv8a: arr[0]=0xffffffffffff70ff
2023-05-03 14:42:39.016 29839-29839/com.example.myapplication D/native-armv8a: arr[1]=0xffffffffffffe0ff
3.9 STTRH
存储寄存器半字(非特权)指令将半字从 32 位寄存器存储到内存。用于存储的地址是根据基址寄存器和立即偏移量计算得出的。
如果 PSTATE.UAO 的 Effective 值为 0 并且满足以下任一条件,则指令进行的内存访问就像指令在 EL0 处执行一样:
- 指令在 EL1 处执行。
- 当 HCR_EL2.{E2H, TGE} 的 Effective 值为 {1, 1} 时,指令在 EL2 处执行。
否则,内存访问将根据执行指令的异常级别确定的限制进行操作。
STTRH <Wt>, [<Xn|SP>{, #<simm>}]
<Wt>
是要传输的通用寄存器的 32 位名称,在“Rt”字段中编码。
<Xn|SP>
是通用基址寄存器或堆栈指针的 64 位名称,在“Rn”字段中编码。
<simm>
是可选的有符号立即字节偏移量,范围为 -256 到 255,默认为 0 并在“imm9”字段中编码。
下面是使用 STTRH 指令的例子。
long long int len = 2;
long long int x = 0x102030450608070;
long long int y = 0x2040608a0c100e0;
auto *arr = new long long int[len]{0};
LOGD("+++++++++++++++++++++++++++++");
LOGD("x=0x%llx y=0x%llx", x, y);
for (int i = 0; i < len; i++) {
arr[i] = 0xffffffffffffffff;
LOGD("arr[%d]=0x%llx", i, arr[i]);
}
char *a = (char *) arr;
asm volatile(
"STTRH %w[x], [%x[a], #1]\n"
"STTRH %w[y], [%x[a], #9]\n"
:[a] "+r"(a),
[x] "+r"(x),
[y] "+r"(y)
:
: "cc", "memory");
LOGD("-----------------------------");
for (int i = 0; i < len; i++) {
LOGD("arr[%d]=0x%llx", i, arr[i]);
}
delete[] arr;
从结果可以看出要将 w 寄存器的最低有效半字写入相应的地址!
运行结果如下:
2023-05-03 15:57:01.285 10565-10565/com.example.myapplication D/native-armv8a: +++++++++++++++++++++++++++++
2023-05-03 15:57:01.285 10565-10565/com.example.myapplication D/native-armv8a: x=0x102030450608070 y=0x2040608a0c100e0
2023-05-03 15:57:01.285 10565-10565/com.example.myapplication D/native-armv8a: arr[0]=0xffffffffffffffff
2023-05-03 15:57:01.285 10565-10565/com.example.myapplication D/native-armv8a: arr[1]=0xffffffffffffffff
2023-05-03 15:57:01.285 10565-10565/com.example.myapplication D/native-armv8a: -----------------------------
2023-05-03 15:57:01.285 10565-10565/com.example.myapplication D/native-armv8a: arr[0]=0xffffffffff8070ff
2023-05-03 15:57:01.285 10565-10565/com.example.myapplication D/native-armv8a: arr[1]=0xffffffffff00e0ff
四、预取内存指令
从内存中预取 (PRFM) 使代码能够向内存系统提供提示,表明来自特定地址的数据将很快被程序使用。此提示的效果是实现定义的,但通常,它会导致数据或指令被加载到其中一个缓存中。
指令语法是:
PRFM <prfop>, <addr> | label
其中 prfop 是以下选项的串联:
Type PLD 或 PST(加载或存储的预取)。
Target L1、L2 或 L3(目标缓存)。
Policy KEEP 或 STRM(保留在缓存中或流数据)。
例如,PLDL1KEEP。在 libyuv 这个库内可以看到 PRFM 指令的使用,prfm pldl1keep, [%[src_y], 448]
指示内存系统预加载 src_y + 448 地址处 Y 数据到 L1 缓存行上,448 立即数代表字节偏移量(8 的倍数)。
这些指令类似于 A32 PLD 和 PLI 指令。
PRFM(立即数)
预取内存(立即数)指令向内存系统发出信号,表明在不久的将来可能会从指定地址访问数据内存。当内存访问确实发生时,内存系统可以通过采取预期加速内存访问的操作来响应,例如将包含指定地址的缓存行预加载到一个或多个缓存中。PRFM 指令的效果是实现定义的。
PRFM (<prfop>|#<imm5>), [<Xn|SP>{, #<pimm>}]
<prfop>
是预取操作,定义为<type><target><policy>
。
<type>
是以下之一:
PLD 用于加载的预取,在Rt<4:3>
字段中编码为 0b00。
PLI 用于预加载指令,在Rt<4:3>
字段中编码为 0b01。
PST 用于存储的预取,在Rt<4:3>
字段中编码为 0b10。
<target>
是以下之一:
L1 一级缓存,在Rt<2:1>
字段中编码为 0b00。
L2 二级缓存,在Rt<2:1>
字段中编码为 0b01。
L3 三级缓存,在Rt<2:1>
字段中编码为 0b10。
<policy>
是以下之一:
KEEP 保留或临时预取,通常在缓存中分配。在Rt<0>
字段中编码为 0。
STRM 流式或非临时预取,用于仅使用一次的数据。在Rt<0>
字段中编码为 1。
<imm5>
是预取操作编码为立即数,范围为 0 到 31,编码在“Rt”字段中。此语法仅适用于无法使用 <prfop>
访问的编码。
<Xn|SP>
是通用基址寄存器或堆栈指针的 64 位名称,在“Rn”字段中编码。
<pimm>
是可选的正立即字节偏移量,在 0 到 32760 范围内是 8 的倍数,默认为 0 并在“imm12”字段中编码为 <pimm>/8
。
下面是使用 PRFM(立即数)指令的例子。
long long int len = 2;
long long int x = 0;
long long int y = 0;
auto *arr = new long long int[len]{0};
LOGD("+++++++++++++++++++++++++++++");
for (int i = 0; i < len; i++) {
arr[i] = 0x1020304050607080 * (i + 1);
LOGD("arr[%d]=0x%llx", i, arr[i]);
}
char *a = (char *) arr;
asm volatile(
"PRFM PLDL1KEEP, [%x[a], #0]\n"
"LDTR %x[x], [%x[a], #0]\n"
"LDTR %x[y], [%x[a], #8]\n"
:[a] "+r"(a),
[x] "+r"(x),
[y] "+r"(y)
:
: "cc", "memory");
LOGD("-----------------------------");
LOGD("x=0x%llx y=0x%llx", x, y);
delete[] arr;
运行结果如下:
2023-05-03 16:27:11.967 12292-12292/com.example.myapplication D/native-armv8a: +++++++++++++++++++++++++++++
2023-05-03 16:27:11.967 12292-12292/com.example.myapplication D/native-armv8a: arr[0]=0x1020304050607080
2023-05-03 16:27:11.967 12292-12292/com.example.myapplication D/native-armv8a: arr[1]=0x20406080a0c0e100
2023-05-03 16:27:11.967 12292-12292/com.example.myapplication D/native-armv8a: -----------------------------
2023-05-03 16:27:11.967 12292-12292/com.example.myapplication D/native-armv8a: x=0x1020304050607080 y=0x20406080a0c0e100
另外还有 PRFM (literal)
和 PRFM (register)
指令,只是预取地址提供的方式有所差异。
五、非临时加载存储对指令
ARMv8 中的一个新概念是非临时加载和存储。这些是执行一对寄存器值的读取或写入的 LDNP 和 STNP 指令。它们还向内存系统提示缓存对这些数据没有用。该提示不禁止内存系统活动,例如地址缓存、预加载或收集。但是,这表明缓存不太可能提高性能。一个典型的用例可能是流数据,但请注意,有效使用这些指令需要一种特定于微体系结构的方法。
非临时加载和存储放宽了内存排序要求。在上述情况下,可能会在前面的 LDR 指令之前观察到 LDNP 指令,这可能导致从 X0 中的不确定地址读取。
例如:
LDR X0, [X3]
LDNP X2, X1, [X0] // 指令执行时可能没有加载 X0
要纠正上述问题,需要一个明确的加载屏障:
LDR X0, [X3]
DMB nshld
LDNP X2, X1, [X0]
5.1 LDNP
Load Pair of Registers,带有非临时提示,根据基址寄存器值和立即偏移量计算地址,从内存中加载两个 32 位字或两个 64 位双字,并将它们写入两个寄存器。
32-bit (opc == 00)
LDNP <Wt1>, <Wt2>, [<Xn|SP>{, #<imm>}]
64-bit (opc == 10)
LDNP <Xt1>, <Xt2>, [<Xn|SP>{, #<imm>}]
<Wt1>
是要传输的第一个通用寄存器的 32 位名称,在“Rt”字段中编码。
<Wt2>
是要传输的第二个通用寄存器的 32 位名称,在“Rt2”字段中编码。
<Xt1>
是要传输的第一个通用寄存器的 64 位名称,在“Rt”字段中编码。
<Xt2>
是要传输的第二个通用寄存器的 64 位名称,在“Rt2”字段中编码。
<Xn|SP>
是通用基址寄存器或堆栈指针的 64 位名称,在“Rn”字段中编码。
<imm>
是可选的有符号立即字节偏移量。对于 32 位变体:在 -256 到 252 范围内是 4 的倍数,默认为 0 并在“imm7”字段中编码为 <imm>/4
。对于 64 位变体:是 -512 到 504 范围内的 8 的倍数,默认为 0 并在“imm7”字段中编码为 <imm>/8
。
下面是使用 LDNP 指令的例子。
long long int len = 2;
long long int x = 0;
long long int y = 0;
auto *addr = new long long int[1]{0};
auto *arr = new long long int[len]{0};
LOGD("+++++++++++++++++++++++++++++");
for (int i = 0; i < len; i++) {
arr[i] = 0x1020304050607080 * (i + 1);
LOGD("arr[%d]=0x%llx", i, arr[i]);
}
char *a = (char *) arr;
char *z = (char *) addr;
asm(
"STR %x[a], [%x[z]]\n"
"LDR X0, [%x[z]]\n"
"LDNP %x[x], %x[y], [X0, #0]\n"
:[a] "+r"(a),
[x] "+r"(x),
[y] "+r"(y),
[z] "+r"(z)
:
: "cc", "memory");
LOGD("-----------------------------");
LOGD("x=0x%llx y=0x%llx addr[0]=0x%llx", x, y, addr[0]);
delete[] addr;
delete[] arr;
例子中可能会在前面的 LDR 指令之前观察到 LDNP 指令,这可能导致从 X0 中的不确定地址读取。内联汇编去掉了 volatile 关键字,表示编译器可以优化,但目前在笔者环境运行还是正常的,没有出现加载错乱的情况发生。
运行结果如下:
2023-05-03 17:31:32.246 13498-13498/com.example.myapplication D/native-armv8a: +++++++++++++++++++++++++++++
2023-05-03 17:31:32.246 13498-13498/com.example.myapplication D/native-armv8a: arr[0]=0x1020304050607080
2023-05-03 17:31:32.246 13498-13498/com.example.myapplication D/native-armv8a: arr[1]=0x20406080a0c0e100
2023-05-03 17:31:32.246 13498-13498/com.example.myapplication D/native-armv8a: -----------------------------
2023-05-03 17:31:32.246 13498-13498/com.example.myapplication D/native-armv8a: x=0x1020304050607080 y=0x20406080a0c0e100 addr[0]=0xb400007356acf240
5.2 STNP
Store Pair of Registers,带非临时提示,根据基址寄存器值和立即偏移量计算地址,并将两个 32 位字或两个 64 位双字从两个寄存器存储到计算出的地址。
32-bit (opc == 00)
STNP <Wt1>, <Wt2>, [<Xn|SP>{, #<imm>}]
64-bit (opc == 10)
STNP <Xt1>, <Xt2>, [<Xn|SP>{, #<imm>}]
<Wt1>
是要传输的第一个通用寄存器的 32 位名称,在“Rt”字段中编码。
<Wt2>
是要传输的第二个通用寄存器的 32 位名称,在“Rt2”字段中编码。
<Xt1>
是要传输的第一个通用寄存器的 64 位名称,在“Rt”字段中编码。
<Xt2>
是要传输的第二个通用寄存器的 64 位名称,在“Rt2”字段中编码。
<Xn|SP>
是通用基址寄存器或堆栈指针的 64 位名称,在“Rn”字段中编码。
<imm>
是可选的有符号立即字节偏移量。对于 32 位变体:在 -256 到 252 范围内是 4 的倍数,默认为 0 并在“imm7”字段中编码为 <imm>/4
。对于 64 位变体:是 -512 到 504 范围内的 8 的倍数,默认为 0 并在“imm7”字段中编码为 <imm>/8
。
六、内存屏障和栅栏指令
ARMv7 和 ARMv8 都支持不同的屏障操作。
-
数据内存屏障(DMB)。这会强制所有程序顺序较早的内存访问在任何后续访问之前变得全局可见。
-
数据同步屏障(DSB)。所有挂起的加载和存储、高速缓存维护指令和所有 TLB 维护指令都在程序继续执行之前完成。DSB 的行为类似于 DMB,但具有其他属性。
-
指令同步屏障(ISB)。该指令刷新 CPU 流水线和预取缓冲区,导致 ISB 之后的指令从缓存或内存中获取(或重新获取)。
ARMv8 引入了与发布一致性模型相关联的单边栅栏。这些称为加载-获取(LDAR)和存储-发布(STLR),并且是基于地址的同步原语。这两个操作可以配对以形成完整的栅栏。这些指令仅支持基址寄存器寻址,不提供偏移量或其他类型的索引寻址。
七、同步原语
ARMv7-A 和 ARMv8-A 架构都支持独占内存访问。在 A64 中,这是加载/存储独占(LDXR/STXR)对。
LDXR 指令从内存地址加载一个值,并尝试静默地声明对该地址的独占锁。只有在成功获得并持有锁的情况下,Store-Exclusive 指令才会将新值写入该位置。 LDXR/STXR 配对用于构造标准同步原语,例如自旋锁。 提供了一组成对的 LDXRP 和 STXRP 指令,以允许代码自动更新跨越两个寄存器的位置。字节、半字、字和双字选项可用。与加载获取/存储释放配对一样,仅支持基址寄存器寻址,不支持任何偏移量。
CLREX 指令清除监视器,但与 ARMv7 不同,异常进入或返回也会清除监视器。监视器也可能被虚假清除,例如缓存逐出或其他与应用程序不直接相关的原因。软件必须避免在配对的 LDXR 和 STXR 指令之间有任何显式内存访问、系统控制寄存器更新或缓存维护指令。
还有一对专用的加载获取/存储释放指令,称为 LDAXR 和 STLXR。
参考资料
1.《ARMv8-A-Programmer-Guide》
2.《Arm® A64 Instruction Set Architecture Armv8, for Armv8-A architecture profile》