【问题标题】:Why more space on the stack frame is reserved than is needed in x86为什么堆栈帧上保留的空间比 x86 中所需的空间多
【发布时间】:2016-11-14 02:00:33
【问题描述】:

参考以下代码

#include <iostream>
using namespace std;

void do_something(int* ptr) {
    cout << "Got address " << reinterpret_cast<void*>(ptr) << endl;
}

void func() {
    int a;
    do_something(&a);
}

int main() {
    func();
}

当我反汇编func函数时,x86(我不确定它是x86还是x86_64)代码是

->  0x100001140 <+0>:  pushq  %rbp
    0x100001141 <+1>:  movq   %rsp, %rbp
    0x100001144 <+4>:  subq   $0x10, %rsp
    0x100001148 <+8>:  leaq   -0x4(%rbp), %rdi
    0x10000114c <+12>: callq  0x100000f90               ; do_something(int*)
    0x100001151 <+17>: addq   $0x10, %rsp
    0x100001155 <+21>: popq   %rbp
    0x100001156 <+22>: retq   
    0x100001157 <+23>: nopw   (%rax,%rax)

我知道第一个 push 语句将基指针推送到堆栈上的前一个函数调用,然后将堆栈指针值复制到基指针。但是为什么要为堆栈保留 16 个字节呢?

这是否与对齐有关?变量a 只需要 4 个字节..

另外,lea 指令在这个函数调用中到底做了什么?它只是获取相对于基指针的整数地址吗?在这种情况下,它似乎与基址相差 4 个字节(假设返回地址是 4 个字节长并且是堆栈上的第一件事)

其他架构似乎保留了超过 16 个字节,并将其他内容存储在堆栈帧的基础上。..

【问题讨论】:

  • 是的,你没看错。它用于对齐,lea&amp; 运算符,但它与另一个方向的返回地址无关。它是-4,因为sizeof(int)4,而locals 低于rbp(也因为编译器决定在它分配的16 个字节的最后4 个字节中分配它)。请注意,在 64 位模式下,返回地址实际上是 8 个字节。
  • @Jester 我很难弄清楚 lea 指令到底在做什么,我不想猜测,因为我觉得以后会适得其反。你能解释一下参数是如何传递的以及这个函数调用中堆栈有什么吗?
  • 参数传入rdi,这就是为什么使用leaq -0x4(%rbp), %rdi。从rbp 向上推送rbp,然后返回地址到main。从rbp 向下,您有局部变量a,后跟12 个未使用的字节。 func的返回地址会放在那个下面。
  • @Jester 如果这与对齐有关,那么为什么要保留 16 个字节? 8个字节不行吗?
  • ABI 需要 16 字节对齐(以使 SSE 对齐指令更容易)。

标签: c++ assembly x86-64 disassembly


【解决方案1】:

这是x64代码,注意rsp寄存器的使用。 x86 代码使用 esp 寄存器。 x64 ABI 最重要的实现细节是堆栈必须始终与 16 对齐。正确运行 64 位代码实际上并不需要,但对齐保证确保编译器可以安全地发出 SSE 指令。它们的操作数需要 16 字节对齐才能很快。在这个 sn-p 中实际上没有使用任何东西,但它们可能在 do_something 中。

进入函数后,调用者的 CALL 指令已将 8 个字节压入堆栈以存储返回地址。第一条 PUSH 指令再次将堆栈对齐到 16,不需要额外的更正。

然后它创建堆栈帧来存储a 变量。虽然只需要 4 个字节,但仅将 rsp 调整 4 不足以提供必要的对齐。所以它选择下一个合适的值,16。额外的 12 个字节根本没有使用。

LEA 指令是一个非常方便的实现&amp;a 的指令。 LEA=加载有效地址=“取地址”。这里不是特别涉及的计算,当您使用&amp;array[ix] 之类的东西时,它会变得更加复杂。如果数组元素大小为 1、2 或 4 个字节长,那么单个 LEA 仍然可以完成一些事情,这很常见。

-4 是从a 变量的堆栈帧开始的偏移量。存储int 需要4 个字节,您的编译器实现LP64 data model。请记住,堆栈向下增长,因此它不是 0。

然后它只是进行函数调用,rdi 寄存器用于传递 x64 ABI 中的第一个参数。然后通过重新调整rsp再次销毁栈帧,恢复rbp。

请记住,您正在查看未优化的代码。通常在优化器完成后这些都不会留下,像这样的小函数几乎总是被内联。所以这并没有教给你关于实际运行的代码的太多实用知识。看看 -O2 代码。

【讨论】:

  • 通常使用“x86”作为一个包罗万象的术语,包括长模式、32 位兼容/传统模式和 16 位实模式。这不是 OP 正在做的事情,但对我来说说“x86 使用 ESP”似乎很奇怪,尤其是在非 Windows / MSVC 问题中。 OP 使用 AT&T 语法和 x86-64 System V ABI(RSI 中的第一个参数)。 gcc / Linux 经常使用 i386 与 amd64(或 x86-64)来区分 32 位与 64 位 x86。 x64 几乎专门用于 Windows / Microsoft 术语。
  • 无论如何,i386 System V ABI 多年来也需要 16B 堆栈对齐,所以这不是 x86-64 特有的。 (x86 tag wiki 中的 ABI 链接。)
  • 对 SO 的强烈错觉是 16 位代码仍然相关。由无法完成家庭作业的学生创建。我不是那么热衷于做任何事情来促进这种幻想。
  • @PeterCordes 16 位代码很有趣!目前正在使用我的 80286 保护模式操作系统。
【解决方案2】:

根据 x86-64 ABI,堆栈必须在子例程调用之前对齐 16 字节。

leaq (mem), reg

等价于以下

reg = &(*mem) = mem

【讨论】:

    猜你喜欢
    • 2013-10-29
    • 1970-01-01
    • 2015-07-15
    • 2020-11-10
    • 1970-01-01
    • 2012-04-22
    • 2013-03-26
    • 2014-03-16
    相关资源
    最近更新 更多