深入理解ARM64的函数调用标准与栈布局

一、引言

随着计算机技术的飞速发展,人们对计算机的性能要求越来越高,为了突破32位架构的4GB地址空间限制,并实现更好的性能提升。ARM公司推出了一种64位处理器架构,也就是我们今天所要讨论的ARM64。ARM64(也称ARMv8)面世以来,在移动设备、嵌入式系统以及服务器领域得到了广泛的应用。对于我们开发人员来说,深入了解ARM64架构下的函数调用标准和栈布局可以帮助我们更好的进行开发以及优化我们的代码。

本文我们将深入探讨ARM64架构下的函数调用约定,包括寄存器约定、参数传递、返回值处理以及调用者/被调用者寄存器保存规则等。同时,我们也将分析ARM64体系结构下的栈布局,包括栈指针、帧指针以及栈帧对齐这些方面。下面我们就一起来看一下ARM64的函数调用标准与栈布局吧。

二、ARM64函数调用标准

2.1 定义

函数调用标准(Procedure Call Standard, PCS)是指在函数调用过程中,对父/子函数如何编译以及链接做一些规范和约定。如参数的传递,返回值处理、寄存器的使用和栈的布局等。函数调用标准定义了函数调用的具体实现机制。

注意:对于不同的处理器架构都有不同的函数调用标准,本文我们将讲解关于ARM64的函数调用标准。

Procedure Call Standard for the Arm 64-bit Architecture.pdf是一份描述ARM64架构函数调用的标准和规范文档。该文档网址地址点这里

2.2 寄存器的使用约定

在深入探讨ARM64函数调用标准和栈布局之前,我们先简要了解一下ARM64架构的通用寄存器以及SP寄存器,它们在ARM64栈布局的实现中起着关键作用。

ARM64架构提供了31个64位通用寄存器,编号从X0到X30.这些通用寄存器可以用于存储整数、地址和指针等数据。
SP寄存器用来管理函数调用期间的栈空间。SP寄存器指向当前栈的栈顶位置。

ARM64架构寄存器的描述我在前面的文章有详细介绍,大家感兴趣可以点这里

下面我们来简单了解一下ARM64架构的通用寄存器以及SP寄存器(特殊寄存器)。

寄存器 特殊寄存器 功能描述
sp 存放栈指针
X30 LR 链接寄存器,在函数调用时存储函数的返回地址
X29 FP 栈帧指针寄存器,指向当前函数的栈帧
X19 ~ X28 被调用函数保存的寄存器。在子函数中使用时需要保存到栈中
X18 通常被用作平台相关的寄存器
X17 IP2 临时寄存器或者第二个IPC(Intra-Procedure-Call)临时寄存器
X16 IP1 临时寄存器或者第一个IPC(Intra-Procedure-Call)临时寄存器
X9 ~ X15 调用者保存的寄存器,临时寄存器
X8 间接结果位置寄存器,用于保存子程序的返回地址
X0 ~ X7 用于传递子程序参数和结果,若参数个数大于8,就采用栈来传递。64位的返回结果采用X0寄存器,128位的返回结果采用X0和X1两个寄存器

三、 ARM64栈布局

3.1 栈的概念

栈是一种特殊的数据结构,它遵循后进先出(LIFO,Last-In-First-Out)的原则。栈只允许在栈顶进行添加(push)或删除(pop)元素的操作。

在 ARM64 架构中,栈的方向是从高地址往低地址生长。栈的起始地址称为栈底,栈从高地址延伸到栈顶(低地址)。
注意: A64指令集使用加载和存储指令来实现入栈和出栈操作。A32指令集提供了PUSH和POP指令来实现入栈和出栈操作,但是A64指令集已经去掉了PUSH和POP指令集。

栈在函数调用过程中起到非常重要的作用,包括存储函数使用的局部变量、传递参数等。

3.2 栈帧的概念

在函数调用过程中,栈是逐步生成的。当一个函数被调用时,CPU 会在内存栈上为该函数创建一个新的栈空间。即从该函数栈底(高地址)到栈顶(低地址)的这段空间我们把它称为栈帧(stack frame)。栈帧是函数调用时在内存栈上分配的一块区域,用于存储函数的局部变量、参数等信息。

栈帧通常包含以下几个部分:

返回地址: 当前函数返回时需要跳转的地址,通常是调用函数的下一条指令。返回地址通常是由调用指令自动压入栈的。
函数参数: 传递给当前函数的参数值,它们需要被保存在栈帧中。当函数返回时,这些参数值需要被恢复。
局部变量: 函数内部声明的局部变量,它们的生命周期仅限于函数的执行期间。这些变量需要被保存在栈帧中,以免被其他函数覆盖。
寄存器备份: 在函数调用过程中,需要保存一些重要的寄存器值,以便在返回时恢复。这些寄存器可能包括函数返回地址寄存器、帧指针寄存器等。
动态分配的内存: 函数在执行过程中动态分配的内存空间,需要在栈帧中保存相关信息。这些动态分配的内存会在函数返回时被自动释放。

四、代码分析

4.1 代码示例

下面我们将以具体的代码为例对ARM64的函数调用标准与栈布局进行详细分析。我们先来看一下这段代码。
文件名:main.c

#include <stdio.h>

int function2(int c, int d) {
   
   
    int ret = c + d;
    return ret;
}

int function1(int a, int b) {
   
   
    int ret = a - b;
    ret = function2(a, ret);
    return ret;
}

int main(void) {
   
   
    int i = 3, j = 1;
    int ret = function1(i, j);
    return 0;
}

这段代码的函数调用关系为main()---->function1()---->function2()。

4.2 反汇编

通过反汇编可以帮助我们深入地分析函数的的调用过程,观察寄存和栈的使用情况,从而全面地理解ARM64函数调用的实现细节。下面我们就来看一下上面C代码所对应的汇编代码。

4.2.1 反汇编的方式

  1. 在线编译平台
    在线编译平台网址:https://godbolt.org/
    编译器: ARM64 gcc trunk
    编译器flag: -O0
    如下图所示
    在这里插入图片描述
  2. 使用交叉编译
    在Linux中使用交叉编译工具
    $ aarch64-linux-gnu-gcc main.c -O0 -o main.out
    $ aarch64-linux-gnu-objdump -j .text -ld -C -S main.out
    
  3. 在ARM64架构上直接反汇编
    gcc main.c -O0 -o main.out
    objdump -j .text -ld -C -S main.out
    

4.2.1 反汇编后的代码

000000000000074c <function2>:
function2():
 74c:	d10083ff 	sub	sp, sp, #0x20
 750:	b9000fe0 	str	w0, [sp, #12]
 754:	b9000be1 	str	w1, [sp, #8]
 758:	b9400fe1 	ldr	w1, [sp, #12]
 75c:	b9400be0 	ldr	w0, [sp, #8]
 760:	0b000020 	add	w0, w1, w0
 764:	b9001fe0 	str	w0, [sp, #28]
 768:	b9401fe0 	ldr	w0, [sp, #28]
 76c:	910083ff 	add	sp, sp, #0x20
 770:	d65f03c0 	ret

0000000000000774 <function1>:
function1():
 774:	a9bd7bfd 	stp	x29, x30, [sp, #-48]!
 778:	910003fd 	mov	x29, sp
 77c:	b9001fe0 	str	w0, [sp, #28]
 780:	b9001be1 	str	w1, [sp, #24]
 784:	b9401fe1 	ldr	w1, [sp, #28]
 788:	b9401be0 	ldr	w0, [sp, #24]
 78c:	4b000020 	sub	w0, w1, w0
 790:	b9002fe0 	str	w
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值