ARMv8 - A64 - 函数调用,内存栈操作

说明

  • 本文环境基于:ARMv8-a架构A53核soc,aarch64状态。

背景知识

  1. 内存栈模型
  1. 相关汇编指令

示例

  • C源码
#include <stdio.h>

int test1()
{
    return test(1, 2);
}

int test(int a, int b)
{
    return a+b;
}

int main()
{
    test1();
    return 0;
}
  • 对应的汇编代码(aarch64-linux-gnu-gcc -S xxx.c)
    .arch armv8-a
    .file   "a.c"
    .text
    .align  2
    .global test1
    .type   test1, %function
test1:
    stp x29, x30, [sp, -16]!
    add x29, sp, 0
    mov w1, 2
    mov w0, 1
    bl  test
    ldp x29, x30, [sp], 16
    ret
    .size   test1, .-test1
    .align  2
    .global test
    .type   test, %function
test:
    sub sp, sp, #16
    str w0, [sp, 12]
    str w1, [sp, 8]
    ldr w1, [sp, 12]
    ldr w0, [sp, 8]
    add w0, w1, w0
    add sp, sp, 16
    ret
    .size   test, .-test
    .align  2
    .global main
    .type   main, %function
main:
    stp x29, x30, [sp, -16]!
    add x29, sp, 0
    bl  test1
    mov w0, 0
    ldp x29, x30, [sp], 16
    ret
    .size   main, .-main
    .ident  "GCC: (Linaro GCC 6.3-2017.05) 6.3.1 20170404"
    .section    .note.GNU-stack,"",@progbits
  • 从汇编代码可以看出存在两种不同实现,如下:
  1. 函数调用栈中间函数(test1)
test1:
    stp x29, x30, [sp, -16]! //将栈空间扩大16字节(更改sp寄存器值),再将x29,x30的数据(遗传自父函数)保存到栈顶 
    add x29, sp, 0 //将栈顶地址(sp)即此函数的栈帧基址保存到x29,
    ...  //函数操作(省略)
    bl  test //跳转到test函数执行
    ldp x29, x30, [sp], 16 //将栈顶数据load到x29,x30中,再缩小栈空间16字节(即将sp恢复到父函数的栈顶)
    ret //返回父函数
  1. 函数调用栈末端函数(test)
test:
    sub sp, sp, #16 //将sp保存的数据减小16字节,即将栈空间扩大16字节
    ... //函数操作(省略)
    add sp, sp, 16 //将sp保存的数据增加16字节,即将栈空间缩小16字节
    ret

问题

  1. 为什么中间函数和末端函数实现不同,中间函数需要将x29,x30保存到栈内存中,最后再从栈内存中load到x29,x30中。
  • 是因为中间函数(test1)bl指令调用末端函数(test)时,会覆盖掉x30的数据(原本保存的是父函数main,跳转test1的下一条指令),覆盖后中间函数(test1)的ret指令就跳不回main函数了,因此需要先将x30的数据保存到栈上,从子函数跳转回来后,需要将x29,x30的数据从栈上恢复。
  • x29是栈帧指针,保存是当前函数的frame pointer,是约定俗成,因此需要保存和恢复,但是也不是必须,例如:test函数中就没有使用x29。

核心寄存器

  1. FP:Frame Pointer(栈帧指针),指向当前栈帧的顶部
  2. SP:Stack Pointer(栈顶指针),保存栈顶地址,
  3. LR:Link Register(链接寄存器),保存子函数运行结束后的返回地址(跳转指令的下一条指令地址)。
  • 问题:初次了解,不好理解和区分FP和SP的作用和角色,SP是全局唯一的保存栈顶地址的寄存器,而FP是保存单个函数的栈帧基址,调用新函数,入栈操作结束后,需要将SP的值赋值给FP,类似于:SP是全局变量,而x29是局部变量,虽然大部分时刻两个寄存器值是一样的。

FP

  • A53平台是通用寄存器x29充当,如示例所示:进入新函数,更改栈顶指针(SP)后,需要将父函数的FP和LR保存到栈上,再手动将SP的新值复制给FP,作为新的栈帧指针。
  • APCS(ARM Procedure Call Standard,ARM 程序调用标准)对FP的使用要求如下:
  1. 除非子程序没有修改链接寄存器,否则FP都需要记录有效的栈帧位置
  2. 其寄存器(r11或者x29)不能被用做一个通用型的寄存器
  • 一级一级的保存FP的主要作用是栈回溯(backtrace),根据栈内存中每个栈帧中的FP数据,可以还原出函数调用栈。
  • 如不需要栈回溯功能,可以不一级一级保存FP,好处:节省一个寄存器和节省很多指令去保存,传递和恢复FP,gcc stack frame的优化选项,如下:
-fomit-frame-pointer //加上该选项则忽略掉FP栈顶指针
* 实例
aarch64-linux-gnu-gcc -fomit-frame-pointer -S xxx.c -o xxx.s
* 汇编代码
main:
    str     x30, [sp, -32]!   //没有保存FP(x29)值到栈上
    str     w0, [sp, 28]
    str     x1, [sp, 16]
    adrp    x0, .LC0
    add     x0, x0, :lo12:.LC0
    bl      puts
    mov     w0, 0
    ldr     x30, [sp], 32
    ret

SP

  • A53平台上是一个特殊寄存器,不同异常等级是不同的寄存器。
  • sp必须16Byte对齐,即扩大和缩小都必须是16字节的倍数。
  • 每个异常等级都有属于自己的栈指针(SP),SP_EL0、SP_EL1、SP_EL2和SP_EL3。
  • 默认情况下,操作SP寄存器会映射到CPU当前异常等级对应的栈指针,例如:EL1 时操作SP寄存器会自动操作SP_EL1,也可以通过spsel 配置成固定使用 SP_EL0。

SPSEL

  • spsel:(Stack Pointer Select)
    在这里插入图片描述
  • 说明:RES0表示预留,当前未使用。

LR

  • A53平台是使用通用寄存器x30充当,详细使用请看bl和ret指令说明
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值