说明
- 本文环境基于:ARMv8-a架构A53核soc,aarch64状态。
背景知识
- 内存栈模型
- 相关汇编指令
示例
#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
- 函数调用栈中间函数(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 //返回父函数
- 函数调用栈末端函数(test)
test:
sub sp, sp, #16 //将sp保存的数据减小16字节,即将栈空间扩大16字节
... //函数操作(省略)
add sp, sp, 16 //将sp保存的数据增加16字节,即将栈空间缩小16字节
ret
问题
- 为什么中间函数和末端函数实现不同,中间函数需要将x29,x30保存到栈内存中,最后再从栈内存中load到x29,x30中。
- 是因为中间函数(test1)bl指令调用末端函数(test)时,会覆盖掉x30的数据(原本保存的是父函数main,跳转test1的下一条指令),覆盖后中间函数(test1)的ret指令就跳不回main函数了,因此需要先将x30的数据保存到栈上,从子函数跳转回来后,需要将x29,x30的数据从栈上恢复。
- x29是栈帧指针,保存是当前函数的frame pointer,是约定俗成,因此需要保存和恢复,但是也不是必须,例如:test函数中就没有使用x29。
核心寄存器
- FP:Frame Pointer(栈帧指针),指向当前栈帧的顶部
- SP:Stack Pointer(栈顶指针),保存栈顶地址,
- 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的使用要求如下:
- 除非子程序没有修改链接寄存器,否则FP都需要记录有效的栈帧位置
- 其寄存器(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指令说明