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

news2024/11/16 0:45:34

一、引言

随着计算机技术的飞速发展,人们对计算机的性能要求越来越高,为了突破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存放栈指针
X30LR链接寄存器,在函数调用时存储函数的返回地址
X29FP栈帧指针寄存器,指向当前函数的栈帧
X19 ~X28被调用函数保存的寄存器。在子函数中使用时需要保存到栈中
X18通常被用作平台相关的寄存器
X17IP2临时寄存器或者第二个IPC(Intra-Procedure-Call)临时寄存器
X16IP1临时寄存器或者第一个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 反汇编的方式
4.2.1.1 在线编译平台  

在线编译平台网址:Compiler Explorer。

编译器: ARM64 gcc trunk
编译器flag: -O0
如下图所示

 4.2.1.2 使用交叉编译

在Linux中使用交叉编译工具

$ aarch64-linux-gnu-gcc main.c -O0 -o main.out
$ aarch64-linux-gnu-objdump -j .text -ld -C -S main.out

4.2.1.3 在ARM64架构上直接反汇编
gcc main.c -O0 -o main.out
objdump -j .text -ld -C -S main.out
4.2.2 反汇编后的代码
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	w0, [sp, #44]
 794:	b9402fe1 	ldr	w1, [sp, #44]
 798:	b9401fe0 	ldr	w0, [sp, #28]
 79c:	97ffffec 	bl	74c <function2>
 7a0:	b9002fe0 	str	w0, [sp, #44]
 7a4:	b9402fe0 	ldr	w0, [sp, #44]
 7a8:	a8c37bfd 	ldp	x29, x30, [sp], #48
 7ac:	d65f03c0 	ret

00000000000007b0 <main>:
main():
 7b0:	a9be7bfd 	stp	x29, x30, [sp, #-32]!
 7b4:	910003fd 	mov	x29, sp
 7b8:	52800060 	mov	w0, #0x3                   	// #3
 7bc:	b9001fe0 	str	w0, [sp, #28]
 7c0:	52800020 	mov	w0, #0x1                   	// #1
 7c4:	b9001be0 	str	w0, [sp, #24]
 7c8:	b9401be1 	ldr	w1, [sp, #24]
 7cc:	b9401fe0 	ldr	w0, [sp, #28]
 7d0:	97ffffe9 	bl	774 <function1>
 7d4:	b90017e0 	str	w0, [sp, #20]
 7d8:	52800000 	mov	w0, #0x0                   	// #0
 7dc:	a8c27bfd 	ldp	x29, x30, [sp], #32
 7e0:	d65f03c0 	ret
 7e4:	d503201f 	nop
 7e8:	d503201f 	nop
 7ec:	d503201f 	nop

这部分汇编代码对应的函数调用关系为main()---->function_1()---->function_2()。

注意: 在C语言程序的执行过程中,程序的入口并不是从main()函数开始的。因为在程序执行之前,底层操作系统需要完成一些初始化工作。具体来说,在程序开始执行之前,操作系统会加载程序的可执行映像到内存中,在这个过程中,操作系统会调用一些特殊的启动例程(startup routine)来执行一些必要的初始化工作,如设置栈指针、处理命令行参数等。这些启动例程通常定义在编译器提供的运行时库(runtime library)中,并在main()函数之前被调用执行。因此,在main()函数被调用之前,这些启动例程就已经完成了必要的初始化工作。

4.3 汇编代码分析

4.3.1 函数调用标准的注意点

在分析函数调用栈帧变化之前,ARM64的函数调用标准我们要确认以下几点:

  • 寄存器是唯一被所有过程(函数调用)共享的资源,虽然在给定时刻只有一个函数调用只在执行,但是我们仍然要确保当一个过程(caller - 调用者)调用另一个过程(callee - 被调用者)时,callee不会覆盖caller稍后会使用的寄存器值,callee必须保存这些寄存器的值,保证他们的值在 callee返回到caller 与 caller调用callee 的值是一样的。 对于ARM64架构 X18 - X28,X29(FP),x30(LR) 被划分为被调用者保存寄存器。

  • callee要想保证一个寄存器不变,要么就是根本不改变它,要么就是把原始值压入栈中,callee把原始值压入栈中就可以使用该寄存器了,返回到caller时,将其从栈中弹出,恢复该寄存器的值。

  • 函数的前8个参数用X0 - X7 寄存器传递,如果参数类型长度小于等于int,那么使用W0 - W7。

  • 函数的返回值保存在X0寄存器中,如果返回值类型长度小于等于int,那么使用W0。

  • 一个函数在分配自己的栈帧时,sp指针只扩大一次,刚进入函数的时候,就会一次性把所有需要的栈空间都申请出来。

  • 栈指针为16字节对齐,分配栈空间的时候以16字节为单位对齐。

  • 所有的函数调用栈都会组成一个单链表。

  • 每个栈由两个地址来构成这个链表,这两个地址都是64位宽的,并且它们都位于栈的底部。

    低地址存放: 指向上一个栈帧(父函数的栈帧)的栈基地址FP,类似于链表的prev指针。我们把这个地址称为P_FP(Previous FP),以区别于处理器内部的FP寄存器。

  • 高地址存放: 当前函数的返回地址,也就是进入该函数时LR的值,我们把这个地址称为P_LR(Previous LR)。

  • 处理器的FP和SP寄存器相同。在函数执行时,FP和SP寄存器会指向该函数栈空间的FP处。即栈底。

  • 函数返回时,ARM64处理器先把栈中的P_LR的值载入当前LR寄存器,然后再执行ret指令。

4.3.2 相关汇编指令说明

ARMv8系统架构是基于指令加载和存储的体系架构。在这种体系架构中,所有的数据处理都需要在通用寄存器中完成,而不能直接在内存中完成。因此需要把待处理的数据从内存加载到通用寄存器,然后进行数据处理,最后把结果写入内存中。

LDR  //把存储器地址中的数据加载到目标寄存器中
STR  //把源寄存器的数据存储到存储器地址中
LDP  //加载寄存器对根据基寄存器值和立即数偏移量计算地址,从内存加载两个32位字或两个64位双字,并将它们写入两个寄存器。
STP  //存储寄存器对根据基寄存器值和立即数偏移量计算地址,并从两个寄存器将两个32位字或两个64位双字存储到计算的地址。
BL   //实现函数跳转操作,带返回地址的调跳转指令。BL指令将返回地址设置到LR(X30寄存器)中,保存的值为调用BL指令的当前PC值加上4。
RET  //从子程序中返回到调用该子程序的主程序
SUB  //将两个操作数相减,并将结果存储在目的操作数中
ADD  //将两个操作数相加,并将结果存储在目的操作数中
NOP  //不执行任何操作。作用为占位,延时或无操作

注意: 上面汇编代码第一列代表汇编代码在内存中的地址,第二列表示的是具体的 ARM 汇编指令的16进制。
例:a9be7bfd 等同于 stp x29, x30, [sp, #-32]!

4.3.3 main函数栈变化过程

main函数开辟自己的栈空间,同时保存caller的FP和LR

当代码执行到main函数时,由于main函数不是叶子函数,而main函数要使用x29(FP), x30(LR)寄存器,需要将这两个寄存器的值保存到当前栈,以便返回时恢复。

7b0:	a9be7bfd 	stp	x29, x30, [sp, #-32]!
# 注释
#stp: 将两个寄存器的值一起存储到内存中。
# x29,x30 要存储的寄存器
# [sp, #-32] 将数据存储到以sp为基地址、偏移量为-32的内存位置。
# sp:栈指针寄存器,用于指示当前栈顶的位置
# #-32: 表示偏移量为-32,即向下偏移32个字节 
# !表示在执行存储操作后,将sp寄存器更新为新的地址。

这条指令的作用是将x29和x30寄存器的值压入栈中,存储在sp寄存器指向的位置减32个字节的地址处。同时将sp寄存器的值减少32,使其指向新的栈顶位置。

重点:  为什么是给main函数的栈帧分配 32 字节的空间?
上文我们说了一个函数在分配自己的栈帧时,sp指针只扩大一次,刚进入函数的时候,就会一次性把所有需要的栈空间都申请出来。现在我们来解释一下。
返回地址(x30寄存器): 在函数调用过程中,需要保存被调用函数的返回地址,以便函数执行完后能正确返回到调用点。x30 寄存器通常用于存储返回地址,占用 8 字节。
函数的栈帧指针 (x29 寄存器): x29 寄存器通常用作帧基址指针(frame base pointer),用于访问函数的局部变量和参数。保存 x29 寄存器也需要 8 字节空间。
函数的局部变量: 函数内部定义的局部变量需要在栈上分配空间,main函数中有i,j,ret三个4字节的局部变量,需要12字节的空间。
函数调用时传递的参数: 在mian函数中,我们没有使用传参,所以不使用栈空间。

main函数的栈空间: x30 + x29 + (3 * int)= 8+8+12 = 28;因为栈指针为16字节对齐,分配栈空间的时候以16字节为单位对齐。所以申请的栈空间为16的倍数,也就是需要申请32字节大小的栈空间。

 程序在内存中分布区域如下图:

 我们主要对stack区进行分析,下面我们用图来理解stp x29, x30, [sp, #-32]!这个指令执行的操作。

SP是当前函数栈指针,指向栈顶,栈顶存储的FP寄存器的值为当前函数(mian)的栈帧基地址,同时也相当于main函数的caller函数的栈顶。FP寄存器是当前函数栈帧指针,指向当前函数(main)的栈帧基地址。

更新栈帧寄存器FP

7b4:	910003fd 	mov	x29, sp
#注释
#将CPU栈指针寄存器sp的值复制到x29中。

对当前函数(main)来说,执行FP = SP。将当前SP的值赋给FP寄存器,使FP指向当前函数(main)的栈帧基地址。每个有caller和callee的函数都要执行该操作。

main函数的局部变量依次入栈保存

7b8:	52800060 	mov	w0, #0x3                   	// #3
7bc:	b9001fe0 	str	w0, [sp, #28]
7c0:	52800020 	mov	w0, #0x1                   	// #1
7c4:	b9001be0 	str	w0, [sp, #24]
#注释
# mov w0, #0x3: 将立即数 0x3(十进制 3)加载到 w0 寄存器中。w0 是 64位 x0 寄存器的低32位。
# str w0, [sp, #28]:指令将 w0 寄存器的值(3)存储到以 sp 为基地址、偏移量为 28 的内存地址处。
# mov w0, #0x1:将立即数 0x1(十进制 1)加载到 w0 寄存器中
# str w0, [sp, #24]:将 w0 寄存器的值(1)存储到以 sp 为基地址、偏移量为 24 的内存地址处。

 将当前函数的局部变量,依次入栈保存。

将局部变量的值读取到寄存器中准备传参,并跳转到子函数

 7c8:	b9401be1 	ldr	w1, [sp, #24]
 7cc:	b9401fe0 	ldr	w0, [sp, #28]
 7d0:	97ffffe9 	bl	774 <function1>
 #注释
 # ldr	w1, [sp, #24]: 当前栈帧的偏移量为 24 的位置读取一个 32 位的值,并将其加载到 w1 寄存器中。把j = 1加载到寄存器w1
 # ldr	w0, [sp, #28]: 从当前栈帧的偏移量为 28 的位置读取一个 32 位的值,并将其加载到 w0 寄存器中。把i = 3加载到寄存器w0
 # bl	774 <function1>:调用function1函数,并保存好当前函数的返回地址,使得function1执行完毕后能够正确的返回到调用点。

 在函数调用过程中(bl),CPU会自动将返回地址保存到x30寄存器中,供函数返回时使用。

将子函数返回值赋值给局部变量

7d4:	b90017e0 	str	w0, [sp, #20]
# 注释
# r	w0, [sp, #20]:将 w0 寄存器中的值写入到当前栈帧的偏移量为 20 的位置。
因为函数返回值默认保存在W0寄存器,所以w0的值就是子函数的返回值,将子函数返回值赋值给局部变量ret并保存在当前栈帧的偏移量为 20 的位置。

 main函数准备返回值

7d8:	52800000 	mov	w0, #0x0                   	// #0
#注释
# 这条指令将立即数 0 加载到寄存器 w0 中。用于将函数的返回值设置为 0。

 main函数将返回值0放在寄存器w0,在ret返回时给caller。

main函数释放栈,从栈上恢复FP和LR寄存器,并返回

 7dc:	a8c27bfd 	ldp	x29, x30, [sp], #32
 7e0:	d65f03c0 	ret
 7e4:	d503201f 	nop
 7e8:	d503201f 	nop
 7ec:	d503201f 	nop
 #注释
 # ldp	x29, x30, [sp], #32: 从栈中加载 x29 和 x30 寄存器的值,并将 sp 寄存器增加 32。x29是帧指针,x30 是链接寄存器(存储返回地址)。这条指令用于恢复函数调用之前的栈帧状态。
 # 7e0:	d65f03c0 	ret: 用于函数返回,它从 x30 寄存器中取出返回地址,并跳转到该地址继续执行。
 # nop:	不执行任何操作,只是占用一些指令执行周期。它们通常用于对齐代码

当从main()返回时,栈指针会调整回父函数的栈顶,于是main()的栈空间就被释放了。

4.3.4 function1函数栈变化过程

由于function1函数不是叶子函数,所以function1函数要使用x29,x30寄存器,而此时x29,x30的值是caller者函数main()的值,因此要保存起来。function1函数为自己分配栈空间,在其caller main函数的底部,栈向下生长。

函数function1为自己分配栈空间,并保存LR和FP到栈顶

 774:	a9bd7bfd 	stp	x29, x30, [sp, #-48]!
 # 注释
 # stp: 将两个寄存器的值一起存储到内存中。
 # x29,x30 要存储的寄存器
 # [sp, #-48] 将数据存储到以sp为基地址、偏移量为-48的内存位置。
 # sp:栈指针寄存器,用于指示当前栈顶的位置
 # #-48: 表示偏移量为-48,即向下偏移48个字节 
 # !表示在执行存储操作后,将sp寄存器更新为新的地址。

 这条指令的作用是将x29和x30寄存器的值压入栈中,存储在sp寄存器指向的位置减32个字节的地址处。同时将sp寄存器的值减少48,使其指向新的栈顶位置。

函数function1更新栈帧FP寄存器

778:	910003fd 	mov	x29, sp
#注释
#将CPU栈指针寄存器sp的值复制到x29中。

 对当前函数(function1)来说,执行FP = SP。将当前SP的值赋给FP寄存器,使FP指向当前函数(function1)的栈顶。每个有caller和callee的函数都要执行该操作。

 function1函数保存寄存器传参的值

从caller(main)传过来的参数保存在w0和w1,接下来需要用到这两个寄存器。因此,先将传参保存到栈上。

 77c:	b9001fe0 	str	w0, [sp, #28]
 780:	b9001be1 	str	w1, [sp, #24]
 #注释
 #str	w0, [sp, #28]:将w0 寄存器中的值写入到当前栈帧的偏移量为 28 的位置。
 #str	w1, [sp, #24]:将 w1 寄存器中的值写入到当前栈帧的偏移量为 24 的位置。

 先保存w0,后保存w1。先存高地址,后存低地址。压栈按照从高到低顺序(栈底->栈顶)

function1函数将运算结果赋值给局部变量

784:	b9401fe1 	ldr	w1, [sp, #28]
788:	b9401be0 	ldr	w0, [sp, #24]
78c:	4b000020 	sub	w0, w1, w0
790:	b9002fe0 	str	w0, [sp, #44]
#注释
# ldr	w1, [sp, #28]:从栈指针 (sp) 偏移 28 字节的地址处加载一个 32 位的值到寄存器 w1 中。也就是将3加载到w1。
# ldr	w0, [sp, #24]:从栈指针 (sp) 偏移 24 字节的地址处加载一个 32 位的值到寄存器 w0 中。也就是将1加载到w0。
# sub	w0, w1, w0:从寄存器 w1 中减去寄存器 w0 中的值,并将结果存储到寄存器 w0 中。
# str	w0, [sp, #44]: 将寄存器 w0 中的值存储到栈指针 (sp) 偏移 44 字节的地址处。
为局部变量ret分配空间,存放计算结果。ret作为funb函数的第一个局部变量,放在栈底的位置。

function1函数为function2准备传参,并跳转到function2

794:	b9402fe1 	ldr	w1, [sp, #44]
798:	b9401fe0 	ldr	w0, [sp, #28]
79c:	97ffffec 	bl	74c <function2>
#注释
# ldr w1, [sp, #44]:从栈指针 (sp) 偏移 44 字节的地址处加载一个 32 位的值到寄存器 w1 中。即把2加载到寄存器w1中。
# ldr w0, [sp, #28]: 从栈指针 (sp) 偏移 28 字节的地址处加载一个 32 位的值到寄存器 w0 中。即把3加载到寄存器w0中。
# bl	74c <function2>: 调用function1函数,并保存好当前函数的返回地址,使得function1执行完毕后能够正确的返回到调用点。

 在函数调用过程中(bl),CPU会自动将返回地址保存到x30寄存器中,供函数返回时使用。

function1函数处理function2返回结果

7a0:	b9002fe0 	str	w0, [sp, #44]
#注释
#str w0, [sp, #44]:将寄存器 w0 中的数据存储到栈指针 sp 偏移 44 字节的地址处。

子函数function2执行结束后,将返回值保存到寄存器W0。function1函数将返回结果保存到ret处。

function1函数准备返回结果

7a4:	b9402fe0 	ldr	w0, [sp, #44]
#注释
# ldr	w0, [sp, #44]:从栈指针 sp 偏移 44 字节的地址处将数据加载到寄存器 w0 中,

function1函数在返回前,将返回结果存放在w0寄存器。

function1在返回前,恢复mian的FP和LR

7a8:	a8c37bfd 	ldp	x29, x30, [sp], #48
7ac:	d65f03c0 	ret
#注释
# ldp	x29, x30, [sp], #48:从内存地址 [sp] 开始,加载 64 位的值到寄存器 x29 和 x30 中,在加载完成后,栈指针 sp 会自动增加 48 字节。
# ret:将程序的执行流程返回到链接寄存器 (x30) 所保存的地址处。

4.3.5 function2函数栈变化过程

function2函数为自己分配栈

 74c:	d10083ff 	sub	sp, sp, #0x20
 #注释
 # sub	sp, sp, #0x20:将栈指针 sp 寄存器的值减小 32 个字节,将减小后的值重新存储回 sp 寄存器。

 由于function2函数为叶子函数,没有callee。不需要修改FP和LR寄存器的值,因此无需保存这两个寄存器。

function2函数保存寄存器传参的值

750:	b9000fe0 	str	w0, [sp, #12]
754:	b9000be1 	str	w1, [sp, #8]
#注释
# str	w0, [sp, #12]: 将 w0 寄存器的值存储到栈上偏移量为 12 字节的位置。也就是将3存储到栈上偏移量为 12 字节的位置
# str	w1, [sp, #8]:  将 w1 寄存器的值存储到栈上偏移量为 8 字节的位置。也就是将2存储到栈上偏移量为 8 字节的位置

function2中给局部变量ret分配空间

758:	b9400fe1 	ldr	w1, [sp, #12]
75c:	b9400be0 	ldr	w0, [sp, #8]
760:	0b000020 	add	w0, w1, w0
764:	b9001fe0 	str	w0, [sp, #28]
# 注释
# ldr	w1, [sp, #12]:从内存地址 sp + 12 处加载数据到寄存器 w1。
# ldr	w0, [sp, #8]: 从内存地址 sp + 8 处加载数据到寄存器 w0。
# add	w0, w1, w0: 将 w1 和 w0 的值相加,结果存储到 w0 寄存器中。
# str	w0, [sp, #28]:	将 w0 的值存储到内存地址 sp + 28 处,也就是ret的值。

function2函数准备返回值到w0,释放自己的栈

 768:	b9401fe0 	ldr	w0, [sp, #28]
 76c:	910083ff 	add	sp, sp, #0x20
 770:	d65f03c0 	ret
 # 注释
 # ldr w0, [sp, #28]: 将之前存储在栈上偏移量为 28 字节的位置的值加载到 w0 寄存器中。
 # add sp, sp, #0x20: 将 sp 加上 0x20,相当于释放掉之前在函数开始时分配的 32 字节栈空间。
 # ret:用于从函数中返回,这条指令将控制权返回到调用该函数的代码处	

将返回值保存到w0寄存器,并释放到function2函数的栈空间。

5. 总结

5.1 ARM64典型栈结构

  •  每个函数(同时具有caller和callee)的栈结构格式

  • 栈顶保存的是自己的栈帧基地址,即指向上一个栈帧的栈顶

  • 栈顶+8处保存的是LR寄存器的值,也就是自己retun后要从哪里开始执行

  • 然后保存的是传参的值,从传参n到1,从低往高保存

  • 然后保存局部变量的值

  • 到上一个函数的FP为止,函数的栈结束

  • 这样构成了一个完整的栈帧

  •  每个函数(同时具有caller和callee)进入后典型的栈操作

  • 首先会将caller的FP(也就是当前函数的栈帧基地址)保存到栈的顶部(SP+0)

  • 然后,将LR寄存器(返回地址)保存在自己的栈(SP+8)

  • 函数总会执行FP=SP操作。因此,对arm64来说,当前函数的FP=SP

  • 注意: 如果函数不具有caller和callee,不会执行FP=SP操作

  •  每个函数(假设同时具有caller和callee)返回前典型的栈操作

  • 将当前栈中保存的LR赋值给LR寄存器,然后ret

5.2 应用场景

FP和LR是两个很重要的寄存器,根据这两个寄存器就可以反推出所有函数的调用栈。

5.2.1 根据当前函数的FP,推断函数调用栈
  •  关键寄存器
     
  • FP寄存器和SP寄存器

  •  关键规则

  • 所有函数调用栈都会组成一个链表。

  • 每个栈有两个地址来构成这个链表。两个64位宽的地址。

  • 低地址(栈顶)存放了当前函数的栈帧基地址,也就是caller函数的栈顶,该函数栈顶存放了该函数的栈帧基地址,这种操作类似于链表的prev指针。

  • 高地址存放了LR寄存器,当前函数的返回地址。

  •  示例

    已知如下图所示的function2执行时寄存器的值,我们如何知道函数调用关系以及调用栈?

执行步骤
gcc -g main.c -o a.out
gdb a.out
b function2
run 
info registers

 

  •  规律分析

  • 由于function2函数为叶子函数,没有callee。不需要修改FP和LR寄存器的值,因此无需保存这两个寄存器。根据当前function2函数执行时的FP(x29)值,我们可以知道function2函数的栈帧基地址,根据当前function2函数执行时的SP值,我们可以知道function2函数的栈顶地址。

  • 根据当前function2函数执行时的FP值,我们可以知道function1函数的栈顶地址。

  • 从function1函数的栈顶地址位置获取上一级的栈帧指针FP。通过读取该地址的值,我们就知道了function1函数的栈帧基地址,同时也就知道了mian函数的栈顶地址。

  • 这样,层层往上递推我们就可以找到所有caller的栈帧基地址,即确定函数的调用关系。
    如下图所示:

 function2函数的栈顶为SP的值:0xfffffffffa60

function2函数的栈帧基地址为FP的值:0xfffffffffa80
function1函数的栈顶为function2函数执行时的栈帧基地址FP(x29)的值:0xfffffffffa80
function1函数的栈帧基地址为function2函数执行时FP(x29)指向的值:0xfffffffffab0

(gdb) x/gx 0xfffffffffa80
0xfffffffffa80: 0x0000fffffffffab0

 main函数的栈顶为function1函数执行时的栈帧基地址FP(x29)的值:0xfffffffffab0

main函数的栈帧基地址为function1函数执行时FP(x29)指向的值:0xfffffffffad0

(gdb) x/gx 0xfffffffffab0
0xfffffffffab0: 0x0000fffffffffad0

 验证
function2函数栈帧的基地址为 :0xfffffffffa80
当前栈帧是由位于地址 0xfffffffffab0 的上一个栈帧调用的(即function1函数的栈帧基地址)
上一个栈帧的Stack Pointer值为 0xfffffffffa80(即function1函数的栈顶地址)
 

Breakpoint 1, function2 (c=3, d=2) at main.c:4
4                    int ret = c + d;
(gdb) info frame
Stack level 0, frame at 0xfffffffffa80:
 pc = 0xaaaaaaaa0758 in function2 (main.c:4); saved pc = 0xaaaaaaaa07a0
 called by frame at 0xfffffffffab0
 source language c.
 Arglist at 0xfffffffffa60, args: c=3, d=2
 Locals at 0xfffffffffa60, Previous frame's sp is 0xfffffffffa80

function1函数栈帧的基地址为 :0xfffffffffab0
当前栈帧是由位于地址 0xfffffffffad0 的上一个栈帧调用的(即main函数的栈帧基地址)
上一个栈帧的Stack Pointer值为 0xfffffffffab0(即main函数的栈顶地址)
 

Breakpoint 1, function1 (a=3, b=1) at main.c:9
9                    int ret = a - b;
(gdb) info frame
Stack level 0, frame at 0xfffffffffab0:
 pc = 0xaaaaaaaa0784 in function1 (main.c:9); saved pc = 0xaaaaaaaa07d4
 called by frame at 0xfffffffffad0
 source language c.
 Arglist at 0xfffffffffa80, args: a=3, b=1
 Locals at 0xfffffffffa80, Previous frame's sp is 0xfffffffffab0
 Saved registers:
  x29 at 0xfffffffffa80, x30 at 0xfffffffffa88

main函数栈帧的基地址为 :0xfffffffffad0
上一个栈帧的Stack Pointer值为 0xfffffffffad0
 

(gdb) info frame
Stack level 0, frame at 0xfffffffffad0:
 pc = 0xaaaaaaaa07b8 in main (main.c:15); saved pc = 0xfffff7e798b8
 source language c.
 Arglist at 0xfffffffffab0, args: 
 Locals at 0xfffffffffab0, Previous frame's sp is 0xfffffffffad0
 Saved registers:
  x29 at 0xfffffffffab0, x30 at 0xfffffffffab8

 函数调用关系图

5.2.2 根据当前函数栈帧,获取所有caller的函数名,和函数Entry地址
  •  关键寄存器
    FP寄存器和LR寄存器
  • 从5.2.1我们知道,通过FP寄存器就能得到每个函数的栈帧基地址。而知道每个函数的栈帧基地址的条件下,可通过当前函数栈帧保存的LR获得当前函数Entry地址和函数名。

  •  理论分析

    通过当前函数栈帧所保存的LR间接获取到caller调用callee时的PC,从而根据符号表得到具体的函数名。在caller(调用者)调用callee(被调函数)时,LR指向被调函数callee的返回的下一条指令,通过LR指向的地址-4字节偏移就得到了跳转时的指令,包括被调用函数callee的入口地址。再通过符号表即可得到此入口函数对应的函数名。

  • caller跳转时的pc= *LR - 4 = *(FP + 8 )- 4

  •  示例
    函数调用关系为:mian()->function1()->function2()
    function2函数执行时寄存器的值如下图

 根据X30(LR)寄存器的值,得到function2的返回地址(0xaaaaaaaa07a0)。

(gdb) x/gx 0xaaaaaaaa07a0
0xaaaaaaaa07a0 <function1+44>: 0xb9402fe0b9002fe0

 function2返回地址-4 (0x0000aaaaaaaa079c)就是calller(function1)跳转到callee时的pc. 配合符号表,对该地址进行反汇编,就得到该地址处的跳转指令。跳转指令中就有callee function2函数的函数名。

第一列: 0x0000aaaaaaaa079c <+40>。表示PC地址 0x0000aaaaaaaa0774指向main函数入口地址+40个字节偏移地址。 0x0000aaaaaaaa079c地址减去偏移40 ,就得到了caller函数function1的入口地址。
第二列: bl 0xaaaaaaaa074c <function2> ,这一列对应的是PC地址处的指令,caller跳转到callee。这里我们可以看到跳转指令(bl) callee的函数名(function2)和callee的入口地址(0xaaaaaaaa074c )。

根据FP寄存器得到function2函数的栈帧基地址为0xfffffffffa80,即function1函数的栈顶地址为0xfffffffffa80
 

x29        0xfffffffffa80

 得到栈帧上保存的LR,0xfffffffffa80 + 8 = 0xfffffffffa88,得到function1的返回地址。

(gdb) x/gx  0xfffffffffa88
0xfffffffffa88: 0x0000aaaaaaaa07d4

 function1返回地址-4 (0x0000aaaaaaaa07d0)就是calller跳转到callee时的pc. 配合符号表, 对该地址进行反汇编,就得到该地址处的跳转指令。跳转指令中就有callee function1函数的函数名。

第一列: 0x0000aaaaaaaa07d0 <+32>。表示PC地址 0x0000aaaaaaaa07d0,指向main函数入口地址+24个字节偏移地址。> 0x0000aaaaaaaa07d0地址减去偏移32 ,就得到了caller函数main的入口地址。
第二列: bl 0xaaaaaaaa0774 <function1> ,这一列对应的是PC地址处的指令,caller跳转到callee。这里我们可以看到跳转指令(bl),callee的函数名(function1)和callee的入口地址(0xaaaaaaaa0774 )。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2078752.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

leetcode 3146 两个字符串的排列差

leetcode 3146 两个字符串的排列差 正文题目描述解题思路方法1 Python 处理字符串的思路方法2 正文 题目描述 解题思路 直接 for 循环遍历第一个字符串&#xff0c;在第二个字符串中找出第一个字符串中的对应字符的位置&#xff0c;做差&#xff0c;再取绝对值&#xff0c;最…

Clickhouse集群化(三)集群化部署

1. 准备 clickhouse支持副本和分片的能力&#xff0c;但是自身无法实现需要借助zookeeper或者clickhouse-keeper来实现不同节点之间数据同步&#xff0c;同时clickhouse的数据是最终一致性 。 2. Zookeeper 副本的写入流程 没有主从概念 平等地位 互为副本 2.1. 部署zookeep…

高效能低延迟:EasyCVR平台WebRTC支持H.265在远程监控中的优势

TSINGSEE青犀视频EasyCVR视频汇聚平台在WebRTC方面确实支持H.265编码&#xff0c;尽管标准的WebRTC API在大多数浏览器中默认并不支持H.265&#xff08;也称为HEVC&#xff0c;高效视频编码&#xff09;编码。EasyCVR平台通过一系列创新的技术手段&#xff0c;实现了在WebRTC协…

区块链应用,密码学会议书籍推荐以及隐私保护知识整理

基于区块链技术的安全多方计算项目示例 1. iCube——全球首个安全多方计算区块链金融项目 iCube团队通过与美国普渡大学区块链人工智能实验室深度合作&#xff0c;实现了区块链的安全多方计算。iCube建立了面向信息的终极抽象基础层和基于个人工智能的算法模型层&#xff0c;…

互联网盲盒小程序,提高企业市场竞争力

盲盒作为一种休闲娱乐的方式&#xff0c;受到了大众的追捧&#xff0c;各大消费者争相购买&#xff0c;市场一时火热非凡&#xff01; 随着互联网电商的出现&#xff0c;盲盒也开始在线上发展&#xff0c;当消费者距离盲盒门店较远或者没有时间下&#xff0c;就可以在小程序上…

Linux:Socket网络编程

目录 1. 理解源 IP 地址和目的 IP 地址 2&#xff1a;认识端口号 3&#xff1a;端口号范围划分 4&#xff1a;理解源端口号和目的端口号 5&#xff1a;理解Socket(套接字) 6&#xff1a;两个传输协议 &#xff08;TCP/UDP&#xff09; 6.1&#xff1a;User Datagram Prot…

重磅!尤文图斯携手Fortinet打造足球界的网络安全堡垒

近日&#xff0c;尤文图斯足球俱乐部与推动网络与安全融合的全球网络安全领导者 Fortinet&#xff08;NASDAQ&#xff1a;FTNT&#xff09;正式宣布建立合作伙伴关系&#xff0c;并签署了一项为期至2026年的赞助协议。在此框架下&#xff0c;Fortinet荣膺尤文图斯未来两个赛季的…

深度学习/机器学习软件教学平台

1、基本介绍 机器学习与深度学习教学系统是基于业界应用广泛的在线机器学习和深度学习建模开发框架JupyterLab开发的&#xff0c;面向高校数据分析、机器学习、深度学习&#xff0c;以及人工智能相关专业教学和实训的教学系统。 2、系统特色 系统首页 系统主界面 在线实验界面…

LLM推理端实现

LLM推理端是什么 Large Language Model&#xff0c;大语言模型。典型代表ChatGPT。 推理端&#xff1a;模型训练出来后&#xff0c;用于模型应用和部署的interface。 推理端实现了本地环境中部署大语言模型。可以实现LLM的基本功能&#xff0c;包括生成文本、自动摘要、语言…

目标小程序和当前小程序主体必须为绑定在同一0pen下的相关或关联主体

第一步&#xff1a;登录微信开放平台->账号中心->基本资料->关联主体信息->管理 第二步&#xff1a;管理员微信扫码二维码验证 第三步&#xff1a;管理员微信手机确认 第四步&#xff1a;管理员微信扫码确认后&#xff0c;即可“添加关联主体” 第五步&#xff1a;…

HarmonyOS开发之Gauge(环形图表)的简单使用

效果图&#xff1a; 代码块&#xff1a; Entry Component struct GaugePage {Builder descriptionBuilder() {Text(优).maxFontSize(30).minFontSize(30).fontColor("#fffa2a2d").fontWeight(FontWeight.Medium).width(100%).height("100%").textAlign(Te…

企业数据防泄密刻不容缓|八个措施拒绝泄密

随着信息技术的飞速发展和全球化的推进&#xff0c;企业数据的安全与保密性日益凸显其重要性。数据泄露不仅可能导致企业的核心资产流失、经济损失&#xff0c;还可能严重损害企业的声誉和市场竞争力。因此&#xff0c;建立一套完善的数据防泄密系统&#xff0c;已成为企业维护…

strace 简介和使用

简介 strace是个功能强大的Linux调试分析诊断工具&#xff0c;可用于跟踪程序执行时进程系统调用(system call)和所接收的信号&#xff0c;尤其是针对源码不可读或源码无法再编译的程序。 在Linux系统中&#xff0c;用户程序运行在一个沙箱(sandbox)里&#xff0c;用户进程不…

园区中的智慧用电管理

随着物联网、云平台等新兴信息科技的日益成熟&#xff0c;未来园区用电管理的发展将更加智能化、信息化。近年来&#xff0c;越来越多的能源互联网企业为园区用电管理提供了解决方案&#xff0c;文献[1]提出基于大数据技术设计开发的智能电表远程抄表系统替代传统的人工抄表方法…

常用标准化图像分辨率

常用标准化图像分辨率 标准化图像格式通常指的是将图像数据转换为统一的、标准化的格式&#xff0c;以便于存储、传输和处理。这种格式化过程可能包括调整图像的大小、颜色空间转换、归一化像素值等。 以下是常见标准化图像名称及分辨率&#xff08;在深度学习和计算机视觉领域…

衡石科技BI的API如何授权文档解析

授权说明​ 授权模式​ 使用凭证式&#xff08;client credentials&#xff09;授权模式。 授权模式流程说明​ 第一步&#xff0c;A 应用在命令行向 B 发出请求。 第二步&#xff0c;B 网站验证通过以后&#xff0c;直接返回令牌。 授权模式结构说明​ 接口说明​ 获取a…

【Qt学习】Qlabel 的核心属性 与 代码实例

文章目录 1. 介绍2. 显示文本3. 显示图片4. 设置文本对齐方式5. 设置自动换行、边距、缩进6. 设置伙伴 1. 介绍 在Qt学习的专题中&#xff0c;我们以及多次使用过QLabel&#xff0c;作用就是用于显示文本和图片&#xff1b; QLabel有以下 核心属性&#xff1a; 好的&#xf…

EasyCVR视频汇聚平台:巧妙解决WebRTC无法播放H.265视频的难题

随着科技的飞速发展&#xff0c;视频监控已经成为现代安全管理不可或缺的一部分&#xff0c;广泛应用于智慧工地、智慧煤矿、智慧工厂、智慧社区和智慧校园等多个领域。在这个过程中&#xff0c;视频编码技术和实时通信技术显得尤为重要。EasyCVR视频汇聚平台&#xff0c;凭借其…

Python中的Request请求重试机制例子解析

在Python中&#xff0c;使用Requests库实现请求的重试机制是一种常见的做法&#xff0c;尤其是在面对不稳定的网络环境或服务时。以下是一些实现重试机制的详细例子&#xff1a; 使用Sessions和HTTPAdapter&#xff1a;Requests库中的Session可以存储跨多个请求的共同设置&…

城市内涝积水监测预警系统应对强降雨挑战

近年以来&#xff0c;我国部分地区频繁遭遇强降雨或持续性降水挑战&#xff0c;城市排水系统不堪重负&#xff0c;内涝问题日益凸显&#xff0c;下穿隧道、立交桥下等低洼路段容易产生大量积水&#xff0c;防洪形势刻不容缓。 为有效应对强降雨带来的挑战&#xff0c;国信华源结…