ARM64架构栈帧以及帧指针FP

文章目录

  • 前言
  • 一、arm64架构寄存器简介
    • 1.1 异常等级
    • 1.2 通用寄存器
    • 1.3 ARM64架构ABI
  • 二、ARM64架构函数调用标准
    • 2.1 AArch64过程调用标准简介
    • 2.2 通用寄存器中的参数
  • 三、demo分析
    • 3.1 main函数
    • 3.2 funb
    • 3.3 funa
  • 四、栈帧总结
  • 五、demo演示
  • 参考资料

前言

这篇文章描述了 x86_64架构栈帧以及帧指针FP,本文描述arm64架构下栈帧相关知识。

一、arm64架构寄存器简介

1.1 异常等级

在ARMv8中,执行发生在四个异常级别之一。在AArch64中,异常级别决定特权级别,其方式与ARMv7中定义的特权级别类似。Exception级别决定了特权级别,因此在ELn执行对应于特权PLn。类似地,一个异常级别的n值比另一个更大,则处于更高的异常级别。数字小于另一个的异常级别被描述为处于较低的异常级别。

异常级别提供了适用于ARMv8体系结构所有操作状态的软件执行权限的逻辑分离。它类似于并支持计算机科学中常见的分级保护域的概念。

以下是在每个异常级别上运行的软件的典型示例:

EL0 Normal user applications.
EL1 Operating system kernel typically described as privileged.
EL2 Hypervisor.
EL3 Low-level firmware, including the Secure Monitor.

如下图所示:
在这里插入图片描述

1.2 通用寄存器

AArch64执行状态提供31×64位通用寄存器,可在任何时候和所有异常级别访问。
每个寄存器是64位宽的,并且它们通常被称为寄存器X0-X30。

每个AArch64 64位通用寄存器(X0-X30)也具有32位(W0-W30)形式。

在AArch64位状态下使用X表示64位通用寄存器。可以使用W表示低32位的数据,W0-W30,W0表示X0寄存器的低32位数据,W30表示X30寄存器的低32位数据。
在这里插入图片描述
从W寄存器读取忽略相应X寄存器的高32位,并保持它们不变。写入W寄存器会将X寄存器的高32位设置为零。也就是说,将0xFFFFFFFF写入W0将X0设置为0x00000000FFFFFFFF。

1.3 ARM64架构ABI

ARM体系结构的应用程序二进制接口(ABI)指定了所有可执行本机代码模块必须遵守的基本规则,以便它们能够正确地协同工作。这些基本规则由特定编程语言(例如C++)的附加规则补充。除了ARM ABI指定的规则之外,单个操作系统或执行环境(例如,Linux)可以指定额外的规则来满足其自身的特定要求。

AArch64体系结构的ABI(应用二进制接口)包括以下几个组成部分:

(1)可执行和可链接格式(ELF – Executable and Linkable Format):AArch64的ELF规范定义了对象和可执行文件的格式。它定义了可执行文件、目标文件、共享库和核心转储文件的结构。ELF用于表示和管理AArch64体系结构中的二进制文件。

(2)过程调用标准(PCS – Procedure Call Standard):AArch64的过程调用标准ABI规范定义了子程序(函数)如何独立编写、编译和汇编以便协同工作的规则和约定。它规定了调用程序与被调用程序之间的契约,或者一个程序与其执行环境之间的契约,例如调用程序调用子程序时的义务或者栈布局等。过程调用标准覆盖了参数传递、寄存器使用、栈布局和函数调用规则等方面。

(3)DWARF:DWARF是一种广泛使用的标准化调试数据格式,也适用于AArch64架构。AArch64的DWARF基于DWARF 3.0,但包含一些特定于AArch64体系结构的附加规则。DWARF提供了调试所需的信息,如变量名、类型和源代码位置等。

(4)C和C++库:ARM Compiler ARM C和C++库以及浮点支持用户指南提供了关于ARM C和C++库的文档。这些库包括AArch64体系结构中C和C++编程语言的标准函数和支持。它们提供了常用函数、数据类型和语言特性的实现,对于在C和C++中开发软件是必要的。

(5)C++ ABI:C++应用二进制接口(ABI)标准针对ARM 64位架构描述了通用的C++ ABI。该C++ ABI标准定义了C++编程语言在ARM 64位架构(AArch64)上的特定ABI约定和规则。C++ ABI规定了C++编译器如何生成和与目标代码交互,包括对象布局、名称修饰、异常处理、虚函数调用等与C++语言特性和运行时行为相关的方面。

二、ARM64架构函数调用标准

2.1 AArch64过程调用标准简介

在AArch64过程调用标准(Procedure Call Standard)中,对寄存器的使用有一些规定,了解这些规定可以帮助你:

(1)编写更高效的C代码:了解参数如何传递给函数可以帮助你优化C代码的设计。根据规定,某些参数可能会以寄存器的形式传递,而不是通过堆栈。通过合理地利用寄存器,可以减少内存访问和数据传输,从而提高代码的执行效率。

(2)理解反汇编代码:反汇编代码是将机器码转换为可读的汇编指令的过程。了解AArch64过程调用标准可以帮助你理解反汇编代码中寄存器的使用方式。这样,你可以更好地分析代码的执行流程和数据传递。

(3)编写汇编代码:如果你需要编写AArch64汇编代码,了解过程调用标准对寄存器的使用是非常重要的。标准规定了哪些寄存器可以用于传递参数、保存临时数据以及返回值的传递方式等。遵循这些规定可以确保你的汇编代码与其他语言编写的代码正确地交互。

(4)调用其他语言编写的函数:不同语言编写的函数在参数传递和寄存器使用方面可能存在差异。了解AArch64过程调用标准可以帮助你正确地调用其他语言编写的函数,确保参数传递和寄存器使用的一致性,从而实现跨语言的函数调用。

2.2 通用寄存器中的参数

在函数调用的情况下,通用寄存器被分为四组:

(1)参数寄存器(X0-X7):
这些寄存器用于将参数传递给函数并返回结果。它们可以用作临时寄存器或调用者保存的寄存器变量,在函数内部和调用其他函数之间保存中间值。有8个寄存器可用于传递参数,相对于AArch32而言,减少了将参数溢出到堆栈的需求。

函数的前八个参数使用X0-X7寄存器传递。多于八个参数,后面的参数使用栈来传递。函数的返回值保存在X0寄存器中。

(2)调用者保存的临时寄存器(X9-X15):
如果调用者需要保留这些寄存器的值在调用其他函数之间保持不变,调用者必须在自己的堆栈帧中保存受影响的寄存器。被调用的子程序可以修改这些寄存器,而无需在返回给调用者之前保存和恢复它们。

(3)被调用者保存的寄存器(X19-X28):
这些寄存器在被调用的子程序中被保存在被调用者的堆栈帧中。只要在返回之前保存和恢复它们,被调用的子程序可以修改这些寄存器。

(4)具有特殊用途的寄存器(X8、X16-X18、X29、X30):
X8是间接结果寄存器。它用于传递间接结果的地址位置,例如函数返回一个大型结构体时。
X16和X17是IP0和IP1,用于函数内部调用的临时寄存器。它们可以由调用委托程序和类似代码使用,或者作为子程序调用之间的临时寄存器用于中间值。它们可被函数修改。委托程序是由链接器自动插入的小段代码,例如当分支目标超出分支指令的范围时。
X18是平台寄存器,保留给平台ABI使用。在不将特殊含义分配给X18的平台上,它是附加的临时寄存器。

X29是帧指针寄存器(FP)。
X30是链接寄存器(LR),函数的返回地址保存在X30寄存器中。

如下如所示:
在这里插入图片描述
X29、X30这两个寄存器是我们本章要讨论的两个重点寄存器。

三、demo分析

在分析函数调用栈帧变化前,函数调用标准以下几点要确认:
(1)寄存器是唯一被所有过程(函数调用)共享的资源,虽然在给定时刻只有一个函数调用只在执行,但是我们仍然要确保当一个过程(caller - 调用者)调用另一个过程(callee - 被调用者)时,callee不会覆盖caller稍后会使用的寄存器值,callee必须保存这些寄存器的值,保证他们的值在 callee返回到caller 与 caller调用callee 的值是一样的。

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

对于ARM64架构 X18 - X28,X29(FP),x30(LR) 被划分为被调用者保存寄存器。

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

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

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

(2)
A64指令集常见的内存加载指令是LDR指令,存储指令是STR指令。
LDR (register):
加载寄存器(Register)根据基寄存器值和偏移寄存器值计算地址,从内存加载一个字,并将其写入寄存器。可以选择性地对偏移寄存器值进行移位和扩展。

LDR 目标寄存器, <存储器地址>  //把存储器地址中的数据加载到目标寄存器中

STR (register):
存储寄存器(Register)根据基寄存器值和偏移寄存器值计算地址,并从寄存器将32位字或64位双字存储到计算的地址。

STR 源寄存器, <存储器地址>  //把源寄存器的数据存储到存储器地址中

(3)
A64指令集提供LDP和STP指令来实现多字节内存加载和存储。
LDP:
加载寄存器对根据基寄存器值和立即数偏移量计算地址,从内存加载两个32位字或两个64位双字,并将它们写入两个寄存器。
STP:
存储寄存器对根据基寄存器值和立即数偏移量计算地址,并从两个寄存器将两个32位字或两个64位双字存储到计算的地址。

(4)
A64指令集使用加载和存储指令来实现入栈和出栈操作。A32指令集提供了PUSH和POP指令来实现入栈和出栈操作,但是A64指令集已经去掉了PUSH和POP指令集。

(5)
A64指令集使用BL指令来实现函数跳转操作,带返回地址的调跳转指令。
BL:

Branch with Link branches to a PC-relative offset, setting the register X30 to PC+4. It provides a hint that this is a subroutine call.

BL指令将返回地址设置到LR(X30寄存器)中,保存的值为调用BL指令的当前PC值加上4。
BL指令为分支与链接(Branch with Link)指令,链接的意思是包含了调用者caller的地址,以便子函数返回到正确的地址。通常,caller把参数放到X0 - X7(W0 - W7)寄存器中,然后使用BL指令来跳转到子函数中,这里的子函数通常称为被调用者callee。调用者在调用BL指令是会把当前程序执行的地址(即PC值)加上4,保存到LR(X30寄存器)中,从而保证被调用者返回时能正确链接(返回)到BL指令的下一条指令。

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

c例程:

int funa(int a, int b)
{int ret = a + b;return ret;}int funb(int c, int d)
{int ret = c + d ;ret = funa(c, ret);return ret;
}int main(void)
{int i = 1, j = 2;int ret = funb(i,j);return 0;
}

反汇编:

000000000000071c <funa>:71c:   d10083ff        sub     sp, sp, #0x20720:   b9000fe0        str     w0, [sp, #12]724:   b9000be1        str     w1, [sp, #8]728:   b9400fe1        ldr     w1, [sp, #12]72c:   b9400be0        ldr     w0, [sp, #8]730:   0b000020        add     w0, w1, w0734:   b9001fe0        str     w0, [sp, #28]738:   b9401fe0        ldr     w0, [sp, #28]73c:   910083ff        add     sp, sp, #0x20740:   d65f03c0        ret0000000000000744 <funb>:744:   a9bd7bfd        stp     x29, x30, [sp, #-48]!748:   910003fd        mov     x29, sp74c:   b9001fe0        str     w0, [sp, #28]750:   b9001be1        str     w1, [sp, #24]754:   b9401fe1        ldr     w1, [sp, #28]758:   b9401be0        ldr     w0, [sp, #24]75c:   0b000020        add     w0, w1, w0760:   b9002fe0        str     w0, [sp, #44]764:   b9402fe1        ldr     w1, [sp, #44]768:   b9401fe0        ldr     w0, [sp, #28]76c:   97ffffec        bl      71c <funa>770:   b9002fe0        str     w0, [sp, #44]774:   b9402fe0        ldr     w0, [sp, #44]778:   a8c37bfd        ldp     x29, x30, [sp], #4877c:   d65f03c0        ret0000000000000780 <main>:780:   a9be7bfd        stp     x29, x30, [sp, #-32]!784:   910003fd        mov     x29, sp788:   52800020        mov     w0, #0x1                        // #178c:   b90017e0        str     w0, [sp, #20]790:   52800040        mov     w0, #0x2                        // #2794:   b9001be0        str     w0, [sp, #24]798:   b9401be1        ldr     w1, [sp, #24]79c:   b94017e0        ldr     w0, [sp, #20]7a0:   97ffffe9        bl      744 <funb>7a4:   b9001fe0        str     w0, [sp, #28]7a8:   52800000        mov     w0, #0x0                        // #07ac:   a8c27bfd        ldp     x29, x30, [sp], #327b0:   d65f03c0        ret7b4:   d503201f        nop

3.1 main函数

Linux一个C语言例程执行时,main函数前面还有其他函数执行,main函数不是第一个被调用执行的函数,从反汇编代码也可以看出来,这里我不过多解释,流程如下:

int main(int argc, char** argv)
shell-->fork() + execve() //execve将命令行参数传递给新程序-->_start()  //准备参数 argc,argv 和 envp-->__libc_start_main() //初始化运行环境-->main()

当执行到main函数时,由于main函数不是叶子函数,而main函数要使用x29, x30寄存器,而此时x29, x30值是caller者函数__libc_start_main()的值,因此要保存起来。

(1)第一行代码:main函数将x29, x30寄存器的值压入到自己的栈中。
这行代码保存了调用者保存的寄存器 x29 (帧指针寄存器 FP) 和 x30 (链接寄存器 LR) 到栈中。栈指针 sp 向下移动 32 字节。

首先会将caller的FP(栈帧地址)保存到栈的顶部(SP+0)。
然后,然后将LR寄存器(返回地址)保存在自己的栈(SP+8)。
函数总会执行FP=SP操作。因此,对arm64来说,当前函数的FP=SP。

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

 780:   a9be7bfd        stp     x29, x30, [sp, #-32]!

在这里插入图片描述

SP寄存器是当前函数栈指针,指向栈顶。
FP寄存器是当前函数栈帧指针,指向栈底。

(2)将sp寄存器的值赋值给x29寄存器(FP寄存器),即 FP = sp。
这行代码将栈指针 sp 的值复制给 x29 (帧指针寄存器 FP),以建立当前函数的堆栈帧。

 784:   910003fd        mov     x29, sp

在这里插入图片描述

(3)这些代码分别将常数值 1 和 2 存储到堆栈帧上的偏移为 20 和 24 的位置。

 788:   52800020        mov     w0, #0x1                        // #178c:   b90017e0        str     w0, [sp, #20]790:   52800040        mov     w0, #0x2                        // #2794:   b9001be0        str     w0, [sp, #24]

这里的局部变量都是 int 类型,使用32位寄存器:w0寄存器即可。
使用str指令将这两个局部变量加载到栈帧中。

STR (register)

(4)这些代码从堆栈帧上的偏移为 24 和 20 的位置加载存储的值到寄存器 w1 和 w0。
因为函数funb的参数是i,j,函数参数只有两个,用两个寄存器传递参数即可,由于 i,j 是 int 类型,使用寄存器 w1 和 w0传递即可。

 798:   b9401be1        ldr     w1, [sp, #24]79c:   b94017e0        ldr     w0, [sp, #20]

(5)这行代码调用了一个名为 “funb” 的函数,跳转到地址 744。
bl 指令将下一条指令的地址保存在LR寄存器中。

 7a0:   97ffffe9        bl      744 <funb>

(6)这行代码将函数 “funb” 的返回值存储到堆栈帧上的偏移为 28 的位置。
函数的返回值保存在X0寄存器中,ret 是 int类型,使用w0寄存器即可。

 7a4:   b9001fe0        str     w0, [sp, #28]

(7)这些代码将常数值 0 存储到寄存器 w0 中,并恢复调用者保存的寄存器 x29 和 x30 的值,然后执行返回指令,从函数中返回。

 7a8:   52800000        mov     w0, #0x0                        // #07ac:   a8c27bfd        ldp     x29, x30, [sp], #327b0:   d65f03c0        ret7b4:   d503201f        nop

main函数的栈帧布局如下图所示:
在这里插入图片描述

3.2 funb

由于funb函数不是叶子函数,而funb函数要使用x29, x30寄存器,而此时x29, x30值是caller者函数main()的值,因此要保存起来。funb函数为自己分配栈空间,在其caller main函数的底部,栈向下生长。
(1)
将 x29 和 x30 的值存储到内存中的 [sp, #-48] 处,并将堆栈指针 sp 减去 48。
将堆栈指针的值 sp 复制到 x29 寄存器中,保存函数的帧指针。FP = SP。

 744:   a9bd7bfd        stp     x29, x30, [sp, #-48]!748:   910003fd        mov     x29, sp

(2)
参数 c 和 参数 d 使用 寄存器 w0 和 寄存器 w1 保存。
将参数 c 的值 w0 存储到 [sp, #28] 处,即在堆栈中存储参数 c。
将参数 d 的值 w1 存储到 [sp, #24] 处,即在堆栈中存储参数 d。
从 [sp, #28] 处加载值到寄存器 w1,即加载参数 c。
从 [sp, #24] 处加载值到寄存器 w0,即加载参数 d。
将寄存器 w1 和寄存器 w0 的值相加,结果存储在寄存器 w0 中,即计算 ret = c + d。

 74c:   b9001fe0        str     w0, [sp, #28]750:   b9001be1        str     w1, [sp, #24]754:   b9401fe1        ldr     w1, [sp, #28]758:   b9401be0        ldr     w0, [sp, #24]75c:   0b000020        add     w0, w1, w0

(3)
寄存器 w0 的值存储到 [sp, #44] 处,即在堆栈中存储变量 ret 的值。
从 [sp, #44] 处加载值到寄存器 w1,即加载变量 ret 的值。
从 [sp, #28] 处加载值到寄存器 w0,即加载参数 c。
调用函数 funa,跳转到地址 71c 执行该函数。

函数funa的形参是两个 int 类型,使用 寄存器 w0 传递参数c,使用寄存器 w1 传递参数 ret

bl指令将下一条指令的地址保存在寄存器x30中。

 760:   b9002fe0        str     w0, [sp, #44]764:   b9402fe1        ldr     w1, [sp, #44]768:   b9401fe0        ldr     w0, [sp, #28]76c:   97ffffec        bl      71c <funa>

(4)
函数的返回值保存在寄存器w0中。
将函数 funa 的返回值寄存器w0存储到 [sp, #44] 处,即在堆栈中存储变量 ret 的值。
从 [sp, #44] 处加载值到寄存器 w0,即加载变量 ret 的值。

 770:   b9002fe0        str     w0, [sp, #44]774:   b9402fe0        ldr     w0, [sp, #44]

(5)
将 [sp] 处的值加载到寄存器 x29 和 x30 中,并将堆栈指针 sp 增加 48。
返回到调用该函数的位置。

 778:   a8c37bfd        ldp     x29, x30, [sp], #4877c:   d65f03c0        ret

funb函数的栈帧如下图所示:
在这里插入图片描述

3.3 funa

函数funa是叶子函数,不会在调用其他函数,因此不需要使用x29, x30寄存器,因此不需要保存这两个寄存器的值了。
(1)在堆栈上分配32个字节的空间,通过将栈指针(sp)减去0x20。

 71c:   d10083ff        sub     sp, sp, #0x20

(2)
将传入的两个参数w0和w1存储到堆栈中的特定位置([sp, #12]和[sp, #8])。
从堆栈中加载之前存储的参数值到寄存器w1和w0。
将w1和w0的值相加,结果存储到w0寄存器。
将w0的值存储到堆栈中的另一个位置([sp, #28])。
从堆栈中加载存储的结果值到w0寄存器。

 720:   b9000fe0        str     w0, [sp, #12]724:   b9000be1        str     w1, [sp, #8]728:   b9400fe1        ldr     w1, [sp, #12]72c:   b9400be0        ldr     w0, [sp, #8]730:   0b000020        add     w0, w1, w0734:   b9001fe0        str     w0, [sp, #28]738:   b9401fe0        ldr     w0, [sp, #28]

(3)
在堆栈上释放之前分配的32个字节的空间,通过将栈指针(sp)加上0x20。
返回函数,恢复程序计数器的值。

 73c:   910083ff        add     sp, sp, #0x20740:   d65f03c0        ret

函数funa的栈帧如下图所示:
在这里插入图片描述

四、栈帧总结

假设下列函数调用:

funb()
{func()
}funa()
{funb()
}main()
{funa()
}

main函数,funa函数,funb函数都不是叶子函数,其栈布局如下所示:
在这里插入图片描述
LR 和FP寄存器保存在每个函数栈帧的栈顶:
FP = SP + 0
LR = SP + 8
根据这两个寄存器就可以反推出所有函数的调用栈。

FP栈帧指针(X29)指向保存在栈上的上一个栈帧的帧指针。在它之后存储了保存的LR(X30)。链中的最后一个帧指针应设置为0。

知道FP寄存器就能得到每个函数的栈帧基地址。而知道每个函数的栈帧基地址的条件下,可通过当前函数栈帧保存的LR获得当前函数的Entry地址和函数名。
通过FP还可以知道上一级的FP(栈帧基地址)。

在ARM64体系结构中,函数调用栈以单链表形式组织,其中每个栈帧都包含两个地址,用于构建这个链表。这种链表通常被称为调用链或链式栈。

在链式栈中,每个栈帧都有两个64位宽的地址:
(1)低地址(栈顶)存放了指向上一个栈帧的基地址,通常使用FP(Frame Pointer)寄存器来保存。类似于链表中的prev指针,它指向上一个栈帧的基地址,以便在函数返回时回到调用者的上下文。

(2)高地址存放了LR(Link Register)寄存器的值,它保存了当前函数的返回地址。LR寄存器中的值指向了调用当前函数的下一条指令的地址。当函数执行完毕时,该地址将被用于恢复程序控制流,并返回到调用者的位置。

通过这种方式,每个栈帧都可以通过链表中的prev指针链接在一起,形成一个完整的函数调用链。当函数返回时,可以使用prev指针获取上一个栈帧的基地址,并利用LR寄存器中的返回地址将控制流传递给调用者。

ARM64栈回溯:
在AAPC64中,栈指针(SP)指向当前栈帧的顶部,其中包含了上一级函数的LR和FP寄存器现场。通过查看SP所指向的地址,可以找到保存的上一级函数的LR和FP寄存器值。

对于LR寄存器,根据(LR-4)可以找到上一级函数所在的地址,减去4是因为ARM64指令集中的跳转指令(例如BL)会将要跳转到的地址加上4。因此,在栈上保存的LR值实际上是要跳转到的下一条指令的地址,而不是当前指令的地址。所以,为了找到上一级函数所在的地址,需要减去4。

上一级函数的FP寄存器实际上等于上一级函数使用的栈顶地址。通过保存上一级FP寄存器现场的位置,可以在栈上找到上一级函数的栈帧。同样,该栈帧中也会保存更上一级函数的LR和FP寄存器现场,以此类推,形成函数调用链。

通过链式保存的方式,可以回溯整个函数的调用流程,从当前函数一直追溯到最外层的调用者。这种方式使得在函数返回时可以按照相反的顺序恢复各个函数的现场,并正确返回到调用者的位置。

五、demo演示

C语言示例:

int fund(int g, int h)
{return g + h;
}int func(int e, int f)
{int ret = e + f;ret = fund(e, ret);return ret;
}int funb(int c, int d)
{int ret = c + d;ret = func(c, ret);return ret;}int funa(int a, int b)
{int ret = a + b ;ret = funb(a, ret);return ret;
}int main(void)
{int i = 1, j = 2;int ret = funa(i,j);return 0;
}
(gdb) b main
(gdb) b funa
(gdb) b funb
(gdb) b func
(gdb) r

(1)
main:

(gdb) disassemble
Dump of assembler code for function main:......0x0000005555555810 <+32>:    bl      0x55555557b4 <funa>0x0000005555555814 <+36>:    str     w0, [sp, #28]......
x29            0x7ffffff400
x30            0x7ff7e5c110

(2)
funa:

(gdb) c
(gdb) disassemble
Dump of assembler code for function funa:......0x00000055555557dc <+40>:    bl      0x5555555778 <funb>0x00000055555557e0 <+44>:    str     w0, [sp, #44]......
(gdb) info registers
x29            0x7ffffff3d0
x30            0x5555555814

可以看到x30寄存器的值就是main函数 bl funa 下一条指令的地址。

根据x29寄存器得到funa栈帧基地址:

0x7ffffff3d0

读取该地址的值(x29寄存器FP存放了指向上一个栈帧的基地址):

(gdb) x/1xg 0x7ffffff3d0
0x7ffffff3d0:   0x0000007ffffff400

那么可以得到main函数的栈帧基地址:0x0000007ffffff400
这个值就等于执行main函数时,x29寄存器的值。

将main函数的栈帧基地址+8然后读取获取main的返回地址:
这里 + 8 的原因:LR = FP + 8

0x0000007ffffff400 + 8 = 0x0000007ffffff408
(gdb) x/1xg 0x0000007ffffff408
0x7ffffff408:   0x0000007ff7e5c110

main的返回地址:0x0000007ff7e5c110

将main的返回地址 - 4 就可以获取 BL main这条函数跳转指令的地址:
这里 - 4 的原因:执行BL指令时,将下一条指令的地址(即返回地址)写入X30寄存器中,这里我们已经获取到了返回地址,那么 -4 就获取到了 BL 指令的地址。

0x0000007ff7e5c110 - 4 = 0x0000007ff7e5c10c

那么其上一条调用main的指令地址就是0x0000007ff7e5c10c:

(gdb) x/i 0x0000007ff7e5c10c0x7ff7e5c10c <__libc_start_main+228>:        blr     x3
(gdb) x/2i 0x0000007ff7e5c10c0x7ff7e5c10c <__libc_start_main+228>:        blr     x30x7ff7e5c110 <__libc_start_main+232>:        bl      0x7ff7e71a40 <exit>

可以看到是__libc_start_main函数调用 main 函数。

(3)
funb:

(gdb) disassemble
Dump of assembler code for function funb:......0x00000055555557a0 <+40>:    bl      0x555555573c <func>0x00000055555557a4 <+44>:    str     w0, [sp, #44]......
(gdb) info registers
x29            0x7ffffff3a0
x30            0x55555557e0

可以看到x30寄存器的值就是 funa bl funb下一条指令的地址。

根据x29寄存器得到funb栈帧基地址:

0x7ffffff3a0

读取该地址的值(x29寄存器FP存放了指向上一个栈帧funa的基地址):

(gdb) x/1xg 0x7ffffff3a0
0x7ffffff3a0:   0x0000007ffffff3d0

这个值就等于执行funa函数时,x29寄存器的值。

将funa函数的栈帧基地址+8然后读取获取funa的返回地址:

0x0000007ffffff3d0 + 8 = 0x0000007ffffff3d8
(gdb) x/1xg 0x0000007ffffff3d8
0x7ffffff3d8:   0x0000005555555814

funa的返回地址:0x0000005555555814

将funa的返回地址 - 4 就可以获取 main BL funa这条函数跳转指令的地址:

0x0000005555555814- 4 = 0x0000005555555810

那么其上一条调用funa的指令地址就是0x0000005555555810:

(gdb) x/i 0x00000055555558100x5555555810 <main+32>:      bl      0x55555557b4 <funa>
(gdb) x/2i 0x00000055555558100x5555555810 <main+32>:      bl      0x55555557b4 <funa>0x5555555814 <main+36>:      str     w0, [sp, #28]

可以看到是main函数调用 funa 函数。

(4)
func:

(gdb) disassemble
Dump of assembler code for function func:......0x0000005555555764 <+40>:    bl      0x555555571c <fund>0x0000005555555768 <+44>:    str     w0, [sp, #44]......
(gdb) info registers
x29            0x7ffffff370
x30            0x55555557a4

可以看到x30寄存器的值就是 funb bl func下一条指令的地址。

根据x29寄存器得到func栈帧基地址:

0x7ffffff370

读取该地址的值(x29寄存器FP存放了指向上一个栈帧funb的基地址):

(gdb) x/1xg 0x7ffffff370
0x7ffffff370:   0x0000007ffffff3a0

这个值就等于执行funb函数时,x29寄存器的值。

将funb函数的栈帧基地址+8然后读取获取funb的返回地址:

0x0000007ffffff3a0 + 8 = 0x0000007ffffff3a8
(gdb) x/1xg 0x0000007ffffff3a8
0x7ffffff3a8:   0x00000055555557e0

funb的返回地址:0x00000055555557e0

将funb的返回地址 - 4 就可以获取 funaBL funb这条函数跳转指令的地址:

0x00000055555557e0 - 4 = 0x00000055555557dc

那么其上一条调用funb的指令地址就是0x00000055555557dc:

(gdb) x/i 0x00000055555557dc0x55555557dc <funa+40>:      bl      0x5555555778 <funb>
(gdb) x/2i 0x00000055555557dc0x55555557dc <funa+40>:      bl      0x5555555778 <funb>0x55555557e0 <funa+44>:      str     w0, [sp, #44]

可以看到是funa函数调用 funb 函数。

参考资料

https://blog.csdn.net/heshuangzong/article/details/126911474
https://blog.csdn.net/rikeyone/article/details/105636895
https://blog.csdn.net/GetNextWindow/article/details/126444049

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

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

相关文章

智过网:一建36岁好不好找工作?能干什么?

在职业发展的道路上&#xff0c;许多人在不同的年龄阶段都会面临不同的挑战和机遇。对于36岁这一年龄阶段的人来说&#xff0c;如果已经通过了国家一级建造师&#xff08;一建&#xff09;的考试&#xff0c;那么他们在找工作方面会有怎样的前景呢&#xff1f;又能从事哪些职业…

气象预测新篇章:Python人工智能的变革力量

Python是功能强大、免费、开源&#xff0c;实现面向对象的编程语言&#xff0c;在数据处理、科学计算、数学建模、数据挖掘和数据可视化方面具备优异的性能&#xff0c;这些优势使得Python在气象、海洋、地理、气候、水文和生态等地学领域的科研和工程项目中得到广泛应用。可以…

20.变量的使用方式和注意事项

文章目录 一、变量的用法二、变量的注意事项三、总结 一、变量的用法 代码示例 public static void main(String[] args) {//1.基本用法// 定义变量&#xff0c;再进行输出int a 10;System.out.println(a);// 10System.out.println(a);// 10//2.变量参与计算int b 30;int c …

代码随想录学习Day 21

回溯算法理论基础 回溯法又叫回溯搜索法。回溯是递归的副产品&#xff0c;有递归就会有回溯&#xff0c;回溯操作一般出现在递归函数的下面。回溯函数 递归函数。回溯法的本质是穷举。 回溯法解决的问题&#xff1a; 组合问题&#xff1a;N个数里面按一定规则找出k个数的集…

【超图 SuperMap3D】【基础API使用示例】51、超图SuperMap3D - 绘制圆|椭圆形面标注并将视角定位过去

前言 引擎下载地址&#xff1a;[添加链接描述](http://support.supermap.com.cn/DownloadCenter/DownloadPage.aspx?id2524) 绘制圆形或者椭圆形效果 核心代码 entity viewer.entities.add({// 圆中心点position: { x: -1405746.5243351874, y: 4988274.8462937465, z: 370…

小狐狸JSON-RPC:钱包连接,断开连接,监听地址改变

detect-metamask 创建连接&#xff0c;并监听钱包切换 一、连接钱包&#xff0c;切换地址&#xff08;监听地址切换&#xff09;&#xff0c;断开连接 使用npm安装 metamask/detect-provider在您的项目目录中&#xff1a; npm i metamask/detect-providerimport detectEthereu…

vue2的孙子传值给爷爷,爷爷传值给孙子

孙---->爷 在爷爷组件&#xff0c;给父亲组件绑定一个方法 定义一个方法用来接受传来的值 在父亲组件中 给孙子组件绑定v-on“$listeners” 在孙子组件 在特定掉件触发下&#xff0c;将值穿过去&#xff0c;注意&#xff1a;这里的this.$emit的名字要跟爷爷绑定的一…

R语言赋值符号<-、=、->、<<-、->>的使用与区别

R语言的赋值符号有&#xff1c;-、、-&#xff1e;、&#xff1c;&#xff1c;-、-&#xff1e;&#xff1e;六种&#xff0c;它们的使用与区别如下: <-’&#xff1a;最常用的赋值符号。它将右侧表达式的值赋给左侧的变量&#xff0c;像一个向左的箭头。例如&#xff0c;x …

【大数据运维】minio 常见shell操作

文章目录 1. 安装2. 入门操作3. 命令帮助 1. 安装 下载 https://dl.min.io/client/mc/release/linux-amd64/ 赋权与使用 cp mc /usr/bin && chmod x /usr/bin/mc ./mc --help 2. 入门操作 # 添加minio到mc mc config host add minio_alias_name endpoint_adress …

超市收银系统-亿发智能收银,引领线上线下一体化新零售管理

社交新零售的传播核心在于移动互联网。从信任关系的角度来看&#xff0c;移动互联网为用户之间搭建了沟通的桥梁&#xff0c;促进了人们之间更频繁的交流&#xff0c;从而建立了信任关系。从场景的角度来看&#xff0c;移动互联网使得用户之间的沟通更加多样化&#xff0c;包括…

Tomcat项目创建 以及 在IDEA当中集成Tomcat

一: 有关Tomcat的WEB项目创建 TOMCAT项目的创建有两种方式, 第一种是利用骨架进行创建, 第二种是利用填补进行相应的创建, 不适用骨架进行创建 ,在这里主要聊第二种 (使用IDEA版本为2023) 1. 创建MAVEN项目, 非骨架形式 2.在相应的pom文件当中设置打包方式 为 war包的打包形…

基于Java在线考试系统系统设计与实现(源码+部署文档)

博主介绍&#xff1a; ✌至今服务客户已经1000、专注于Java技术领域、项目定制、技术答疑、开发工具、毕业项目实战 ✌ &#x1f345; 文末获取源码联系 &#x1f345; &#x1f447;&#x1f3fb; 精彩专栏 推荐订阅 &#x1f447;&#x1f3fb; 不然下次找不到 Java项目精品实…

HarmonyOS 应用开发之任务(Mission)与启动模式

如前文所述&#xff0c;一个UIAbility实例对应一个任务。UIAbility实例个数与UIAbility配置的启动模式有关。在FA模型下&#xff0c;通过config.json配置文件中的“launchType”属性配置&#xff1b;在Stage模型下&#xff0c;通过 module.json5配置文件 中的“launchType”属性…

兆欧表揭秘:到底是摇表还是电器?

兆欧表&#xff0c;又称摇表&#xff0c;是一种用于测量电气设备、电缆、电机绕组等绝缘电阻的测试工具。虽然现代兆欧表采用电动型和电池供电等多种形式&#xff0c;但其基本功能和用途保持一致。早期的兆欧表多采用手动机械式设计&#xff0c;通过手柄摇动发电来提供所需的高…

OpenCV4.9关于矩阵上的掩码操作

返回&#xff1a;OpenCV系列文章目录&#xff08;持续更新中......&#xff09; 上一篇:如何使用OpenCV扫描图像、查找表和时间测量 下一篇:OpenCV4.9的是如何进行图像操作 引言&#xff1a; 矩阵上的掩码操作非常简单。这个想法是&#xff0c;我们根据掩码矩阵&#xff08…

海外媒体发稿:3种媒体宣发套餐内容推广方法

现如今&#xff0c;伴随着信息技术的不断进步和推广&#xff0c;新闻媒体宣发变成企业品牌推广的重要手段之一。为了方便让新闻信息新闻资讯传递给目标群体&#xff0c;公司一般会选择不同的套餐内容和推广方法。下面我们就详细介绍3种新闻资讯新闻媒体宣发套餐内容推广方法。 …

kubernetes(K8S)学习(九):K8S之日志监控

K8S之日志监控 一、Log and Monitor1.1 Log1.1.1 容器级别1.1.2 Pod级别1.1.3 组件服务级别1.1.4 LogPilot ES Kibana 1.2 Monitor1.2.1 Prometheus简介1.2.2 Prometheus架构1.2.3 Prometheus知识普及1.2.4 数据采集1.2.5 Prometheus Grafana 二、Trouble Shooting&#xff…

项目Weblogic切换Tomcat-包含数据源配置

目录 准备工作 修改Tomcat配置 Tomcat数据源加密 解密 加密 部署 问题解决 1.执行启停脚本时候&#xff0c;爆出&#xff1a;Cannot find ./catalina.sh The file is absent or does not have... 2.org.apache.catalina.core.StandardService.initInternal Failed to …

UE中:200W个对象单场景实现(待更新)

实现背景&#xff1a;需要显示城市级的行人以及地理市级范围内的路灯的状态&#xff0c;行人需要有状态以及位置的更新&#xff0c;路灯只需要状态的更新&#xff0c;二者都不需要物理 方案1概述&#xff1a;Niagara粒子系统实现 实际效果展示 UE5 集群模拟&#xff08;20W&a…

Oracle 控制文件详解

1、控制文件存储的数据信息 1&#xff09;数据库名称和数据库唯一标识符&#xff08;DBID) 2&#xff09;创建数据库的时间戳 3&#xff09;有关数据文件、联机重做日志文件、归档重做日志文件的信息 4&#xff09;表空间信息 5&#xff09;检查点信息 6&#xff09;日志序列号…