0%

初识ARM64汇编

所有的程序,最终编译为机器(汇编)代码,运行起来,本质都是操作CPU的寄存器和内存,了解了寄存器和内存的操作,就能知道程序具体做了什么

汇编是依赖机器架构的,不同的CPU的指令不同

iOS汇编

  • 真机:amr64,GNU 汇编
  • 模拟器:x86,AT&T 汇编

寄存器

CPU运行时用于临时存储的数据,都会放到寄存器中

通用寄存器(数据地址寄存器)

主要用户存储临时数据,例如加法运算,地址操作等

  • 64bit 的:0x ~ x28,有29个寄存器
  • 32bit 的:w0 ~ w28(属于 x0 ~ x28的低32bit)

其中 x0 ~ x7通常用来存放函数的参数,超过的食用堆栈来存放
x0 通常用来存放函数的返回值

通过 lldb 存取寄存器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 读取所有寄存器地址的值
registe read

# 读取寄存器地址为x0的值
registe read x0

# 向寄存器地址为x0写入值
registe write x0 0x1100000000000011

# 读取寄存器x0的值
registe read x0
x0 = 0x1100000000000011

# 读取寄存器 w0的值(相当于取 x0 的低32bit)
registe read w0
x0 = 0x00000011

再xcode查看寄存器的值

PC寄存器(program counter)

PC寄存器为指令指针寄存器,用于存放当前CPU要读取的指令的地址,PC寄存器指到哪,CPU执行到哪

SP寄存器(stack pointer registers)

是一个指向堆栈顶部的寄存器,通常用于为方法执行开辟空间和回收空间,进入方法入栈,离开方法出栈,例如下面

1
2
3
4
5
6
7
8
Test`main:
0x100dc3f9c <+0>: sub sp, sp, #0x10 ; sp的指针往下移 0x10,开辟栈空间用于存放临时变量
0x100dc3fa0 <+4>: str wzr, [sp, #0xc]
0x100dc3fa4 <+8>: str w0, [sp, #0x8]
0x100dc3fa8 <+12>: str x1, [sp]
-> 0x100dc3fac <+16>: mov w0, #0x1
0x100dc3fb0 <+20>: add sp, sp, #0x10 ; sp的指针往上移 0x10,回收栈空间
0x100dc3fb4 <+24>: ret

CPSR状态寄存器(current program status register)

CPSR是状态寄存器,用于存放程序运行中一些状态标识。其他寄存器是用来存放数据的,而CPSR寄存器是按位起作用的,它的每一位都有专门的含义,通常存放比较结果,溢出等信息,下面为C语言判断语句的

1
2
3
4
5
if (a == 10) {
printf("aa");
} else {
printf("bb");
}

对应的汇编代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...

cmp w8, #0xa ; =0xa ; 比较w8和#0xa的值,w8就是a,比较结果会放到CPSR寄存器中
b.ne 0x104037f64 ; <+60> at main.m:16:9 ; 如果不等,则跳到 0x104037f64,通过CPSR的状态判断

adrp x0, 0
add x0, x0, #0xfb0 ; =0xfb0
bl 0x104037f80 ; symbol stub for: printf
b 0x104037f70 ; <+72> at main.m:18:1 ; 跳到 0x104037f70

adrp x0, 0
add x0, x0, #0xfb3 ; =0xfb3
bl 0x104037f80 ; symbol stub for: printf

ldur w0, [x29, #-0x4] ; 判断外

...

高速缓存

由于CPU读取指令需要从内存中读取,而内存的读写速度要远远低于寄存器的速度,为了拟补这里的时间差,通常CPU还会提供一个速度介于内存和寄存器的高速缓存,再程序运行前,会先将要执行的指令和数据提前读取到高速缓存中,而CPU从高速缓存中读取指令来执行

指令

汇编使用;分号开头表示注释

函数定义

声名 test 函数

1
2
3
4
5
6
7
8
9
10
11
; global 用于声明函数为公开的,可以被外部调用
; 汇编语言的函数比上层函数多一个下划线(外部调用为 test),如果是内部使用的函数,可以不加下划线
.global _test, _add, _sub

_test:
; 赋值操作: x0 = 0x8
mov x0 #0x8
; 赋值赋值: x1 = x0
mov x1, x0
; 函数返回: return
ret

mov:赋值操作

mov指令只允许操作寄存器的,不允许直接操作内存

1
2
; 把 0x8 赋值给寄存器 x0
mov x0, #0x8

add: 加法运算

add只能对寄存器做加法,内存中的数据必须先放到寄存器中

1
2
; x1 = x1 + x0
add x1, x0

sub:减法

1
2
3
4
5
; sp的指针往下移 0x20
sub sp, sp, #0x20

; sp的指针往上移 0x20
add sp, sp, #0x20

ret指令

返回函数调用出,相当于c语言的return,与bllr配合使用,本质是将lr寄存器的值赋值给pc寄存器

1
ret

cmp:比较两个数(减法)

比较结果会被放在 cpsr(程序状态寄存器里面),cpsr使用标识位控制

1
2
3
mov x0, #0x3
mov x1, #0x2
cmp x0, x1

b跳转指令

相当于 C 语言的 goto

1
2
3
4
5
; 直接跳到 mycode
b mycode
mov x0, #0x5
mycode:
mov x1, #0x6

b指令可以带条件,通常跟 cmp 配合使用

  • eq: 相等
  • ne:不相等
  • gt:大于
  • lt:小于
  • ge:大于或等于
  • le:小于或等于
1
2
3
4
5
6
7
8
9
mov x0, #0x1
mov x1, #0x2
; 比较 x0, x1
cmp x0, x1

; 如果x0 = x1就跳转
beq mycode
; 如果x0 > x1则跳转
bgt mycode

bl指令

可以带返回的跳转指令,跳转执行完成后会回到调用的地方继续往下执行,相当于函数调用

1
2
3
4
5
6
7
8
9
10
11
privatecode:
mov x0, #0x1
mov x2, #0x2
add x2, x0, x1
ret

_test:
; 在 privatecode ret 之后,回继续走下面的语句,如果是 b 指令的话,不会走下面的语句
bl privatecode
mov x3, #0x2
mov x4, #0x4

bl 会将吓一跳指令的地址存储到 lr(x30)寄存器中

ldr/ldur/ldp(从内存读取数据)

如果偏移地址是正数,用ldr,如果是负数,用ldur

1
2
3
4
5
6
7
8
9
10
; 从x1的地址中读取数据值到 x0 中
ldr x0, [x1]

; 从[x1 + 0x4]地址读取数据值到 x0 中
ldr x0, [x1, #0x4]

; 在上面的基础上,加上 x1 = x1 + 0x4 的操作,会改变 x1
ldr x0, [x1, #0x4]!

ldur x0, [x1, #-0x8]

ldp,读取一对数据到(2个)寄存器

1
2
; x1偏移0x4的地址分别读取4个字节到 w1 和 w2
ldp w1, w2, [x1, #0x4]

str/stur/stp(往内存写数据)

寄存器在左边,如果偏移地址是正数,用str,如果是负数,用stur

1
2
3
4
; 把 w0的数据写到 x1的地址
str w0, [x1, #0x5]

stur w0, [x1, #-0x5]

stp: 写入2个寄存器的数据到连续的内存

1
2
; x1偏移0x4的地址分别读取4个字节到 w1 和 w2
stp w1, w2, [x1, #-0x5]!

wzr/xzr(零寄存器)

里面存储的值为0,不能在lldb中读/写,表示清零操作(false, nil, 0)

  • wzr(32bit)
  • xzr(64bit)

lr寄存器

lr:存储函数的返回地址,与bl配合使用(bl执行跳转执行指令ret后,会回到调用的地方,就是根据lr存放的地址)

函数的分类

  • 叶子函数:内部不再调用其他函数的函数,不用再开辟栈空间
  • 非叶子函数:函数内部会调用其他的函数