【问题标题】:Where, and why, is the x64 frame pointer supposed to point? (Windows x64 ABI)x64 帧指针应该指向哪里,为什么? (Windows x64 ABI)
【发布时间】:2014-08-24 08:29:19
【问题描述】:

我一直在阅读关于 Windows x64 ABI 的非常好的文章的长目录。这些文章的一个非常小的方面是对帧指针的描述。一般的要点是,因为 Windows x64 调用堆栈规则非常严格,所以通常不需要专用的帧指针,尽管它是可选的。

我经常看到的一个例外是alloca() 用于在堆栈上动态分配内存。这样做的函数显然需要一个帧指针。例如,引用微软在"Stack Allocation" 上的文档(我添加的斜体和粗体):

如果空间是在函数中动态分配(alloca)的,那么非易失性寄存器必须用作帧指针来标记固定部分的基址 堆栈 并且该寄存器必须在序言中保存和初始化。请注意,当使用 alloca 时,来自同一个调用者的对同一个被调用者的调用可能具有不同的注册参数的家乡地址。

对此,微软的 x64 ABI alloca() documentation 神秘地补充道:

_alloca 需要 16 字节对齐,另外 需要使用帧指针。

首先,为什么必须使用它?我假设调用堆栈在异常时展开,但我还没有找到令人满意的解释。

下一个问题:必须指向哪里?在上述两个引用中的第一个中,它说“必须”用于标记“堆栈的固定部分”的基础。什么是“堆栈的固定部分”?我得到的印象是,这个术语表示,在给定的框架中,地址范围包括(较高的地址到较低的地址):

  • 调用者返回地址(如果你认为它是当前函数框架的一部分);
  • 函数序言保存非易失性寄存器的地址;和
  • 存储局部变量的地址。

同样,我还没有为这个“固定部分”找到一个令人满意的定义。我在上面链接的"Stack Allocation" 页面包含下图以及“如果使用,堆栈指针将一般指向此处”的字样:

This very nifty blog post 同样含糊不清,包括一个图表说明帧指针“指向此处某处”,其中“此处”是保存的非易失性寄存器和本地寄存器的地址。

最后一点神秘,来自微软的 MSDN 文章"Dynamic Parameter Stack Area Construction",其中仅包含以下内容:

如果使用帧指针,则存在动态创建参数堆栈区域的选项。目前在 x64 编译器中没有这样做。

“一般”是什么意思? “这里的某个地方”在哪里?存在的选项是什么?有规律吗?谁在乎?

或者,tl;dr:标题要求什么。任何包含带注释的程序集的答案都非常感谢。

【问题讨论】:

    标签: c windows assembly 64-bit calling-convention


    【解决方案1】:

    alloca 旨在与仅在运行时可用的大小一起使用。因此,它将以编译时未知的量更改堆栈指针。由于固定布局,您通常可以相对于堆栈指针在堆栈上寻址局部变量和参数,但alloca 将其弄乱,因此需要另一个稳定的寄存器。只要您知道与固定区域的关系,该帧指针就可以指向您想要的任何位置。

    当需要释放alloca 内存时,帧指针也很方便,因为您可以简单地将堆栈指针恢复到已知位置,而不必担心堆栈指针发生了多少变化。

    我不认为 ABI 需要帧指针本身,或者它必须是 rbp 或者它必须指向任何特定位置(免责声明:我不使用 Windows)。

    【讨论】:

      【解决方案2】:

      该图非常清楚地表明,帧指针指向本地堆栈帧的固定部分的底部。 “固定部分”是大小不变且位置相对于初始堆栈指针是固定的部分。在图中,它被标记为“局部变量和保存的非易失性寄存器”。[1]

      帧指针的精确位置对操作系统来说并不重要,因为从信息论的角度来看,局部变量与alloca在进入函数时立即分配的内存是无法区分的。

      void function1()
      {
        int a;
        int *b = (int*)alloca(sizeof(int));
        ...
      }
      
      void function2()
      {
        int& a = *(int*)alloca(sizeof(int));
        int *b = (int*)alloca(sizeof(int));
        ...
      }
      

      操作系统无法区分这两个功能。它们都将a 存储在非易失性寄存器正下方的堆栈中。

      这种等价性就是图表显示“一般”的原因。在实践中,编译器会将其指向指定的位置,但理论上他们可以将其指向本地框架内的任何位置,只要从框架指针到返回地址的距离是一个常数。

      该函数需要通知操作系统帧指针在哪里,以便在异常处理期间可以展开堆栈。如果没有这些信息,就不可能遍历堆栈,因为帧是可变大小的。

      [1] 你可以从文本说帧指针指向“堆栈的固定部分的底部”和图表说“帧指针通常指向这里”这一事实推断出这一点,它是指向局部变量和保存的非易失性寄存器的基数。假设文本和图表一致,这意味着堆栈的固定部分与局部变量和保存的非易失性寄存器相同。这与你每天都在不知不觉中做出的推理相同。例如,如果一个故事说

      莎莉喊她哥哥。 “比利,你在哪里?”

      你可以推断出比利是莎莉的兄弟。

      【讨论】:

      • 好答案。唯一让我感到困惑的是提到了“操作系统”,恕我直言,这一切都没有涉及。调用约定和堆栈使用在没有任何操作系统的裸机设备上也可以正常工作。 “通知操作系统”意味着类似于系统调用……这显然不会在这里发生。
      • “通知操作系统”在这里是相关的,因为在您的程序(操作系统)之外有代码需要了解您的代码的堆栈布局。如果您正在编写裸机代码,那么您可以按照自己喜欢的方式设置堆栈,其他人不会在意。
      猜你喜欢
      • 2014-05-04
      • 2017-01-11
      • 1970-01-01
      • 2021-08-16
      • 2012-12-13
      • 2010-12-28
      • 2016-11-23
      • 2010-10-10
      • 1970-01-01
      相关资源
      最近更新 更多