Skip to content

Arm64笔记

alt text

  • 参数寄存器(X0-X7): 用作临时寄存器或可以保存的调用者保存的寄存器变量函数内的中间值,调用其他函数之间的值(8 个寄存器可用于传递参数)

  • 调用者保存的临时寄存器(X9-X15): 如果调用者要求在任何这些寄存器中保留值调用另一个函数,调用者必须将受影响的寄存器保存在自己的堆栈中帧。 它们可以通过被调用的子程序进行修改,而无需保存并在返回调用者之前恢复它们。

  • 被调用者保存的寄存器(X19-X29): 这些寄存器保存在被调用者帧中。 它们可以被被调用者修改子程序,只要它们在返回之前保存并恢复。

  • 特殊用途寄存器(X8,X16-X18,X29,X30):

  • X8: 是间接结果寄存器,用于保存子程序返回地址,尽量不使用

  • X16 和 X17: 程序内调用临时寄存器

  • X18: 平台寄存器,保留用于平台 ABI,尽量不使用

  • X29: 帧指针寄存器(FP)

  • X30: 链接寄存器(LR)

  • X31: 堆栈指针寄存器 SP 或零寄存器 ZXR

内核空间

|--------------|
| Kernal Space |
|--------------| 高地址 0xFFFF
|              | 栈地址 从高到低 向⬇增长
|     Stack    |
|              |
|--------------|
|              |
|   待分配内存   |
|              |
|--------------|
|              | 堆地址 从低到高 向⬆增长
|     Heap     |
|              |
|--------------|
| Data Segment |
|--------------|
| Code Segment |
|--------------| 低地址 0x0000

通过https://godbolt.org/生成汇编代码

c源文件

cpp
long test(){
    long x = 5;
    long y = 3;
    long z = 4;
    return x+y;
}

int main(){

    test();
    return 0;
}

生成的汇编的代码

asm
test():  // @test()
    // 栈空间是从高地址往低地址分配空间的, 我们看到有 x y z 三个本地临时变量
    // 共 3*long = 24bytes, 也就是需要 24 字节的栈空间
    // 但是 arm64 有个约定, 分配栈空间的大小须为 16 字节的倍数, 所以这里需申请 32bytes

    // sp = stack pointer, 指向栈顶(也是栈空间里可用的最低地址)
    // 我们看到这里直接 通过 sp=sp-32 来开辟了 32 字节的空间
    // 而且 32 是立即数, 也就是编译器在编译期就已经确定了的.
    sub     sp, sp, #32   // =32

    // 申请之后可用的栈空间是这样的, sp 指向了栈顶:
    // | sp + 24|  8 bytes
    // | sp + 16|  8 bytes
    // | sp + 8 |  8 bytes
    // | sp     |  8 bytes

    // 对应 x=5, 不能直接把 5 放到内存, 需要寄存器中转一下, 先把 5 放入 x8 寄存器
    mov     x8, #5  // 立即数以#开头, 这里把5放到x8寄存器中
    // sp 既然是指针, 也就是地址, 所以支持
    // 1. 地址支持加减运算, 2: 存取(store/load) 数据都需要使用 [] 来找到地址所对应的值
    // 然后接上面, 把 x8 也就是 5, 放入了 sp + 24 对应的地址里
    str     x8, [sp, #24]

    mov     x8, #3  // 同上, 操作y
    str     x8, [sp, #16]

    mov     x8, #4  // 同上, 操作z
    str     x8, [sp, #8]

    操作完之后, 栈空间是这样的:
    // | sp + 24|  就是 x, 值为 5
    // | sp + 16|  就是 y, 值为 3
    // | sp + 8 |  就是 z, 值为 4
    // | sp     |	 未使用

    // 可见这里入栈顺序和临时变量定义的顺序是一致的

    //  操作 x + y
    ldr     x8, [sp, #24] //把 x 读取到x8
    ldr     x9, [sp, #16] //把 y 读取到x9

    // 现在 x0 = x8+x9, 保存着相加的结果值 8
    add     x0, x8, x9

    // 释放分配的栈空间, 其实就是把 sp + 32, 相当于 sp 指针向上移动了 32 个字节
    // 那我们知道栈空间分配的方向是从高地址到低地址, 释放就是相反的方向也容易理解了.
    add     sp, sp, #32                     // =32

    // 默认返回 x0, 后文会介绍
    ret

main:                                   // @main
        sub     sp, sp, #32
        stp     x29, x30, [sp, #16]             // 16-byte Folded Spill
        add     x29, sp, #16
        mov     w8, wzr
        str     w8, [sp, #8]                    // 4-byte Folded Spill
        stur    wzr, [x29, #-4]
        bl      test
        ldr     w0, [sp, #8]                    // 4-byte Folded Reload
        ldp     x29, x30, [sp, #16]             // 16-byte Folded Reload
        add     sp, sp, #32
        ret

函数调用

c
long add(long x, long y) {
    return x + y;
}

int main() {
    long z = add(1, 2);
    return 0;
}
asm
    main:                                   // @main
    // 1. 分配 48 字节的栈空间, 使用情况见 step 11
    sub     sp, sp, #48                     // =48

    // 2. stp 和 str 类似, 区别是 stp 一次保存多个
    // 这里等于把 x29/FP => [sp + 32], x30/LR => [sp + 40]
    stp     x29, x30, [sp, #32]             // 16-byte Folded Spill

    // | sp + 48|  8 bytes
    // | sp + 40|  8 bytes  <=x30/LR
    // | sp + 32|  8 bytes  <=x29/FP
    // | sp + 0 |  8 bytes

    // 3. x29 = sp + 32                     
    add     x29, sp, #32                    // =32

    // 4. w8 = 0, 然后存入后面能用到
    mov     w8, wzr

    // 5. x29-4 = sp+32-4 = sp + 28
    stur    wzr, [x29, #-4]

    // 6. 把字面量 12 放入 X0, X1, 作为入参传给 add
    mov     x0, #1
    mov     x1, #2

    // 7. 前面把 w8 置为 0, 这里相当于在 sp+12 位置保存了一个 0
    str     w8, [sp, #12]                   // 4-byte Folded Spill

    // 8. 函数调用
    bl      add(long, long)

    // 9. 把 X0 也就是返回值, 放入 sp + 16
    str     x0, [sp, #16]

    // 10. 因为 main 的返回值是 int, 4 字节, 所以用的是 w0, sp+12 前面我们知道保存的是 0
    // 所以这里相当于把 0 放入了 w0, 作为 main 函数的返回值
    ldr     w0, [sp, #12]                   // 4-byte Folded Reload

    // 11. 回顾一下分配的 48 字节栈空间的使用情况
    | sp + 40  |  LR (8 bytes)
    | sp + 32  |  FP (8 bytes)
    | sp + 24  |  0  (8 bytes, 低四位(sp + 28) 存放 0)
    | sp + 16  |  X0 (8 bytes)
    | sp + 8   |  0  (8 bytes, 低四位(sp + 28) 存放 0)
    | sp       |     (8 bytes, 为了16对齐, 多分配出来的)

    // 和 step2 操作相反, 恢复 X29, X30, 也就是 FP 和 LR 寄存器
    // 类似 ldr, ldp load 多个: X29 <= [sp + 32], X30 <= [sp + 40]
    ldp     x29, x30, [sp, #32]             // 16-byte Folded Reload

    // 释放栈空间
    add     sp, sp, #48                     // =48
    ret

    add(long, long):                               // @add(long, long)
    // add 函数有两个 long 参数, 会占用栈空间, 分配 16 字节
    sub     sp, sp, #16                     // =16

    // X0 是第一个参数 x, 保存到 sp + 8
    str     x0, [sp, #8]
    // X1 是第二个参数 y, 保存到 sp
    str     x1, [sp]

    // 取出 x 和 y
    ldr     x8, [sp, #8]
    ldr     x9, [sp]

    // 相加, 把和放入 X0 中, 也是约定的返回值存放位置
    add     x0, x8, x9

    // 释放栈空间
    add     sp, sp, #16                     // =16
    // 返回
    ret