分享|函数栈帧探秘
821
2024.01.28
2025.01.28
发布于 未知归属地

我们知道,栈是现代计算机最为重要的概念之一,没有栈就没有函数,没有局部变量。栈的内存分配是从高地址向低地址逐步扩展。

程序运行过程中,实际内存空间会有单独的一个栈空间,一般来说,操作系统会为每个线程分配固定大小的栈空间。
image.png

操作系统给进程分配的stack的地址空间0x7ffffffde000~0x7fffffff000,栈存储的是数据,而非指令。栈内存以栈帧作为一个基本单位,每个栈帧对应一个函数调用
image.png
上面图有个错误纠正下,就是“保存的寄存器、局部变量和临时值”处应该是ebp-4

1.函数的栈帧
按照下面的格式来记录函数调用过程中的数据:
image.png

  1. 栈相关的寄存器
    image.png

  2. 函数栈帧创建流程
    函数A的栈帧如下:
    image.png
    函数A执行过程中调用函数B,怎么创建B函数的栈帧?
    image.png
    image.png
    image.png
    image.png
    第四步不是必须的,有的函数没有这一步,而是rbp始终等于rsp,但实际上还是使用rbp+偏移的方式来记录局部变量和函数参数。

    从地址最低的栈帧开始,将每个栈帧基址存储的值连接起来,可组成一个链表。通过这种方式来实现函数嵌套调用的正确返回。例如,函数A调用函数B,函数B调用函数C;
    那么函数C栈帧基址存储的值=函数B栈帧基址;函数B栈帧基址存储的值=函数A栈帧基址;

    image.png

  3. 函数栈帧清理流程
    image.png

函数A调用了函数B,函数B中的leave指令完成对B函数栈帧的清理,并通过ret指令完成函数B的执行,并回到函数A中继续执行;
image.png
image.png
image.png

总结:

  1. 每个函数的第一个指令是将调用者的栈基指针先压入栈。也就是说,栈帧的第一个数据是外层函数的RBP。这样做的目的是为了函数返回的时候,能够找到调用函数。反过来想,如果没有记录调用函数的RBP,函数执行完之后是没法收尾的。
    image.png

  2. 因为RBP寄存器的作用是作为一个函数的栈帧基址,它存在的意义是作为一个基准,RBP寄存器的值变化相比RSP就会少很多。每个函数的汇编代码中,只有两处涉及RBP寄存器的值发生变化,分别是一进一出。
    2.1 函数开始处,在保存外层函数的栈帧基址后,通过mov rbp, rsp指令,强制将rbp从外层函数跳转到当前函数。对应下图:
    image.png
    2.2 函数ret之前通过pop rbp指令,强制将当前函数跳转到外层函数。对应下图:
    image.png

  3. 函数栈帧占据的是stack空间,存放的只有数据,没有指令。数据包括:函数的局部变量,寄存器,外层调用函数的栈帧基址,调用子函数的后一条指令地址(如果存在子函数调用)

评论 (1)