ARMv7和ARMv8中关于内存访问的汇编指令总结对比
时间:2023-07-29 10:07:21
ARMv7和ARMv8中关于内存访问的汇编指令总结对比
文章目录
- ARMv7和ARMv8中关于内存访问的汇编指令总结对比
- 前言
- ARMv7下内存访问指令
-
- 寻址模式
- 多加载/存储
- ARMv8下内存访问指令
-
- 加载和存储指令格式
- 浮点和 NEON 加载和储存标量
- 指定加载或存储指令的地址
-
- 偏移模式
- 索引模式
- 访问多个内存位置
前言
ARM计算机精简指令集处理器 (Reduced Instruction Set Computer,RISC) 复杂的指令集计算机(Complex Instruction Set Computer,CISC)例如,处理器X86.指令集丰富,可以用单个指令执行复杂操作。将机器指令解码为内部操作序列(微码,microcode )。相比之下,RISC架构具有较少数量的更通用的指令,这些指令可使用更少的晶体管执行,从而节省生产成本并且更节能。与其他RISC架构一样,ARM内核有大量的通用寄存器,单个周期有许多指令(cycle)内执行。它有一个简单的搜索模式,可以从寄存器内容和指令字段中确定所有加载/存储的地址。
ARM 指令集通常被认为是简单、合乎逻辑和高效的。然而,它不能直接在内存上执行数据处理操作:例如,为了在内存位置增加值,必须首先加载值 ARM 寄存器在寄存器中增加,需要第三条指令将更新值写回内存。
与 x86(但与 68K 不同)一样,ARM 指令通常采用两个或三个操作数格式。在大多数情况下,第一个操作数指定结果的目标(多加载/多存储例外)。相比之下,68K 以目标为最后一个操作数。 ARM 该指令通常对哪些寄存器可用作操作数量没有限制。以下是不同汇编语言的风格:
#将寄存器值自增100指令 x86: add eax, #100 68K: ADD #100, D0 ARM: add r0, r0, 100 #将r保存在1中的地址上的值加载到r0,r11可视为寄存器指针 x86: mov eax, DWORD PTR [ebx] 68K: MOVE.L (A0), D0 ARM: ldr r0, [r1]
ARMv7和ARMv8都是 加载/存储 这意味着它们不能直接处理内存中的数据, 只有加载和存储指令才能访问内存。因此,通用寄存器需要通过(GPR),将内存中的数据加载到寄存器中,然后存储回内存中。此外,ARMv8具有A64模式和A32模式,即64bits模式和32bits模式,32bits指令集和模式ARMv7指令集几乎一致。所以在学习ARMv同时,7指令集也相当于学习ARMv8的A32模式指令集。
作者最近在做ARMv8下内存复制实验需要进行A64模式和A在32模式下进行,所以对ARMv7和ARMv总结和比较8下内存访问汇编指令。
文章开始前有必要ARMv7和ARMv简要说明寄存器的位宽。一般情况下,ARMv8中用X表示A64下的64bits寄存器,W则是32bits另外还有寄存器 B、H、S、D 以及 Q 寄存器的位宽如下图所示。ARMv7中通用寄存器用R表示ARMv8下W是一致的。
ARMv7下内存访问指令
ARM 内核只在寄存器上执行算术逻辑单元 (ALU) 操作。唯一支持的内存操作是加载(LOAD,LDR,将数据从内存读取到寄存器或存储(STORE,STR,将数据从寄存器写入内存)。
用户可以在指令中添加它 B 表示 Byte(8位)、H 表示Halfword ,半字(16位),或 D表示doubleword ,双字(64 位),指定加载或储存传输的大小,如 LDRB。仅对于加载LDR,还可以使用额外的 S 指示符号字节或半字符(SB 表示符号字节,SH 表示符号半字)。附加类型可以是:
- B – unsigned Byte. (Zero extend to 32 bits on loads.)
- SB – signed Byte. (Sign extend to 32 bits.)
- H – unsigned Halfword. (Zero extend to 32 bits on loads.)
- SH – signed Halfword. (Sign extend to 32 bits.)
这种方法可能很有用,因为如果是这样的话 8 位或 16 位加载到 32 在寄存器中,必须决定如何处理寄存器中的符号位。无符号数字为零扩展(即寄存器最重要的16或24位设置为零),但对于符号数字,必须将符号位(字节位[7]或半字位[15])复制到寄存器的前16位(或24位)。
寻址模式
LDR和STR有多种寻址模式,如下示例:
- (1) LDR R0, [R1] ;保存加载地址R1当中
- (2) LDR R0, [R1, R2] ;加载地址为R1 R2
- (3) LDR R0, [R1, R2, LSL #2] ;加载的地址R1 (R2*4)逻辑左移1位相当于乘以2
- (4) LDR R0, [R1, #32]! ;先更新 R1为R1 然后从地址开始R1(R1:=R1 32)上加载
- (5) LDR R0, [R1], #32 ;先从R加载1个,然后更新 R1为R1 32
方法(1)寄存器寻址(Register addressing)方式,从R1中保存的数据是地址,地址上的数据被加载到R0。
方法(2)和(3)(Pre-indexed addressing),基址寄存器的偏移量在内存访问前添加,基本形式为:LDR Rd, [Rn, Op2]
,偏移可以是正数或负数,也可以是立即数或另一个应用可选移位的寄存器。
方法(4)前下标寻址 写回(Pre-indexed with write-back),它的指令最后使用了一个感叹号!
,可理解为优先R1:=R1 32操作,即先更新 R1为R1 32,然后再将R加载1所指向的数据R0当中。
方法(5)后下标 写回(Post-index with write-back),偏移量超过方括号,表示先加载数据,即先从R加载1个,然后更新 R1为R1 32。
多加载/存储
多加载和储存(LDM/STM)使连续的字(word,4字节)可以从存储器中读取或写入。这些对堆栈操作和内存复制非常有用。word可以这样操作,必须使用和使用word对齐地址。以下说明:
LDMIA R10!, { R0-R3, R12 }
操作数是基址寄存器(可选) !表示基址寄存器的回写,即更新基址寄存器),大括号之间有一个寄存器列表。寄存器列表用逗号分隔,连接字符-
用于指示范围。寄存器的加载或存储顺序与列表中指定的顺序无关。相反,操作是固定的,最低编号的寄存器总是映射到最低地址。
大括号中有上述示例指令R0,R1,R2,R3和R基址寄存器为125个目标寄存器R指令如下:
[R10] -> R0 [R10 4] -> R1 [R10 8] -> R2 [R10 12] -> R3 [R10 16] -> R12
由于加了!
,操作完成后需要正确R10进行更新,将R10更新为R10 20。
该指令还必须指定如何从基址寄存器中指定 Rd 中继续操作。有四种情况:
- IA/IB(之后/之前增加,Increment After/Before )
- DA/DB(之后/之前递减,Decrement After/Before )
后缀也可以用(FD,FA,ED和EA),后缀是从栈stack从角度工作,ARM7支持四种堆栈模式:满递减(FD)、满递增(FA)、空递减(ED)、空递增(EA),并指定栈指针是指向栈的完整性(Full)顶部还是空(Empty)的顶部,而且栈在内存中上升(Ascend)还是下降(Descend)。
- FD:堆栈地址从上到下递减,指针指向最后一个堆栈元素。
- FA:堆栈地址自下而上增加,指针指向最后一个堆栈元素。
- ED:栈地址从上往下递减,且指针指向下一个可用空位。
- EA:堆栈地址从下网上递增,且指针指向下一个可用空位。
按照惯例,只有(FD) 选项是用于基于 ARM 处理器系统中的栈。这意味着栈指针指向栈内存中最后填充的位置,并且将随着压入到栈的每个新数据项而递减。
STMFD sp!, {r0-r5} ; 将r0至r5压入FD栈,并且更新指针
LDMFD sp!, {r0-r5} ; 将r0至r5弹出FD栈,并且更新指针
下图展示了将R1和R2压入一个FD栈中,在执行 STMFD (PUSH) 指令之前,栈指针SP指向栈中最后一个占用的word。指令完成后,栈指针SP递减 8(两个word),两个寄存器的内容已写入内存,由于内存地址是向上递增,并且按照编号最低的寄存器将写入最低的内存地址的规则,所以R2在R1的上方。
ARMv8下的内存访问指令
与所有以前的 ARM 处理器一样,ARMv8 架构是加载/存储架构。程序必须指定地址、要传输的数据的大小以及源或目标寄存器。ARMv8还有其他加载和存储指令,提供了更多选项,例如非临时加载/存储(non-temporal Load/Store )、加载/存储独占项(Load/Store exclusives),和获取/释放(Acquire/Release)。
加载和存储指令格式
ARMv8中的加载和存储指令和ARMv7中的相同:
LDR Rt,
STR Rn,
当加载整数寄存器时,可选择数据大小进行加载,例如,要加载小于指定寄存器值的大小,可将以下后缀之一追加到 LDR 指令中:
- LDRB (8-bit, zero extended).
- LDRSB (8-bit, sign extended).
- LDRH (16-bit, zero extended).
- LDRSH (16-bit, sign extended).
- LDRSW (32-bit, sign extended).
还有未缩放的偏移形式,例如LDUR,程序员通常不需要显式使用 LDUR形式,因为大多数汇编程序可以根据使用的偏移量选择适当的版本。
用户不需要指定对 X 寄存器的零扩展加载,因为写入 W 寄存器实际上为零会扩展到整个寄存器宽度。
下图为指令LDRSB W4,
的示意图,在ARMv8中寄存器为64位,W寄存器为A32模式下的描述,所以只用上了前32位。SB为有符号8位扩展,在addr地址上的数据为0x8A,将其加载到W4中,并进行有符号扩展,最后得到的结果为:
下图为指令LDRSB X4,
的示意图,在ARMv8中寄存器为64位,X寄存器为A64模式下的描述,所以为64位。SB为有符号8位扩展,在addr地址上的数据为0x8A,将其加载到X4中,并进行有符号扩展,最后得到的结果为:
下图为指令LDRB W4,
的示意图,SB为零扩展,在addr地址上的数据为0x8A,将其加载到W4中,并进行零扩展,最后得到的结果为:
此外,如果要存储的数据大小可能小于寄存器,可以通过向 STR 添加 B 或 H 后缀来指定此项。
浮点和 NEON 标量加载和存储
加载和存储指令还可以访问浮点/NEON 寄存器。大小仅由正在加载或存储的寄存器确定,寄存器可以是任何 B、H、S、D 或 Q 寄存器。并且浮点和标量 NEON 的加载和存储,使用与整数寄存器加载和存储相同的寻址模式。
需要注意的是,符号扩展(sign-extension)并不支持加载到 FP/SIMD 寄存器中。此类加载的地址仍使用通用寄存器指定。比如:
LDR D0, [X0, X1]
D寄存器也为64位(doubleword ,8字节),上述指令会将地址X0+X1上的数据加载到D0寄存器当中。
指定加载或存储指令的地址
A64 可用的寻址模式与 A32 和 T32 中的寻址模式类似。有一些额外的限制以及一些新功能,但对于熟悉A32或T32的人来说,A64可用的寻址模式并不陌生。 在 A64 中,地址操作数的基本寄存器必须始终是 X 寄存器。但是,有几条指令支持零扩展或符号扩展,因此可以提供 32 位偏移量作为 W 寄存器。
偏移模式
偏移寻址模式将立即数或可选的修改的寄存器值添加到 64 位基址寄存器以生成地址。
LDR X0, [X1] ;Load from the address in X1
LDR X0, [X1, #8] ;Load from address X1 + 8
LDR X0, [X1, X2] ;Load from address X1 + X2
LDR X0, [X1, X2, LSL, #3] ;Load from address X1 + (X2 << 3)
LDR X0, [X1, W2, SXTW] ;Load from address X1 + sign_extend(W2)
LDR X0, [X1, W2, SXTW, #3] ;Load from address X1 + (sign_extend(W2) << 3)
通常,在指定移位或扩展选项时,移位量可以是访问大小的 0(默认值)或 log2(以字节为单位)(以便 Rn << 将 Rn 乘以访问大小)。
以下是一段C程序的示例:
// A C example showing accesses that a compiler is likely to generate.
void example_dup(int32_t a[], int32_t length)
{
int32_t first = a[0]; // LDR W3, [X0]
for (int32_t i = 1; i < length; i++)
{
a[i] = first; // STR W3, [X0, W2, SXTW, #2]
}
}
索引模式
索引模式类似于偏移模式,但它们会更新基址寄存器。语法与 A32 和 T32 中的语法相同,但操作集的限制性更强。通常,只能为索引模式提供立即数偏移量。同ARMv7一样,有两种变体:前索引模式(在访问内存之前应用偏移量)和后索引模式(在访问内存后应用偏移量):
LDR X0, [X1, #8]! ;Pre-index: Update X1 first (to X1 + #8), then load from the new address
LDR X0, [X1], #8 ;Post-index: Load from the unmodified address in X1 first, then update X1 (to X1 + #8)
STP X0, X1, [SP, #-16]! ;Push X0 and X1 to the stack.
LDP X0, X1, [SP], #16 ;Pop X0 and X1 off the stack
C语言示例:
// A C example showing accesses that a compiler is likely to generate.
void example_strcpy(char * dst, const char * src)
{
char c;
do {
c = *(src++); // LDRB W2, [X1], #1
*(dst++) = c; // STRB W2, [X0], #1
} while (c != '\0');
}
访问多个内存位置
A64 不包括 A32 和 T32 代码可用的多加载 (LDM) 或多存储(STM) 指令。
在 A64 代码中,有成对加载(Load Pair ,LDP) 和存储对(Store Pair,STP)。与 A32中的LDRD 和 STRD 指令不同,LDP/STP可以读取或写入任何两个整数寄存器。数据被读取或写入到相邻的内存位置或从相邻的内存位置写入。为这些指令提供的寻址模式选项比其他内存访问指令更具限制性。
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,如图: -
指令
LDPSW X3, X4, [X0]
,将[X0]加载到X3,将[X0+4]加载到X4,并且进行有符号扩展,扩展到64bits -
指令
LDP D8, D2, [X11], #0x10
,将[X11]加载到D8,将[X11+8]加载到D2,最后将X11更新为X11+0x10 -
指令
STP X9, X8, [X4]
, 将64bits的数据从X9 存储到地址[X4],并且将X8中的64bits数据存储到[X4+8]