文章目录
- 前言
- 一、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