汇编基础介绍——ARMv8指令集(二)

发布于:2025-06-28 ⋅ 阅读:(20) ⋅ 点赞:(0)

一、A64指令集介绍

指令集是处理器体系结构设计的重点之一。ARM 公司定义和实现的指令集一直在变化和发展中。ARMv8 体系结构最大的改变是增加了一个新的 64 位的指令集,这是早前 ARM 指令集的有益补充和增强。它可以处理 64 位宽的寄存器和数据并且使用 64 位的指针来访问内存。这个新的指令集称为 A64 指令集,运行在 AArch64 状态。ARMv8 兼容旧的 32 位指令集——A32 指令集,它运行在 AArch32 状态。

A64 指令集和 A32 指令集是不兼容的,它们是两套完全不一样的指令集,它们的指令编码是不一样的。需要注意的是,A64 指令集的指令宽度是 32 位,而不是 64 位。

A64 指令集有如下特点:

  • 具有特有的指令编码格式。
  • 只能运行在 AArch64 状态。
  • 指令的宽度为 32 位。

A64 指令集可以分成如下几类:

  • 内存加载和存储指令;
  • 多字节内存加载和存储指令;
  • 算术和移位指令;
  • 移位操作指令;
  • 位操作指令;
  • 条件操作指令;
  • 跳转指令;
  • 独占访存指令;
  • 内存屏障指令;
  • 异常处理指令;
  • 系统寄存器访问指令。

二、加载与存储指令

ARMv8体系下,所有的数据处理都需要在通用寄存器中完成, 而不能直接在内存中完成。因此,首先把待处理数据从内存加载到通用寄存器,然后进行数据处理,最后把结果写入内存中。常见的内存加载指令是 LDR 指令,存储指令是 STR 指令。 LDR 和 STR 指令的基本格式如下。

LDR 目标寄存器, <存储器地址> //把存储器地址中的数据加载到目标寄存器中
STR 源寄存器, <存储器地址> //把源寄存器的数据存储到存储器中

在 A64 指令集中,加载和存储指令有多种寻址模式,如下表所示:

寻址模式

说明

基地址模式

[Xn]

基地址加偏移量模式

[Xn, #offset]

前变基模式

[Xn, #offset]!

后变基模式

[Xn], #offset

PC 相对地址模式

<label>

2.1、基于基地址的寻址模式

基地址模式首先使用寄存器的值来表示一个地址,然后把这个内存地址的内容加载到通用寄存器中。基地址加偏移量模式是指在基地址的基础上再加上偏移量,从而计算内存地址,并且把这个内存地址的值加载到通用寄存器中。偏移量可以是正数,也可以是负数。

常见的指令格式如下。

2.1.1、 基地址模式

以下指令以 Xn 寄存器中的内容作为内存地址,加载此内存地址的内容到 Xt 寄存器。

LDR Xt, [Xn]

如下图所示:

以下指令把 Xt 寄存器中的内容存储到 Xn 寄存器的内存地址中。

STR Xt, [Xn]

2.1.2、基地址加偏移量模式

以下指令把 Xn 寄存器中的内容加一个偏移量(offset 必须是 8 的倍数) ,以相加的结果作为内存地址,加载此内存地址的内容到 Xt 寄存器。

LDR Xt, [Xn, $offset]

如下图所示:

以下指令把 Xt 寄存器的值存储到以 Xn 寄存器的值加一个偏移量(offset 乘以 8)表示的内存地址中。

STR Xt, [Xn, $offset]

2.1.3、基地址扩展模式

基地址扩展模式的命令如下。

LDR <Xt>, [<Xn>, (<Xm>){, <extend> {<amount>}}]
STR <Xt>, [<Xn>, (<Xm>){, <extend> {<amount>}}]

基地址扩展模式的指令参数如下:

  • Xt:目标寄存器。
  • Xn:基地址寄存器。
  • Xm:用来表示偏移的寄存器。
  • extend:扩展/移位指示符,默认是 LSL。
  • amount:索引偏移量,当 extend 参数不是 LSL 时有效。amount 的值只能是 0 或者 3,如果是其他值,汇编器会报错。

如下代码使用了基地址加偏移量模式。

//内存地址为 X1寄存器的值,加载此内存地址的值到 X0寄存器
LDR X0, [X1]

//内存地址为 X1寄存器的值再加上偏移量(8),加载此内存地址的值到 X0寄存器
LDR X0, [X1, #8]

//内存地址为 X1寄存器的值加 X2寄存器的值,加载此内存地址的值到 X0寄存器
LDR X0, [X1, X2]

//内存地址为 X1寄存器的值加(X2寄存器的值<<3), 加载此内存地址的值到 X0寄存器
LDR X0,[X1, X2, LSL #3]

//先对 W2的值做有符号的扩展,和 X1寄存器的值相加后,将结果作为内存地址,
//加载此内存地址的值到 X0寄存器
LDR X0, [X1, W2 SXTW]

//先对 W2的值做有符号的扩展,然后左移 3位,和 X1寄存器的值相加后,将
//结果作为内存地址,加载此内存地址的值到 X0寄存器
LDR X0, [X1, W2, SXTW #3] 

2.2、变基模式

变基模式主要有如下两种。

  • 前变基(pre-index)模式:先更新偏移量地址,后访问内存地址。
  • 后变基(post-index)模式:先访问内存地址,后更新偏移量地址。

2.2.1、前变基模式

前变基模式的指令格式如下。

LDR <Xt>, [<Xn|SP>, #<simm>]!

首先,更新 Xn/SP 寄存器的值为 Xn/SP 寄存器的值加 simm。然后,以新的 Xn/SP 寄存器的值为内存地址, 加载该内存地址的值到 Xt 寄存器。如下图所示:

以下指令首先更新 Xn/SP 寄存器的值为 Xn/SP 寄存器的值加 simm,然后把 Xt 寄存器的值存储到以 Xn/SP 寄存器的新值为地址的内存单元中。

STR <Xt>, [<Xn|SP>, #<simm>]!

2.2.2、后变基模式

后变基模式的指令格式如下。

LDR <Xt>, [<Xn|SP>], #<simm>

首先以 Xn/SP 寄存器的值为内存地址,加载该内存地址的值到 Xt 寄存器, 然后更新 Xn 寄存器的值为 Xn/SP 寄存器的值加 simm,如下图所示:

从前变基模式和后变基模式的指令编码可以看到,偏移量 simm 是一个有符号的立即数,也就是说,地址偏移量可以是正数也可以是负数,取值范围为−256~255。

以下指令首先把 Xt 寄存器的值存储到以 Xn/SP 寄存器的值为地址的内存单元中,然后更新 Xn/SP 寄存器的值为 Xn/SP 寄存器的值加 simm。

STR <Xt>, [<Xn|SP>], #<simm>

如下代码使用了变基模式。

//前变基模式。先更新 X1寄存器的值为 X1寄存器的值加 8,然后以新的 X1寄存器的值
//为内存地址,加载该内存地址的值到 X0寄存器中
LDR X0, [X1, #8]!

//后变基模式。以 X1寄存器的值为内存地址,加载该内存地址的值到 X0寄存器,然后更
//新 X1寄存器的值为 X1寄存器的值加 8中
LDR X0, [X1], #8  

//把 X0 和 X1 寄存器的值压回栈中
SDP X0, X1, [SP, #-16]!

//把 X0和 X1寄存器的值弹出栈
LDP X0, X1, [SP], #16

2.3、PC相对寻址模式

汇编代码里常常会使用标签(label)来标记代码片段。LDR 指令还提供一种访问标签的地址模式,指令格式如下。

LDR <Xt>, <label>

这条指令读取 label 所在内存地址的内容到 Xt 寄存器中。但是这个 label 必须在当前 PC 地址前后 1 MB 的范围内,若超出这个范围,汇编器会报错。

如下 LDR 指令会把标签 my_data 的数据读出来。

my_data:
    .word 0x40

ldr x0, my_data

最终 X0 寄存器的值为 0x40。

假设当前 PC 值为 0x806E4,那么这条 LDR 指令读取 0x806E4 + 0x20 地址的内容到 X6 寄存器中。

#define MY_LABEL 0x20

ldr x6, MY_LABEL

2.4、LDR 伪指令

伪指令是对汇编器发出的命令,它在源程序汇编期间由汇编器处理。伪指令可以完成选择处理器、定义程序模式、定义数据、分配存储区、指示程序结束等功能。总之,伪指令可以分解为几条指令的集合。

LDR 指令既可以是在大范围内加载地址的伪指令,也可以是普通的内存访问指令。当它的第二个参数前面有“=”时,表示伪指令;否则,表示普通的内存访问指令。注意,GNU 汇编器没有对应的 STR 伪指令。

LDR 伪指令的格式如下。

//把 label标记的地址加载到 Xt寄存器
LDR Xt,=<label> 

如下代码会把 MY_LABEL 宏的值加载到 X6 寄存器中。

#define MY_LABEL 0x20

ldr x6, =MY_LABEL

如下代码定义了一个数据标签 my_data1,数据的值为 0x8。第一条 LDR 指令是伪指令,它把标签 my_data1 对应的地址加载到 X5 寄存器中。 第二条 LDR 指令是普通的内存访问指令,它以 X5 寄存器的值作为内存地址,加载这个内存地址的值到 X6 寄存器中,最终 X6 寄存器的值为 0x8。

my_data1:
    .quad 0x8

ldr x5, =my_data1
ldr x6, [x5]

2.5、不同位宽的加载与存储指令

LDR 和 STR 指令根据不同的数据位宽有多种变种,如下表所示:

指令

说明

LDR

数据加载指令

LDRSW

有符号的数据加载指令,单位为字

LDRB

数据加载指令,单位为字节

LDRSB

有符号的加载指令,单位为字节

LDRH

数据加载指令,单位为半字

LDRSH

有符号的数据加载指令,单位为半字

STRB

数据存储指令,单位为字节

STRH

数据存储指令,单位为半字

访问和存储 4 字节和 8 字节的无符号数都使用 LDR 和 STR 指令,只不过目标寄存器使用Wn 或者 Xn 寄存器。

下面以 LDRB 和 LDRSB 指令为例来说明相关指令的区别。

  • LDRB 指令加载一字节的数据,这些数据是按照无符号数来处理的。
  • LDRSB 指令加载一字节的数据,这些数据是按照有符号数来进行扩展的。
1 my_data:
2     .quad 0x8a
3
4 ldr x5, =my_data
5
6 ldrb x1, [x5]
7 ldrsb x2,[x5]

在第 1 行中,使用标签 my_data 来定义一个数据,数据的值为 0x8A。

在第 4 行中,使用 LDR 伪指令来加载标签 my_data 的内存地址。

在第 6 行中,使用 LDRB 指令来读取标签 my_data 内存地址中第 1 字节的数据,此时读到的数据为 0x8A。LDRB 指令读取的数据是按照无符号数来处理的,因此高字节部分填充为0,如下图所示。

在第 7 行中,使用 LDRSB 指令来读取标签 my_data 内存地址中第 1 字节的数据,此时读到的数据为 0xFFFFFFFFFFFFFF8A。LDRSB 指令读取的数据是按照有符号数来处理的,高字节部分会做符号扩展。数值 0x8A 的第 7 位为 1,表明这是一个有符号的 8 位数据,因此高字节部分填充为 0xFF,如下图所示。

2.6、多字节内存加载和存储指令

A32 指令集提供 LDM 和 STM 指令来实现多字节内存加载与存储, 而 A64 指令集不再提供LDM 和 STM 指令,而提供 LDP 和 STP 指令。LDP 和 STP 指令支持 3 种寻址模式。

2.6.1、基地址偏移量模式

基地址偏移量模式 LDP 指令的格式如下。

LDP <Xt1>, <Xt2>, [<Xn|SP>{, #<imm>}]

它以 Xn/SP 寄存器的值为基地址,然后读取 Xn/SP 寄存器的值 + imm 地址的值到 Xt1 寄存器,读取 Xn/SP 寄存器的值 + imm + 8 地址的值到 Xt2 寄存器中。

基地址偏移模式 STP 指令的格式如下。

STP <Xt1>, <Xt2>, [<Xn|SP>{, #<imm>}]

它以 Xn/SP 寄存器的值为基地址,然后把 Xt1 寄存器的内容存储到[Xn/SP + imm]处,把 Xt2 寄存器的内容存储到[Xn/SP + imm + 8]处。

2.6.2、前变基模式

前变基模式 LDP 指令的格式如下。

LDP <Xt1>, <Xt2>, [<Xn|SP>, #<imm>]!

它先计算 Xn 寄存器的值加 imm,并存储到 Xn 寄存器中,然后以 Xn 寄存器的最新值作为基地址。即读取 Xn 寄存器的值加 imm 地址的值到 Xt1 寄存器,读取[Xn + imm + 8]的值到 Xt2 寄存器中。Xn 寄存器可以使用 SP 寄存器。

前变基模式 STP 指令的格式如下。

STP <Xt1>, <Xt2>, [<Xn|SP>, #<imm>]!

它先计算 Xn 寄存器的值加 imm,并存储到 Xn 寄存器中,然后以 Xn 寄存器的最新值作为基地址,把 Xt1 寄存器的内容存储到 Xn 内存地址处,把 Xt2 寄存器的值存储到 Xn 寄存器的值加 8 对应的内存地址处。

2.6.3、后变基模式

后变基模式 LDP 指令的格式如下。

LDP <Xt1>, <Xt2>, [<Xn|SP>], #<imm>

它以 Xn 寄存器的值为基地址,读取[Xn + imm]的值到 Xt1 寄存器,读取[Xn + imm + 8]的值到 Xt2 寄存器中,最后更新 Xn 寄存器。Xn 寄存器可以使用 SP 寄存器。

后变基模式 STP 指令格式如下。

STP <Xt1>, <Xt2>, [<Xn|SP>], #<imm>

它以 Xn 寄存器的值为基地址,把 Xt1 寄存器的内容存储到 Xn 寄存器的值加 imm 对应的内存地址处,Xt2 寄存器的值存储到[Xn + imm + 8]处,并更新 Xn 寄存器。Xn 寄存器可以使用 SP 寄存器。

如下代码使用了前变基模式。

//以 X0 寄存器的值为内存地址,加载此内存地址的值到 X3 寄存器中,然后以 X0 寄存器的
//值加 8 作为内存地址,加载此内存地址的值到 X7 寄存器中
LDP X3, X7, [X0]

//前变基模式。先计算 X0 + 0x10,然后以 X0 寄存器的值作为内存地址,
//加载此内存地址的值到 X1 寄存器中,接着以 X0 寄存器的值加 8 作为内存地址,
//加载此内存地址的值到 X2 寄存器中
LDP X1, X2, [X0, #0x10]!


//存储 X1 寄存器的值到地址为 X4 寄存器的值的内存单元中,然后存储 X2 寄存器的值到地址为
//X4 寄存器的值加 8 的内存单元中
STP X1, X2, [X4]

需要注意偏移量(imm)使用的两个条件,否则汇编器就会报错。

  • imm 的取值范围为−512~504。
  • imm 必须为 8 的整数倍。

2.7、独占内存访问指令

ARMv8 体系结构提供独占内存访问(exclusive memory access)的指令。在 A64 指令集中,LDXR 指令尝试在内存总线中申请一个独占访问的锁,然后访问一个内存地址。STXR 指令会往刚才 LDXR 指令已经申请独占访问的内存地址中写入新内容。 LDXR 和 STXR 指令通常组合使用来完成一些同步操作。

另外,ARMv7 和 ARMv8 还提供多字节独占内存访问指令,即 LDXP 和 STXP 指令。

独占内存访问指令如下表所示。

指令

说明

LDXR

独占内存访问指令。指令的格式如下。

STXR

独占内存访问指令。指令的格式如下。

LDXP

多字节独占内存访问指令。指令的格式如下。

STXP

多字节独占内存访问指令。指令的格式如下。


网站公告

今日签到

点亮在社区的每一天
去签到