我一直在阅读有关Windows x64 ABI上非常好的文章的长目录。这些文章的一个非常小的方面是帧指针的描述。一般的要点是,因为Windows x64调用堆栈规则非常严格,所以不需要专用的帧指针 ,尽管它是可选的。
我一直注意到的一个例外是当alloca()
用于动态分配堆栈上的内存时。这样做的函数显然需要一个帧指针。例如,引用微软关于"Stack Allocation"的文档(我添加的斜体和粗体):
如果在函数中动态分配空间(alloca),则非易失性寄存器必须用作帧指针以标记固定部分的基础堆栈并且注册 必须 在prolog中保存并初始化。请注意,当使用alloca时,来自同一调用者的同一被调用者的调用可能会为其寄存器参数设置不同的home地址。
对此,微软的x64 ABI alloca()
documentation加密地添加:
_alloca需要16字节对齐,另外需要使用帧指针。
首先,必须使用为什么?我假设在异常时调用堆栈展开但我还没有找到令人满意的解释。
下一个问题:哪里一定要点?在上述两个引文中的第一个中,它表示“必须”用于标记“固定部分”堆栈的基础。什么是“堆栈的固定部分”?我得到的印象是,这个术语在给定的框架中表示包含的地址范围(较高的地址到较低的地址):
同样,我还没有找到这个“固定部分”的令人满意的定义。我上面链接的"Stack Allocation"页面包含下面的图表以及“如果使用的话,堆栈指针通常指向此处”:
This very nifty blog post同样含糊不清,包括一个图表,说明帧指针“指向此处某处”,其中“here”是保存的非易失性寄存器和本地的地址。
最后一点含糊不清,来自Microsoft的MSDN文章,标题为"Dynamic Parameter Stack Area Construction",仅包含此内容:
如果使用帧指针,则存在动态创建参数堆栈区域的选项。目前在x64编译器中没有这样做。
“一般”是什么意思? “在这里的某个地方”在哪里?存在的选项是什么?有规则吗?谁在乎?
或者,tl;博士:标题要求什么。任何包含注释程序集的答案都会感激不尽。
答案 0 :(得分:7)
该图清楚地表明帧指针指向本地堆栈帧的固定部分的底部。 “固定部分”是其大小不变并且其位置相对于初始堆栈指针固定的部分。在图中,它标记为“局部变量和保存的非易失性寄存器。”[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]你可以从文本说框架指针指向“堆栈固定部分的基础”的事实推断出这一点,并且图表说“框架指针通常指向这里”,它是指向局部变量的基础并保存非易失性寄存器。假设文本和图表一致,这意味着堆栈的固定部分与局部变量相同并保存非易失性寄存器。这是你每天所做的同样的推断,甚至没有意识到。例如,如果一个故事说
莎莉叫她哥哥。 “比利,你在哪里?”
你可以推断比利是莎莉的兄弟。
答案 1 :(得分:1)
alloca
旨在与仅在运行时可用的大小一起使用。因此,它将把堆栈指针改变一个在编译时未知的量。由于固定布局,您通常可以在堆栈指针上相对于堆栈指针寻址局部变量和参数,但alloca
会因此需要另一个稳定的寄存器。只要您知道与固定区域的关系,此帧指针就可以指向您想要的任何位置。
当释放alloca
内存时,帧指针也很方便,因为您可以简单地将堆栈指针恢复到已知位置,而不必担心堆栈指针的变化程度。
我不认为ABI需要帧指针,或者它必须是rbp
或者它必须指向任何特定的地方(免责声明:我不使用Windows)