x64汇编中的“影子空间”是什么?

43

我发现有很多关于这个“影子空间”的话题,但是在它们中没有找到答案,所以我的问题是:

在进入一个过程之前,我需要减去多少字节的堆栈指针?

在减去“影子空间”之前,我应该将过程参数推送到堆栈中吗?

我已经反汇编了我的代码,但是我找不到逻辑。


相关:Shadow space example有一个可行的Hello World函数示例,并且看起来是一个很好的标准重复问题,适用于那些因为没有正确或根本没有保留而导致崩溃的问题。 - undefined
2个回答

62
Shadow space(有时也称为 Spill spaceHome space)是位于调用函数所拥有的返回地址32字节上方的空间(可用作临时存储空间),如果存在栈参数,则在其下方。 在运行call指令之前,调用者必须为其被调用者的影子空间保留空间。
它旨在用于使x64调试更加容易。
请记住,前4个参数将通过寄存器传递。 如果您打断点调试器并检查线程的调用堆栈,将无法看到传递给函数的任何参数。 寄存器中存储的值是瞬态的,不能在向上移动调用堆栈时重建。
这就是Home space发挥作用的地方:编译器可以将寄存器值的副本留在堆栈上以供稍后在调试器中检查。 这通常发生在未优化的构建中。 然而,当启用优化时,编译器通常将Home space视为可用于临时使用。 不会在堆栈上留下任何副本,调试崩溃转储会变成一场噩梦。
调试优化的x64代码的挑战 提供了深入的信息。

4
影子空间还可以简化可变参数函数。它们可以将寄存器参数直接"倾倒"到影子空间中,然后整个参数列表就是一个连续的数组。如果我没记错,ABI甚至要求FP参数在整数和xmm寄存器中都传递,因此例如printf的开始可以将4个整数参数寄存器转储到影子空间中,而不需要找出哪些参数是“double”。或者它可以直接使用xmm0中的内容。这种做法非常令人恼火,看起来过于追求简单而牺牲了性能。 :/ - Peter Cordes
1
这对我来说没有意义 - 为什么调试器不能聪明到在堆栈(alloca)或堆上为寄存器值分配新空间?为什么你总是想要在调试时分配空间呢? - Evan Carroll
6
@eva:调试器是一个观察者。它的作用不是改变被观察的代码。当然,调试器可以使用私有内存来跟踪函数调用时的寄存器值。但是,如果在程序开始运行后附加调试器,则无法检查完整的调用堆栈。尽管我不知道更好的解决方案,但我赞同你这一切都感觉有点笨重。 - IInspectable
一个函数也拥有它的堆栈参数,并且可以在函数进入后修改它们。为了能够在回溯时看到函数实际调用的参数,您必须编写使用不同变量而不是修改传入参数的代码。(或者编译器可以复制堆栈参数如果您这样做)。由于调试信息显示所有变量的位置,而不仅仅是参数,因此您可以在编译器溢出它们的堆栈帧中看到参数变量,而不管是否有阴影空间。例如,x86-64 System V调用约定没有这个问题,即使没有阴影空间。 - Peter Cordes
是的,可变参数是我看到的原因(包括 Raymond Chen,如果我没记错的话)。 (还有其他设计决策,例如通过引用传递任何宽度大于8字节的args,以及选择不允许4个整数*和4个FP args在寄存器中,而是严格4个寄存器-arg slot。并且对于可变参数函数,您必须将任何FP寄存器参数复制到相应的整数reg。)因此,函数始终能够通过将整数regs转储到阴影空间来创建其所有args的数组。 相关 - Peter Cordes
显示剩余3条评论

19

阴影空间是必须预留给被调用过程的32个字节(4x8字节)的空间。这意味着在调用之前你必须在栈上提供32个字节的空间。该空间可以不初始化,没有关系。

需要注意的是,在x64调用约定中,第四个参数后的参数都被推送到栈上,它们在这个阴影空间的顶部(在32字节之前被推入栈中)。

简而言之,你可以将其看作是在x64函数中具有至少4个参数,但前4个的值在寄存器中。

在调用x64时还应考虑堆栈对齐等问题。


非常感谢,所以最小预留量必须为32字节,是否有最大预留量? - Igor Bezverhi
@IgorBezverhi 不是按照惯例,但被调用的函数只期望32个字节+额外的参数,因此它(应该)永远不会使用更多。对于当前的函数,您可以尽可能地使用,只要不超过最大堆栈大小(所谓的堆栈溢出)。 - ElderBug

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接