一、引言
随着计算机技术的飞速发展,人们对计算机的性能要求越来越高,为了突破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 反汇编的方式
- 在线编译平台
在线编译平台网址:https://godbolt.org/。
编译器: ARM64 gcc trunk
编译器flag: -O0
如下图所示

- 使用交叉编译
在Linux中使用交叉编译工具$ aarch64-linux-gnu-gcc main.c -O0 -o main.out $ aarch64-linux-gnu-objdump -j .text -ld -C -S main.out - 在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

最低0.47元/天 解锁文章
3465

被折叠的 条评论
为什么被折叠?



