美文网首页
ARM64汇编入门

ARM64汇编入门

作者: chonglingliu | 来源:发表于2022-08-23 16:08 被阅读0次

    现在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
    
    通用寄存器

    通用寄存器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寄存器中
    
    • 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

    相关文章

      网友评论

          本文标题:ARM64汇编入门

          本文链接:https://www.haomeiwen.com/subject/fueggrtx.html