ARM常用指令(1)

加载与存储指令

基于基地址的寻址模式

  • 基地址寻址
1
ldr x0, [x1]
  • 基地址偏移量寻址
1
ldr x0, [x1, #4]
  • 基地址扩张寻址
1
2
3
4
ldr x0, [x1, x2]
ldr x0, [x1, x2, LSL #3]
ldr x0, [x1, x2, UXTW]
ldr x0, [x1, w2, UXTW #3]

UXTW表示从寄存器中提取32位数据,高位补0,无符号数。相同的extend还包括UXTB,UXTH。

SXTW表示从寄存器中提取32位数据,高位补0,有符号数。相同的extend还包括SXTB,SXTH。

变基寻址模式

  • 前变基模式
1
ldr x0, [x1, #4]!
  • 后变基模式
1
ldr x0, [x1], #4

LDR伪指令

ldr既可以是普通的加载指令,也可以在第二个参数前加上“=”后,表示大范围加载地址的伪指令。注:无str伪指令

1
ldr x0, =label // 此时x0中即为label的值

‘[]’方括号表示从括号中参数的内存地址中读取或者存储数据,‘!’感叹号表示更新存放地址的寄存器,也即写回和更新寄存器

ldr和str的变种

指令
ldr 普通数据加载指令,4字节与8字节数据,以W或者X寄存器区分
ldrsw 有符号数据加载指令,word
ldrb 数据加载指令,byte
ldrsb 有符号数据加载指令,byte
ldrh 数据加载指令,half-word
ldrsh 有符号数据加载指令,half-word
strb 数据存储指令,byte
strh 数据存储指令,halfword

多字节加载与存储指令

在A32中通常使用LDM,STM指令实现多字节内存加载和存储,但是在A64架构中取消了LDM和STM指令,取而代之的是LDP(Load Pair)和STP(Store Pair)指令。LDP和STP支持基地址偏移量寻址、前变基模式和后变基模式三种寻址方式。

  1. 基地址偏移量寻址

    1
    2
    ldp x0, x1, [x2]
    stp x0, x1, [x2]

  2. 前变基寻址

    1
    2
    ldp x0, x1, [x2, #4]!
    stp x0, x1, [x2]

  3. 后编辑寻址

    1
    2
    ldp x0, x1, [x2], #4
    stp x0, x1, [x2], #4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 以上常用指令简易测试汇编代码如下
.arch armv8-a
.text
.align 3

my_data:
.word 0x40

.equ MY_LABEL, 0x20

.global test2
test2:

// int a[10]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

// x1保存了数组首地址
mov x1, x0
// 立即数
mov x3, #1
mov w4, #1
mov w5, #4

// 1. 基地址模式
ldr x0, [x1] //x0 = 1

// 2. 基地址+偏移量模式
ldr x0, [x1, #4] // x0 = 2

// 3. 基地址扩展模式
ldr x0, [x1, x3, lsl #3] // x0 = 3
ldr x0, [x1, w5, SXTW] // x0 = 2
ldr x0, [x1, w4, SXTW #3] // x0 = 3

// 4. 变基模式
ldr x0, [x1, #4]! // 前变基模式 x0 = 2
ldr x0, [x1] // 前变基模式后x1自动加4 此时x0 = 2
ldr x0, [x1], #4 // 后变基模式 x0 = 2,
ldr x0, [x1] // x0 = 3

// 基地址寻址
ldp w6, w7, [x1, #4]!
mov w0, w6 // w0 = 6
mov w0, w7 // w0 = 7
ret

入栈与出栈

在程序中栈通常有以下用途:

  • 临时存储数据,如临时变量等
  • 传递参数。ArmV8平台,如果函数参数不超过8个,那么将使用x0~x7通用寄存器传递参数,当传递参数超过8个时,就需要用到栈了。

一般情况,栈是从高地址向低地址生长的数据结构,起始地址称为栈底,最后一个入栈元素地址称之为栈顶,指向栈顶的指针称之为栈指针,SP(Stack Point)。

A32提供了push和pop指令进行入栈出栈操作,在A64指令集中移除了push和pop,使用ldp和stp取代。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.arch armv8-a
.text
.align 3

.global test2
test2:

mov x1, x0
ldp x2, x3, [x1] // x2 = 1, x3 = 2
// 栈向下扩展16字节
stp x2, x3, [sp, #-16]!
// 出栈,并使用后变基恢复sp位置
ldp x2, x3, [sp], #16

mov x0, x3 // x2 = 1, x3 = 2

ret

MOV指令

mov指令通常用于寄存器之间的搬移和立即数搬移。

而能搬运的立即数只有以下两种:

  1. 16位立即数
  2. 16位立即数左移16位、32位或者48位的立即数

此时mov指令等同于movz指令

1
movz x0, 0x12bc, LSL #16

mov指令还可以用来搬运bitmask,此时等同orr指令

1
orr x0, XZR, #0xffff0000ffff

可以通过反汇编验证以上结论:

1
2
3
4
5
6
7
8
9
10
11
main.s:

.arch armv8-a
.text
.align 3

.global test2
test2:
mov x0, 0x12bc0000
mov x1, 0xffff0000ffff
ret

通常一个cpp文件要变成熟悉的可执行文件a.out,需要经过以下四个步骤

调用aarch64-linux-android-g++对main.s进行汇编过程:

1
aarch64-linux-android-g++ -c main.s -o main.o

调用aarch64-linux-android-objdump -s -d -M no-aliases test.o对main.o进行反汇编

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
aarch64-linux-android-objdump -s -d -M no-aliases main.o

# shell会打印出反汇编结果:
main.o: file format elf64-littleaarch64

Contents of section .text:
0000 8057a2d2 e13f00b2 c0035fd6 1f2003d5 .W...?...._.. ..

Disassembly of section .text:

0000000000000000 <test2>:
0: d2a25780 movz x0, #0x12bc, lsl #16
4: b2003fe1 orr x1, xzr, #0xffff0000ffff
8: d65f03c0 ret
c: d503201f hint #0x0

可以看到mov指令变成了movz和orr指令