当前位置:   article > 正文

ARM64汇编入门

arm64汇编

现在iOS设备几乎已经都是ARM64架构,此外,Mac M1芯片的电脑也是基于ARM64架构,本文对ARM64汇编做一个简单的介绍。本文后面给出了一个汇编案例,通过汇编窥探代码底层的实现逻辑。

寄存器

ARM64汇编中有34个寄存器,其中包含31个通用寄存器(x0-x30),sppccpsr
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
  • 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
通用寄存器

通用寄存器x0-x3064bit, 如果用不到64位,可以用低位的32位(w0-w30), 也就是说x0w0本质上是一个寄存器,只是利用的位数不一样而已。

  • 通用寄存器可以用来存放函数参数

说明:有的博客说的是“只有x0-x7用来存放参数,如果函数参数超过了8个,则在压入函数栈中”,我目前测试的情况是至少15个参数都是可以通过寄存器来传值,更多的参数就没有测试了。

  • x0通常用来来存放返回值,如果返回的数据比较复杂,会放在x8的这个执行地址上。
特殊寄存器
  • sp寄存器:Stack Pointer,保存栈顶地址
  • fp寄存器:Frame Pointer,保存栈底地址
  • lr寄存器:Link Register,保存跳转指令下一条指令的地址(eg:bl跳转进入执行函数,lr则会保存函数调用完成后需要执行的指令地址)
  • pc寄存器: 保存当前执行中的指令的下一条指令的地址
cpsr寄存器(状态寄存器)

其他寄存器都是存储数据,是个统一的整体;状态寄存器有点特殊,它的每一位都有特殊的含义,记录特定的信息。

最常见的是NZCV标志位,分别代表运算过程中产生的不同状态,可以决定运算结果或者代码执行逻辑。

  • NNegative Cndition Flag,代表运算结果是负数
  • ZZero Condition Flag, Z 为 1 代表0,否则Z 为 0 代表 1
  • CCarry Condition Flag, 无符号运算有溢出时C 为 1
  • VOverflow Condition Flag, 有符号运算有溢出时C 为 1
xzr(零寄存器)

xzr/wzr分别表示64/32位,其做用就是0,写进去表明丢弃结果,读出来是0。

常用指令
加载/存储指令

加载/存储指令⽤于在寄存器和存储器之间传送数据,加载指令⽤于将存储器中的数据传送到寄存器,存储 指令则完成相反的操作。

  • ldr指令
ldr    x8, [sp]        // 将存储地址为sp的数据读取到x8寄存器中
ldr    x9, [sp, #0x8]  // 将存储地址为sp+0x8的数据读取到x9寄存器中
  • 1
  • 2
  • ldrb指令(只操作一个字节)
ldrb   w8, [sp, #0x8] // 将存储器地址为sp+0x8的1个字节数据读⼊寄存器w8,并将w8的⾼24位清零。
  • 1
  • ldrh指令(只操作两个字节)
ldrh   w8, [sp, #0x8] // 将存储器地址为sp+0x8的2个字节数据读⼊寄存器w8,并将w8的⾼16位清零。
  • 1
  • ldur指令 (u和负地址运算相关)
ldurb  w0, [x29, #-0x8]  // 将存储地址为x29-0x8的数据读取到w0寄存器中
  • 1
  • stp指令(str 的变种指令,能够同时操做两个寄存器)
stp x29, x30, [sp, #0x10]  // 将 x29, x30 的值存⼊sp+0x10存储地址
  • 1
  • str指令
str    x0, [sp]         // 将x0寄存器中的值存储到sp的存储地址
str    x0, [sp, #0x10]  // 将x0寄存器中的值存储到sp+0x10的存储地址
  • 1
  • 2
  • strb指令(只操作一个字节)
strb    x0, [sp, #0x10]  // 将x0寄存器中的低8位的字节的数据存储到sp+0x10的存储地址
  • 1
  • strh指令(只操作两个字节)
strh    x0, [sp, #0x10]  // 将x0寄存器中的低16位的字节的数据存储到sp+0x10的存储地址
  • 1
  • stur指令 (u和负地址运算相关)
stur    w0, [x29, #-0x8]  // 将x0寄存器中的数据存储到x29-0x8的存储地址
  • 1
  • ldp指令(ldr 的变种指令,能够同时操做两个寄存器)
ldp    x29, x30, [sp, #0x10]  // 将sp+0x10的值取出来,存⼊寄存器 x29 和寄存器 x30
  • 1
数据处理指令
  • mov指令(不用于内存地址)
mov    w8, #0x1   //将立即数赋值给w8寄存器
mov    x0, x8     //将给x0寄存器赋值给给x8寄存器
  • 1
  • 2
  • add指令 (加)
add    sp, sp, #0x20      // 将寄存器sp的值和立即数0x20相加后保存在寄存器sp中
add    x0, x1, x2         // 将寄存器 x1 和 x2 的值相加后保存到寄存器 x0 中
add    x0, x1, [x2]       // 将寄存器x1的值加上寄存器x2 的值做为地址,再取该内存地址的内容放⼊寄存器x0中
  • 1
  • 2
  • 3
  • sub指令 (减)
sub    sp, sp, #0x20     // 将寄存器sp的值和立即数0x20相减后保存在寄存器sp中
sub    x0, x1, x2        // 将寄存器 x1 和 x2 的值相减后保存到寄存器 x0 中
  • 1
  • 2
  • mul指令 (乘)
mul    x0, x1, x2     // 将寄存器 x1 和 x2 的值相乘后结果保存到寄存器 x0 中
  • 1
  • sdiv指令 (除,无符号除是udiv)
sdiv    x0, x1, x2     //  将寄存器 x1 和 x2 的值相除后结果保存到寄存器 x0 中
  • 1
  • and指令 (位与)
and    x0, x1, x2     //  将寄存器 x1 和 x2 的值按位与后保存到寄存器 x0 中
  • 1
  • orr指令 (位或)
orr    x0, x1, x2     //  将寄存器 x1 和 x2 的值按位或后保存到寄存器 x0 中
  • 1
  • eor指令 (位异或)
eor    x0, x1, x2     //  将寄存器 x1 和 x2 的值按位异或后保存到寄存器 x0 中
  • 1
  • cbnz指令 (和⾮ 0 ⽐较)
cbnz   x0, 0x100002f70    // 如果非0,跳转到0x100002f70指令执行
  • 1
  • cbz指令 (和0 ⽐较)
cbz   x0, 0x100002f70    // 如果为0,跳转到0x100002f70指令执行
  • 1
跳转指令
  • b指令 (直接跳转)
b      0x100002fd0
  • 1
  • bl指令 (跳转前记录下一条指令地址)
bl     0x100002f54
  • 1
  • blr指令 (跳转到某寄存器 (的值)指向的地址)
blr     x10
  • 1
  • ret指令 (函数返回)
ret
  • 1
汇编窥探代码底层
enum EnumTest: Int {
    case n1, n2, n3, n4
}

var e = EnumTest.init(rawValue: 1)
  • 1
  • 2
  • 3
  • 4
  • 5

下面的代码是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                               // 函数执行完后返回
  • 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

其实编译器帮我们实现的逻辑代码就是如下所示,因为下面的代码的汇编和上面的汇编代码是一致的:

init?(rawValue: Int) {
    switch rawValue {
        case 0: self = .n1
        case 1: self = .n2
        case 2: self = .n3
        case 3: self = .n4
        default: return nil
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
惊奇的发现

原来原始值的枚举类型EnumTest .n1, .n2, .n3, .n4, 内存真实存储的值是 0,1,2,3, 而且nil的存储值竟然是4

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/不正经/article/detail/487043
推荐阅读
  

闽ICP备14008679号