ARM64架构栈帧以及帧指针FP

打印 上一主题 下一主题

主题 548|帖子 548|积分 1644

前言

这篇文章描述了 x86_64架构栈帧以及帧指针FP,本文描述arm64架构下栈帧相关知识。
一、arm64架构寄存器简介

1.1 异常等级

在ARMv8中,执行发生在四个异常级别之一。在AArch64中,异常级别决定特权级别,其方式与ARMv7中定义的特权级别雷同。Exception级别决定了特权级别,因此在ELn执行对应于特权PLn。雷同地,一个异常级别的n值比另一个更大,则处于更高的异常级别。数字小于另一个的异常级别被描述为处于较低的异常级别。
异常级别提供了适用于ARMv8体系布局全部操作状态的软件执行权限的逻辑分离。它雷同于并支持计算机科学中常见的分级掩护域的概念。
以下是在每个异常级别上运行的软件的范例示例:
  1. EL0 Normal user applications.
  2. EL1 Operating system kernel typically described as privileged.
  3. EL2 Hypervisor.
  4. 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)根据基寄存器值和偏移寄存器值计算地址,从内存加载一个字,并将其写入寄存器。可以选择性地对偏移寄存器值举行移位和扩展。
  1. LDR 目标寄存器, <存储器地址>  //把存储器地址中的数据加载到目标寄存器中
复制代码
STR (register)

存储寄存器(Register)根据基寄存器值和偏移寄存器值计算地址,并从寄存器将32位字或64位双字存储到计算的地址。
  1. STR 源寄存器, <存储器地址>  //把源寄存器的数据存储到存储器地址中
复制代码
(3)
A64指令集提供LDP和STP指令来实现多字节内存加载和存储。
LDP:
加载寄存器对根据基寄存器值和立即数偏移量计算地址,从内存加载两个32位字或两个64位双字,并将它们写入两个寄存器。
STP:
存储寄存器对根据基寄存器值和立即数偏移量计算地址,并从两个寄存器将两个32位字或两个64位双字存储到计算的地址。
(4)
A64指令集利用加载和存储指令来实现入栈和出栈操作。A32指令集提供了PUSH和POP指令来实现入栈和出栈操作,但是A64指令集已经去掉了PUSH和POP指令集。
(5)
A64指令集利用BL指令来实现函数跳转操作,带返回地址的调跳转指令。
BL:
  1. 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例程:
  1. int funa(int a, int b)
  2. {
  3.     int ret = a + b;
  4.     return ret;
  5. }
  6. int funb(int c, int d)
  7. {
  8.     int ret = c + d ;
  9.     ret = funa(c, ret);
  10.     return ret;
  11. }
  12. int main(void)
  13. {
  14.     int i = 1, j = 2;
  15.     int ret = funb(i,j);
  16.     return 0;
  17. }
复制代码
反汇编:
  1. 000000000000071c <funa>:
  2. 71c:   d10083ff        sub     sp, sp, #0x20
  3. 720:   b9000fe0        str     w0, [sp, #12]
  4. 724:   b9000be1        str     w1, [sp, #8]
  5. 728:   b9400fe1        ldr     w1, [sp, #12]
  6. 72c:   b9400be0        ldr     w0, [sp, #8]
  7. 730:   0b000020        add     w0, w1, w0
  8. 734:   b9001fe0        str     w0, [sp, #28]
  9. 738:   b9401fe0        ldr     w0, [sp, #28]
  10. 73c:   910083ff        add     sp, sp, #0x20
  11. 740:   d65f03c0        ret
  12. 0000000000000744 <funb>:
  13. 744:   a9bd7bfd        stp     x29, x30, [sp, #-48]!
  14. 748:   910003fd        mov     x29, sp
  15. 74c:   b9001fe0        str     w0, [sp, #28]
  16. 750:   b9001be1        str     w1, [sp, #24]
  17. 754:   b9401fe1        ldr     w1, [sp, #28]
  18. 758:   b9401be0        ldr     w0, [sp, #24]
  19. 75c:   0b000020        add     w0, w1, w0
  20. 760:   b9002fe0        str     w0, [sp, #44]
  21. 764:   b9402fe1        ldr     w1, [sp, #44]
  22. 768:   b9401fe0        ldr     w0, [sp, #28]
  23. 76c:   97ffffec        bl      71c <funa>
  24. 770:   b9002fe0        str     w0, [sp, #44]
  25. 774:   b9402fe0        ldr     w0, [sp, #44]
  26. 778:   a8c37bfd        ldp     x29, x30, [sp], #48
  27. 77c:   d65f03c0        ret
  28. 0000000000000780 <main>:
  29. 780:   a9be7bfd        stp     x29, x30, [sp, #-32]!
  30. 784:   910003fd        mov     x29, sp
  31. 788:   52800020        mov     w0, #0x1                        // #1
  32. 78c:   b90017e0        str     w0, [sp, #20]
  33. 790:   52800040        mov     w0, #0x2                        // #2
  34. 794:   b9001be0        str     w0, [sp, #24]
  35. 798:   b9401be1        ldr     w1, [sp, #24]
  36. 79c:   b94017e0        ldr     w0, [sp, #20]
  37. 7a0:   97ffffe9        bl      744 <funb>
  38. 7a4:   b9001fe0        str     w0, [sp, #28]
  39. 7a8:   52800000        mov     w0, #0x0                        // #0
  40. 7ac:   a8c27bfd        ldp     x29, x30, [sp], #32
  41. 7b0:   d65f03c0        ret
  42. 7b4:   d503201f        nop
复制代码
3.1 main函数

Linux一个C语言例程执行时,main函数前面还有其他函数执行,main函数不是第一个被调用执行的函数,从反汇编代码也可以看出来,这里我不过多表明,流程如下:
  1. int main(int argc, char** argv)
复制代码
  1. shell
  2.         -->fork() + execve() //execve将命令行参数传递给新程序
  3.                 -->_start()  //准备参数 argc,argv 和 envp
  4.                         -->__libc_start_main() //初始化运行环境
  5.                                 -->main()
复制代码
请参考:https://blog.csdn.net/v6543210/article/details/122950395
当执行到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指针只扩大一次,刚进入函数的时候,就会一次性把全部需要的栈空间都申请出来。
  1. 780:   a9be7bfd        stp     x29, x30, [sp, #-32]!
复制代码

SP寄存器是当前函数栈指针,指向栈顶。
FP寄存器是当前函数栈帧指针,指向栈底。
(2)将sp寄存器的值赋值给x29寄存器(FP寄存器),即 FP = sp。
这行代码将栈指针 sp 的值复制给 x29 (帧指针寄存器 FP),以建立当前函数的堆栈帧。
  1. 784:   910003fd        mov     x29, sp
复制代码

(3)这些代码分别将常数值 1 和 2 存储到堆栈帧上的偏移为 20 和 24 的位置。
  1. 788:   52800020        mov     w0, #0x1                        // #1
  2. 78c:   b90017e0        str     w0, [sp, #20]
  3. 790:   52800040        mov     w0, #0x2                        // #2
  4. 794:   b9001be0        str     w0, [sp, #24]
复制代码
这里的局部变量都是 int 类型,利用32位寄存器:w0寄存器即可。
利用str指令将这两个局部变量加载到栈帧中。
  1. STR (register)
复制代码
(4)这些代码从堆栈帧上的偏移为 24 和 20 的位置加载存储的值到寄存器 w1 和 w0。
由于函数funb的参数是i,j,函数参数只有两个,用两个寄存器传递参数即可,由于 i,j 是 int 类型,利用寄存器 w1 和 w0传递即可。
  1. 798:   b9401be1        ldr     w1, [sp, #24]
  2. 79c:   b94017e0        ldr     w0, [sp, #20]
复制代码
(5)这行代码调用了一个名为 “funb” 的函数,跳转到地址 744。
bl 指令将下一条指令的地址生存在LR寄存器中。
  1. 7a0:   97ffffe9        bl      744 <funb>
复制代码
(6)这行代码将函数 “funb” 的返回值存储到堆栈帧上的偏移为 28 的位置。
函数的返回值生存在X0寄存器中,ret 是 int类型,利用w0寄存器即可。
  1. 7a4:   b9001fe0        str     w0, [sp, #28]
复制代码
(7)这些代码将常数值 0 存储到寄存器 w0 中,并规复调用者生存的寄存器 x29 和 x30 的值,然后执行返回指令,从函数中返回。
  1. 7a8:   52800000        mov     w0, #0x0                        // #0
  2. 7ac:   a8c27bfd        ldp     x29, x30, [sp], #32
  3. 7b0:   d65f03c0        ret
  4. 7b4:   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。
  1. 744:   a9bd7bfd        stp     x29, x30, [sp, #-48]!
  2. 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。
  1. 74c:   b9001fe0        str     w0, [sp, #28]
  2. 750:   b9001be1        str     w1, [sp, #24]
  3. 754:   b9401fe1        ldr     w1, [sp, #28]
  4. 758:   b9401be0        ldr     w0, [sp, #24]
  5. 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中。
  1. 760:   b9002fe0        str     w0, [sp, #44]
  2. 764:   b9402fe1        ldr     w1, [sp, #44]
  3. 768:   b9401fe0        ldr     w0, [sp, #28]
  4. 76c:   97ffffec        bl      71c <funa>
复制代码
(4)
函数的返回值生存在寄存器w0中。
将函数 funa 的返回值寄存器w0存储到 [sp, #44] 处,即在堆栈中存储变量 ret 的值。
从 [sp, #44] 处加载值到寄存器 w0,即加载变量 ret 的值。
  1. 770:   b9002fe0        str     w0, [sp, #44]
  2. 774:   b9402fe0        ldr     w0, [sp, #44]
复制代码
(5)
将 [sp] 处的值加载到寄存器 x29 和 x30 中,并将堆栈指针 sp 增加 48。
返回到调用该函数的位置。
  1. 778:   a8c37bfd        ldp     x29, x30, [sp], #48
  2. 77c:   d65f03c0        ret
复制代码
funb函数的栈帧如下图所示:

3.3 funa

函数funa是叶子函数,不会在调用其他函数,因此不需要利用x29, x30寄存器,因此不需要生存这两个寄存器的值了。
(1)在堆栈上分配32个字节的空间,通过将栈指针(sp)减去0x20。
  1. 71c:   d10083ff        sub     sp, sp, #0x20
复制代码
(2)
将传入的两个参数w0和w1存储到堆栈中的特定位置([sp, #12]和[sp, #8])。
从堆栈中加载之前存储的参数值到寄存器w1和w0。
将w1和w0的值相加,结果存储到w0寄存器。
将w0的值存储到堆栈中的另一个位置([sp, #28])。
从堆栈中加载存储的结果值到w0寄存器。
  1. 720:   b9000fe0        str     w0, [sp, #12]
  2. 724:   b9000be1        str     w1, [sp, #8]
  3. 728:   b9400fe1        ldr     w1, [sp, #12]
  4. 72c:   b9400be0        ldr     w0, [sp, #8]
  5. 730:   0b000020        add     w0, w1, w0
  6. 734:   b9001fe0        str     w0, [sp, #28]
  7. 738:   b9401fe0        ldr     w0, [sp, #28]
复制代码
(3)
在堆栈上开释之前分配的32个字节的空间,通过将栈指针(sp)加上0x20。
返回函数,规复步伐计数器的值。
  1. 73c:   910083ff        add     sp, sp, #0x20
  2. 740:   d65f03c0        ret
复制代码
函数funa的栈帧如下图所示:

四、栈帧总结

假设下列函数调用:
  1. funb()
  2. {
  3.         func()
  4. }
  5. funa()
  6. {
  7.         funb()
  8. }
  9. main()
  10. {
  11.         funa()
  12. }
复制代码
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语言示例:
  1. int fund(int g, int h)
  2. {
  3.     return g + h;
  4. }
  5. int func(int e, int f)
  6. {
  7.    int ret = e + f;
  8.    ret = fund(e, ret);
  9.    return ret;
  10. }
  11. int funb(int c, int d)
  12. {
  13.     int ret = c + d;
  14.     ret = func(c, ret);
  15.     return ret;
  16. }
  17. int funa(int a, int b)
  18. {
  19.     int ret = a + b ;
  20.     ret = funb(a, ret);
  21.     return ret;
  22. }
  23. int main(void)
  24. {
  25.     int i = 1, j = 2;
  26.     int ret = funa(i,j);
  27.     return 0;
  28. }
复制代码
  1. (gdb) b main
  2. (gdb) b funa
  3. (gdb) b funb
  4. (gdb) b func
  5. (gdb) r
复制代码
(1)
main:
  1. (gdb) disassemble
  2. Dump of assembler code for function main:
  3.         ......
  4.    0x0000005555555810 <+32>:    bl      0x55555557b4 <funa>
  5.    0x0000005555555814 <+36>:    str     w0, [sp, #28]
  6.    ......
复制代码
  1. x29            0x7ffffff400
  2. x30            0x7ff7e5c110
复制代码
(2)
funa:
  1. (gdb) c
  2. (gdb) disassemble
  3. Dump of assembler code for function funa:
  4.         ......
  5.    0x00000055555557dc <+40>:    bl      0x5555555778 <funb>
  6.    0x00000055555557e0 <+44>:    str     w0, [sp, #44]
  7.    ......
复制代码
  1. (gdb) info registers
  2. x29            0x7ffffff3d0
  3. x30            0x5555555814
复制代码
可以看到x30寄存器的值就是main函数 bl funa 下一条指令的地址。
根据x29寄存器得到funa栈帧基地址:
  1. 0x7ffffff3d0
复制代码
读取该地址的值(x29寄存器FP存放了指向上一个栈帧的基地址):
  1. (gdb) x/1xg 0x7ffffff3d0
  2. 0x7ffffff3d0
  3. :   0x0000007ffffff400
复制代码
那么可以得到main函数的栈帧基地址:0x0000007ffffff400
这个值就等于执行main函数时,x29寄存器的值。
将main函数的栈帧基地址+8然后读取获取main的返回地址:
这里 + 8 的原因:LR = FP + 8
  1. 0x0000007ffffff400 + 8 = 0x0000007ffffff408
复制代码
  1. (gdb) x/1xg 0x0000007ffffff408
  2. 0x7ffffff408:   0x0000007ff7e5c110
复制代码
main的返回地址:0x0000007ff7e5c110
将main的返回地址 - 4 就可以获取 BL main这条函数跳转指令的地址:
这里 - 4 的原因:执行BL指令时,将下一条指令的地址(即返回地址)写入X30寄存器中,这里我们已经获取到了返回地址,那么 -4 就获取到了 BL 指令的地址。
  1. 0x0000007ff7e5c110 - 4 = 0x0000007ff7e5c10c
复制代码
那么其上一条调用main的指令地址就是0x0000007ff7e5c10c:
  1. (gdb) x/i 0x0000007ff7e5c10c
  2.    0x7ff7e5c10c <__libc_start_main+228>:        blr     x3
  3. (gdb) x/2i 0x0000007ff7e5c10c
  4.    0x7ff7e5c10c <__libc_start_main+228>:        blr     x3
  5.    0x7ff7e5c110 <__libc_start_main+232>:        bl      0x7ff7e71a40 <exit>
复制代码
可以看到是__libc_start_main函数调用 main 函数。
(3)
funb:
  1. (gdb) disassemble
  2. Dump of assembler code for function funb:
  3.         ......
  4.    0x00000055555557a0 <+40>:    bl      0x555555573c <func>
  5.    0x00000055555557a4 <+44>:    str     w0, [sp, #44]
  6.         ......
复制代码
  1. (gdb) info registers
  2. x29            0x7ffffff3a0
  3. x30            0x55555557e0
复制代码
可以看到x30寄存器的值就是 funa bl funb下一条指令的地址。
根据x29寄存器得到funb栈帧基地址:
  1. 0x7ffffff3a0
复制代码
读取该地址的值(x29寄存器FP存放了指向上一个栈帧funa的基地址):
  1. (gdb) x/1xg 0x7ffffff3a0
  2. 0x7ffffff3a0
  3. :   0x0000007ffffff3d0
复制代码
这个值就等于执行funa函数时,x29寄存器的值。
将funa函数的栈帧基地址+8然后读取获取funa的返回地址:
  1. 0x0000007ffffff3d0 + 8 = 0x0000007ffffff3d8
复制代码
  1. (gdb) x/1xg 0x0000007ffffff3d8
  2. 0x7ffffff3d8:   0x0000005555555814
复制代码
funa的返回地址:0x0000005555555814
将funa的返回地址 - 4 就可以获取 main BL funa这条函数跳转指令的地址:
  1. 0x0000005555555814- 4 = 0x0000005555555810
复制代码
那么其上一条调用funa的指令地址就是0x0000005555555810:
  1. (gdb) x/i 0x0000005555555810
  2.    0x5555555810 <main+32>:      bl      0x55555557b4 <funa>
  3. (gdb) x/2i 0x0000005555555810
  4.    0x5555555810 <main+32>:      bl      0x55555557b4 <funa>
  5.    0x5555555814 <main+36>:      str     w0, [sp, #28]
复制代码
可以看到是main函数调用 funa 函数。
(4)
func:
  1. (gdb) disassemble
  2. Dump of assembler code for function func:
  3.         ......
  4.    0x0000005555555764 <+40>:    bl      0x555555571c <fund>
  5.    0x0000005555555768 <+44>:    str     w0, [sp, #44]
  6.         ......
复制代码
  1. (gdb) info registers
  2. x29            0x7ffffff370
  3. x30            0x55555557a4
复制代码
可以看到x30寄存器的值就是 funb bl func下一条指令的地址。
根据x29寄存器得到func栈帧基地址:
  1. 0x7ffffff370
复制代码
读取该地址的值(x29寄存器FP存放了指向上一个栈帧funb的基地址):
  1. (gdb) x/1xg 0x7ffffff370
  2. 0x7ffffff370
  3. :   0x0000007ffffff3a0
复制代码
这个值就等于执行funb函数时,x29寄存器的值。
将funb函数的栈帧基地址+8然后读取获取funb的返回地址:
  1. 0x0000007ffffff3a0 + 8 = 0x0000007ffffff3a8
复制代码
  1. (gdb) x/1xg 0x0000007ffffff3a8
  2. 0x7ffffff3a8:   0x00000055555557e0
复制代码
funb的返回地址:0x00000055555557e0
将funb的返回地址 - 4 就可以获取 funaBL funb这条函数跳转指令的地址:
  1. 0x00000055555557e0 - 4 = 0x00000055555557dc
复制代码
那么其上一条调用funb的指令地址就是0x00000055555557dc:
  1. (gdb) x/i 0x00000055555557dc
  2.    0x55555557dc <funa+40>:      bl      0x5555555778 <funb>
  3. (gdb) x/2i 0x00000055555557dc
  4.    0x55555557dc <funa+40>:      bl      0x5555555778 <funb>
  5.    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

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

飞不高

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表