Arm64笔记
参数寄存器(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
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. 把字面量 1 和 2 放入 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