跳至主要內容

ARM Instructions

weigao大约 10 分钟

Abstract

本文章作为一个 ARM 指令的速查表使用。

MNEMONIC{S}{condition} {Rd}, Operand1, Operand2

上面就是 ARM 汇编指令的一个通用的格式说明,下面对每一个字段进行具体的说明:

  • MNEMONIC: 指令的助记符,如 ADD
  • S: 可选的扩展位,如果指令后带了这个,将根据计算结果更新 CPSR 寄存器中相应的 FLAG
  • condition: 执行条件,如果没有指定,则默认位 AL(无条件执行)
  • Operand1: 第一个操作数,可以是寄存器或者立即数
  • Operand2: 第二个操作数,可变的,可以是一个寄存器或者立即数,甚至带移位操作的寄存器

对于 Operand2 的解释和研究举例:

#123		@ - 立即数
Rx			@ - 寄存器,如 R1
Rx, ASR n	 @ - 对寄存器中的值进行算术右移 n 位后的值
Rx RRX		@ - 对寄存器中的值进行带扩展的循环右移 1 位后的值

Instruction

InstructionExampleRemark
SUB不进位的减法

sub

减法指令,并且是不进位的减法。

b

(branch)跳转到某地址(无返回), 不会改变 lr (x30) 寄存器的值;一般是本方法内的跳转,如 while 循环,if else 等 ,如:

b LBB0_1      ; 直接跳转到标签 ‘LLB0_1’ 处开始执行

b 指令的一些变体[1]

bl: 跳转到标号出执行

b.le :判断上面cmp的值是小于等于 执行标号,否则直接往下走

b.ge 大于等于 执行地址 否则往下

b.lt 判断上面camp的值是 小于 执行后面的地址中的方法 否则直接往下走

b.gt 大于 执行地址 否则往下

b.eq 等于 执行地址 否则往下

b.hi 比较结果是无符号大于,执行地址中的方法,否则不跳转

b.hs 指令是判断是否无符号小于

b.ls 指令是判断是否无符号大于

b.lo 指令是判断是否无符号大于等于

我们总结了一些常见的跳转指令的集合,如下所示:

[
 'b.pl', 'b.ge', 'b.ls', 'b.vs', 'tbnz', 'b.gt', 
 'b', 'cbnz', 'svc', 'b.mi', 'b.lo', 'tbz', 
 'b.ne', 'b.hi', 'br', 'b.le', 'b.eq', 'ret', 
 'bl', 'b.lt', 'blr', 'b.hs', 'cbz'
]

tst

把一个寄存器的内容和另一个寄存器的内容进行按位与操作,并根据结果更新 CPSR 中条件标志位的值,当前运算结果为 1, 则 Z=0, 反之 Z=1.

fcvtz

浮点数转换为定点数。

cbz

和 0 比较(Compare),如果结果为零(Zero)就转移(只能跳到后面的指令);

CBZ Rn, label

Rn: is the register holding the operand.

label: is the branch destination.

同样的,还有不为 0 的时候跳转:

CBNZ Rn, label

tbnz

TBNZ X1, #3, label

上述汇编的含义为,如果 x1 寄存器的第 3 位不为 0, 则跳转到 label.

还有用法如下:

tbnz w16, #0, #+0xc (addr 0x1baecc)

按照上述的例子可以推断,判断 w16 的第 0 位是否为 0, 如果不为 0, 则跳转到上述地址。

sxtw

sxtw 指令的使用方法如下:

sxtw x7, w6

其含义为将 w6 进行符号位扩展,并传给 x7; w6x7 的低 32 位.

这个博客上面展示了一个 sxtw 导致的惨案:一个include引起的惨案open in new window

内存读写

ARM 使用加载存储模型进行内存访问,这意味着只有加载/存储(LDR 和 STR)指令才能访问内存。在 x86 上,大多数指令都可以直接对内存中的数据进行操作,而在ARM上,必须先将内存中的数据从内存移到寄存器中,然后再进行操作。这意味着递增ARM上特定内存地址上的 32 位值将需要三种类型的指令(加载,递增和存储),以便首先将特定地址上的值加载到寄存器中,在寄存器中递增值,以及将其从寄存器存储回存储器[2]

ldr

加载一个寄存器:

  • 32 位常量
  • 地址

用于从存储器中将一个 32 位的字数据传送到目的寄存器中。

  • 将寄存器 x1 的值作为地址,取该内存地址的值放入寄存器 x0 中:x0 <- [x1]

    ldr x0, [x1]
    
  • 将栈内存 [sp + 0x8] 处的值读取到 w8 寄存器中

    ldr w8, [sp, #0x8]
    
  • 将寄存器 x1 的值加上 4 作为内存地址, 取该内存地址的值放入寄存器 x0 中, 然后将寄存器 x1 的值加上 4 放入寄存器 x1 中: x0 <- [x1 + 4]; x1 <- x1 + 4

    ldr x0, [x1, #4]!
    
  • 将寄存器 x1 的值作为内存地址,取内该存地址的值放入寄存器 x0 中, 再将寄存器 x1 的值加上 4 放入寄存器 x1 中

    ldr x0, [x1], #4
    
  • 将寄存器 x1 和寄存器 x2 的值相加作为地址,取该内存地址的值放入寄存器 x0 中

    ldr x0, [x1, x2]
    

ldur

ldr 一样,只不过,ldur 后面的立即数是负数。

ldur w16, [x5, #-8]

ldp

举例来说:

ldp	x20, x19, [sp, #0x150] 

简单可以理解为将栈弹出到 x20, x19 中。

ldrb

和下文中的 strb 的含义一样,将内存中的值读入寄存器中,并且只读取一个字节,也就是说把取到的数据放在目的寄存器的低 8 位,然后将高 24 位填充位 0。

ldrb w2, [x5, x2]

读取 x5 + x2 内存的值并且存储其低 8 位到 w2 中。

关于其硬件原理的介绍,也可以参考这篇博客[3]ARM的STRB和LDRB指令分析open in new window

ldrh

ldrhldrb 一样,不同之处在于 ldrh 会读入半个字长,就是 4 位。

ldrh w2, [x5, x2, lsl #1]

x5 + (x2 << 1) 的地址对应的值放入寄存器 w2 中,注意只放入读取到的值的最低 4 位,剩余的高 28 位填 0.

ldrsw

在 ARM 架构中,LDRSW 指令是用于从内存中读取一个 32 位带符号整数到寄存器的指令。LDRSW 的全称是“Load Register Signed Word”,其中的 S 表示的是 Signed,即有符号类型。

具体来说,LDRSW 指令的语法如下所示:

LDRSW Xt, [Xn{,#0|,#Imm}] 

其中:

  • Xt:目标寄存器,用于存储从内存中读取的带符号整数。
  • Xn:基地址寄存器,用于存储待读取数据的内存地址。
  • #Imm:偏移量,用于计算实际要读取的内存地址,可以是 1~4 字节的立即数,根据指令变体的不同,也可能存在其他可选的偏移量格式。

执行 LDRSW 指令时,它会从指定的内存地址中取得一个 32 位带符号整数,将其符号位扩展(sign extension)至 64 位,并将结果存储到目标寄存器中。需要注意的是,LDRSW 指令只能读取 32 位的有符号整数,如果需要读取其它类型的数据,则需要使用其他类型的 Load 指令。

总之,LDRSW 指令是 ARM 架构中的一种用于从内存中读取带符号整数的指令,可以广泛应用于各种需要使用 32 位有符号整数的场景中。

adrp

adrp x23, #-0x3ed000 (addr -0x234000)

adrp 一般用于获得地址,就我个人的理解而言,adrp 将当前 PC 所在的页的基地址计算得到并存储到寄存器中,后续根据这个寄存器中的基地址进行偏移运算。

引用一个博客中的一段描述[4]

adrp指令根据 PC 的偏移地址计算目标页地址。首先 adrp 将一个 21 位有符号立即数左移 12 位,得到一个 33 位的有符号数(最高位为符号位),接着将 PC地址的低 12 位清零,这样就得到了当前 PC 地址所在页的地址,然后将当前 PC 地址所在页的地址加上 33 位的有符号数,就得到了目标页地址,最后将目标页地址写入通用寄存器。此处页大小为 4KB,只是为了得到更大的地址范围,和虚拟内存的页大小没有关系。通过 adrp 指令,可以获取当前 PC 地址 ±4GB范围内的地址。通常的使用场景是先通过 adrp 获取一个基地址,然后再通过基地址的偏移地址获取具体变量的地址。

从上面的描述中我们可以看出,adrp 的结果是与当前的 PC 有关的,通过当前 PC 地址的偏移地址计算目标地址,因此属于位置无关码;在示例中括号也给出了最终的偏移地址。

stp

入栈指令,store pair

str

(store register) 将寄存器中的值写入到内存中,如:

str w9, [sp, #0x8] 

将寄存器 w9 中的值保存到栈内存 [sp + 0x8] 处。

strb

(store register byte) 将寄存器中的值写入到内存中(只存储一个字节),如:

strb w8, [sp, #7] 

将寄存器 w8 中的低 1 字节的值保存到栈内存 [sp + 7]

stlxr

在 ARM 架构中,STLXR 指令是原子性的存储、条件执行和即时跳转指令。该指令用于在多核创建共享内存并发访问的场景中,对数据进行原子性操作,以保证数据的一致性和正确性。

具体来说,STLXR 指令的含义如下:

  • STLXR: Store Exclusive Register
  • Rd: 目标寄存器,用于存储“存储操作”是否成功,取值为 0 或者 1。
  • Rt: 源寄存器,其中存储“写入数据”。
  • Rn: 目标地址寄存器,其中存储需要写入数据的内存地址。

当执行 STLXR 指令时,它会将目标存储地址(Rn)处的数据与当前处理器正在执行的 CPU 的标识进行比较。如果这个位置的值与标识符相同,则将源寄存器(Rt)中的数据写入该位置,并将目标寄存器(Rd)设置为 1,表示存储成功;否则,将目标寄存器设置为 0,表示存储失败。

可以看出,STLXR 指令实现了一种快速锁定和释放内存地址的机制,使得在多核场景中,多个 CPU 可以同时读取和修改共享内存的数据,而不会出现资源竞争的情况。

位操作

ubfx

举例说明:

ubfx	x10, x3, #3, #29

含义为从 x3 寄存器的第 3 位开始,提取 29 位到 x10 寄存器中。剩余高位用 0 填充,即 无符号位域提取指令。

UBFX 指令一般有两种用法:

UBFX Wd, Wn, #lsb, #width ; 32-bit
UBFX Xd, Xn, #lsb, #width ; 64-bit

and

AND 为按位与操作。

我们结合一个 AND 指令的指令编码来分析一下 AND 指令中的细节。

指令如下:

d2ffffe9 	mov	x9, #-281474976710656

二进制编码如下:

1011 0010 0100 1111 1111 1011 1110 1001

结合 arm v8 手册,我们 @todo,以后研究该命令。

lsl

lsl 为逻辑左移指令。

lsl	w9, w11, w9

左移指令分两种,可以给定寄存器或者立即数进行移位:

LSL <Wd>, <Wn>, #<shift> ; 32-bit
LSL <Xd>, <Xn>, #<shift> ; 64-bit

or

LSL <Wd>, <Wn>, <Wm> ; 32-bit
LSL <Xd>, <Xn>, <Xm> ; 64-bit

lsr

lsr 为右移指令,用法和 lsl 相似。


  1. https://juejin.cn/post/6978137866152968222open in new window ↩︎

  2. https://azeria-labs.com/memory-instructions-load-and-store-part-4/open in new window ↩︎

  3. ARM的STRB和LDRB指令分析open in new window ↩︎

  4. https://blog.csdn.net/u011037593/article/details/121877496open in new window ↩︎