现在iOS设备几乎已经都是ARM64架构,此外,Mac M1芯片的电脑也是基于ARM64架构,本文对ARM64汇编做一个简单的介绍。本文后面给出了一个汇编案例,通过汇编窥探代码底层的实现逻辑。
寄存器
ARM64汇编中有34个寄存器,其中包含31个通用寄存器(x0-x30),sp,pc和cpsr。
Xcode可以通过register read
指令查看所有寄存器的存储值:
(lldb) register read
General Purpose Registers:
x0 = 0x0000000000000008
x1 = 0x000000016fdfdc70
x2 = 0x000000016fdfdc80
x3 = 0x000000016fdfdd80
x4 = 0x0000000000000000
x5 = 0x0000000000000000
x6 = 0x0000000000000000
x7 = 0x0000000000000000
x8 = 0x0000000000000008
x9 = 0x0000000000000002
x10 = 0x0000000000000000
x11 = 0x0000000000000002
x12 = 0x0000000000000002
x13 = 0x0000000000000000
x14 = 0x0000000000000001
x15 = 0x0000000000000044
x16 = 0x000000030006f120
x17 = 0x6ae100016fdfa280
x18 = 0x0000000000000000
x19 = 0x00000001000d4060
x20 = 0x0000000100002ea0
x21 = 0x0000000100080070
x22 = 0x0000000000000000
x23 = 0x0000000000000000
x24 = 0x0000000000000000
x25 = 0x0000000000000000
x26 = 0x0000000000000000
x27 = 0x0000000000000000
x28 = 0x0000000000000000
fp = 0x000000016fdfdaf0
lr = 0x000000010000315c
sp = 0x000000016fdfdae0
pc = 0x0000000100002f4c
cpsr = 0x60001000
通用寄存器
通用寄存器x0-x30有64bit, 如果用不到64位,可以用低位的32位(w0-w30), 也就是说x0和w0本质上是一个寄存器,只是利用的位数不一样而已。
- 通用寄存器可以用来存放函数参数
说明:有的博客说的是“只有
x0-x7
用来存放参数,如果函数参数超过了8个,则在压入函数栈中”,我目前测试的情况是至少15个参数都是可以通过寄存器来传值,更多的参数就没有测试了。
-
x0
通常用来来存放返回值,如果返回的数据比较复杂,会放在x8
的这个执行地址上。
特殊寄存器
- sp寄存器:Stack Pointer,保存栈顶地址
- fp寄存器:Frame Pointer,保存栈底地址
- lr寄存器:Link Register,保存跳转指令下一条指令的地址(eg:bl跳转进入执行函数,lr则会保存函数调用完成后需要执行的指令地址)
- pc寄存器: 保存当前执行中的指令的下一条指令的地址
cpsr寄存器(状态寄存器)
其他寄存器都是存储数据,是个统一的整体;状态寄存器有点特殊,它的每一位都有特殊的含义,记录特定的信息。
最常见的是NZCV标志位,分别代表运算过程中产生的不同状态,可以决定运算结果或者代码执行逻辑。
- N:Negative Cndition Flag,代表运算结果是负数
- Z:Zero Condition Flag, Z 为 1 代表0,否则Z 为 0 代表 1
- C:Carry Condition Flag, 无符号运算有溢出时C 为 1
- V:Overflow Condition Flag, 有符号运算有溢出时C 为 1
xzr(零寄存器)
xzr/wzr
分别表示64/32位,其做用就是0,写进去表明丢弃结果,读出来是0。
常用指令
加载/存储指令
加载/存储指令⽤于在寄存器和存储器之间传送数据,加载指令⽤于将存储器中的数据传送到寄存器,存储 指令则完成相反的操作。
-
ldr
指令
ldr x8, [sp] // 将存储地址为sp的数据读取到x8寄存器中
ldr x9, [sp, #0x8] // 将存储地址为sp+0x8的数据读取到x9寄存器中
-
ldrb
指令(只操作一个字节)
ldrb w8, [sp, #0x8] // 将存储器地址为sp+0x8的1个字节数据读⼊寄存器w8,并将w8的⾼24位清零。
-
ldrh
指令(只操作两个字节)
ldrh w8, [sp, #0x8] // 将存储器地址为sp+0x8的2个字节数据读⼊寄存器w8,并将w8的⾼16位清零。
-
ldur
指令 (u和负地址运算相关)
ldurb w0, [x29, #-0x8] // 将存储地址为x29-0x8的数据读取到w0寄存器中
-
stp
指令(str 的变种指令,能够同时操做两个寄存器)
stp x29, x30, [sp, #0x10] // 将 x29, x30 的值存⼊sp+0x10存储地址
-
str
指令
str x0, [sp] // 将x0寄存器中的值存储到sp的存储地址
str x0, [sp, #0x10] // 将x0寄存器中的值存储到sp+0x10的存储地址
-
strb
指令(只操作一个字节)
strb x0, [sp, #0x10] // 将x0寄存器中的低8位的字节的数据存储到sp+0x10的存储地址
-
strh
指令(只操作两个字节)
strh x0, [sp, #0x10] // 将x0寄存器中的低16位的字节的数据存储到sp+0x10的存储地址
-
stur
指令 (u和负地址运算相关)
stur w0, [x29, #-0x8] // 将x0寄存器中的数据存储到x29-0x8的存储地址
-
ldp
指令(ldr 的变种指令,能够同时操做两个寄存器)
ldp x29, x30, [sp, #0x10] // 将sp+0x10的值取出来,存⼊寄存器 x29 和寄存器 x30
数据处理指令
-
mov
指令(不用于内存地址)
mov w8, #0x1 //将立即数赋值给w8寄存器
mov x0, x8 //将给x0寄存器赋值给给x8寄存器
-
add
指令 (加)
add sp, sp, #0x20 // 将寄存器sp的值和立即数0x20相加后保存在寄存器sp中
add x0, x1, x2 // 将寄存器 x1 和 x2 的值相加后保存到寄存器 x0 中
add x0, x1, [x2] // 将寄存器x1的值加上寄存器x2 的值做为地址,再取该内存地址的内容放⼊寄存器x0中
-
sub
指令 (减)
sub sp, sp, #0x20 // 将寄存器sp的值和立即数0x20相减后保存在寄存器sp中
sub x0, x1, x2 // 将寄存器 x1 和 x2 的值相减后保存到寄存器 x0 中
-
mul
指令 (乘)
mul x0, x1, x2 // 将寄存器 x1 和 x2 的值相乘后结果保存到寄存器 x0 中
-
sdiv
指令 (除,无符号除是udiv)
sdiv x0, x1, x2 // 将寄存器 x1 和 x2 的值相除后结果保存到寄存器 x0 中
-
and
指令 (位与)
and x0, x1, x2 // 将寄存器 x1 和 x2 的值按位与后保存到寄存器 x0 中
-
orr
指令 (位或)
orr x0, x1, x2 // 将寄存器 x1 和 x2 的值按位或后保存到寄存器 x0 中
-
eor
指令 (位异或)
eor x0, x1, x2 // 将寄存器 x1 和 x2 的值按位异或后保存到寄存器 x0 中
-
cbnz
指令 (和⾮ 0 ⽐较)
cbnz x0, 0x100002f70 // 如果非0,跳转到0x100002f70指令执行
-
cbz
指令 (和0 ⽐较)
cbz x0, 0x100002f70 // 如果为0,跳转到0x100002f70指令执行
跳转指令
-
b
指令 (直接跳转)
b 0x100002fd0
-
bl
指令 (跳转前记录下一条指令地址)
bl 0x100002f54
-
blr
指令 (跳转到某寄存器 (的值)指向的地址)
blr x10
-
ret
指令 (函数返回)
ret
汇编窥探代码底层
enum EnumTest: Int {
case n1, n2, n3, n4
}
var e = EnumTest.init(rawValue: 1)
下面的代码是Swift
枚举类型init?(rawValue:)
方法的汇编代码,通过汇编就可以窥探该方法的实现逻辑。
0x100002f54 <+0>: sub sp, sp, #0x20 // sp = sp - 32,将栈顶指针向下移动 32 字节
100002f58 <+4>: str x0, [sp, #0x8] // 将x0参数存储到sp+0x8的存储位置 (将参数保存到局部变量)
100002f5c <+8>: str xzr, [sp, #0x10] // 将sp+0x10的存储位置的数据清零
100002f60 <+12>: str x0, [sp, #0x10] // 将x0的数据又保存到sp+0x10的存储位置
100002f64 <+16>: cbnz x0, 0x100002f70 // 判断x0是否是0,如果是0执行0x100002f68指令,否则执行0x100002f70指令(分支1)
100002f68 <+20>: strb wzr, [sp, #0x1f] // 将0保存到sp+0x1f的存储位置
100002f6c <+24>: b 100002fd0 // 直接跳转到0x100002fd0处理结果
100002f70 <+28>: ldr x9, [sp, #0x8] // 将保存的局部变量参数读取出来,放在x9寄存器上
100002f74 <+32>: mov w8, #0x1 // 将0x1存储到x8寄存器上
100002f78 <+36>: subs x8, x8, x9 // 用0x1-x9寄存器的值放到x8寄存器上
100002f7c <+40>: b.ne 0x100002f8c // 如果等于0执行0x100002f80指令,否则执行0x100002f8c指令(分支2)
100002f80 <+44>: mov w8, #0x1 // 将1赋值给w8寄存器
100002f84 <+48>: strb w8, [sp, #0x1f] // 将w8寄存器中的1保存到sp+0x1f的存储位置
100002f88 <+52>: b 100002fd0 // 直接跳转到0x100002fd0处理结果
100002f8c <+56>: ldr x9, [sp, #0x8]
100002f90 <+60>: mov w8, #0x2
100002f94 <+64>: subs x8, x8, x9
100002f98 <+68>: b.ne 0x100002fa8 // (分支3)
100002f9c <+72>: mov w8, #0x2
100002fa0 <+76>: strb w8, [sp, #0x1f]
100002fa4 <+80>: b 100002fd0
100002fa8 <+84>: ldr x9, [sp, #0x8]
100002fac <+88>: mov w8, #0x3
100002fb0 <+92>: subs x8, x8, x9
100002fb4 <+96>: b.ne 0x100002fc4 // (分支4)
100002fb8 <+100>: mov w8, #0x3
100002fbc <+104>: strb w8, [sp, #0x1f]
100002fc0 <+108>: b 100002fd0 // (分支5)
100002fc4 <+112>: mov w8, #0x4
100002fc8 <+116>: str w8, [sp, #0x4]
100002fcc <+120>: b 100002fd8
100002fd0 <+124>: ldrb w8, [sp, #0x1f] // 将结果取出来放在w8寄存器中
100002fd4 <+128>: str w8, [sp, #0x4] // 将w8寄存器中的结果又放入sp+0x4的存储地址中
100002fd8 <+132>: ldr w0, [sp, #0x4] // sp+0x4的存储地址中的值赋值给w0 (相当于将结果放在了x0寄存器上)
100002fdc <+136>: add sp, sp, #0x20 // 恢复栈顶位置
100002fe0 <+140>: ret // 函数执行完后返回
其实编译器帮我们实现的逻辑代码就是如下所示,因为下面的代码的汇编和上面的汇编代码是一致的:
init?(rawValue: Int) {
switch rawValue {
case 0: self = .n1
case 1: self = .n2
case 2: self = .n3
case 3: self = .n4
default: return nil
}
}
惊奇的发现
原来原始值的枚举类型EnumTest
.n1
, .n2
, .n3
, .n4
, 内存真实存储的值是 0,1,2,3
, 而且nil
的存储值竟然是4
。