【问题标题】:Why is it better to use the ebp than the esp register to locate parameters on the stack?为什么使用 ebp 比使用 esp 寄存器在堆栈上定位参数更好?
【发布时间】:2019-06-18 04:35:27
【问题描述】:

我是 MASM 的新手。我对这些指针寄存器感到困惑。如果你们能帮助我,我将不胜感激。

谢谢

【问题讨论】:

  • 如果您使用标准堆栈帧,ebp 在序言中被初始化并在函数执行期间保持固定。因此,人们和调试器发现它更方便。使用esp 可以释放ebp 用于一般用途,您也可以保持esp 不变,除非您需要动态堆栈数据(alloca)。

标签: assembly x86 cpu-architecture callstack masm32


【解决方案1】:

使用[ebp + disp8] 编码寻址模式比[esp+disp8] 短一个字节,因为使用ESP 作为基址寄存器需要一个SIB 字节。有关详细信息,请参阅rbp not allowed as SIB base?。 (该问题的标题是询问[ebp] 必须编码为[ebp+0]。)

在推入或弹出之后或call 之后首次使用[esp + disp8],将需要英特尔 CPU 上的堆栈同步微指令。 (What is the stack engine in the Sandybridge microarchitecture?)。当然,mov ebp, esp 首先制作堆栈帧也会触发堆栈同步 uop:在乱序核心(不仅仅是寻址模式)中对 ESP 的任何显式引用都会导致堆栈同步 uop,如果堆栈引擎可能具有乱序后端不知道的偏移量。


使用ebp 的传统堆栈帧设置会创建堆栈帧的链接列表(每个保存的 EBP 都指向父级保存的 EBP,位于返回地址的正下方),如果您的代码没有,则便于分析和调试没有备用元数据让您的调试器展开堆栈以显示堆栈回溯。


尽管使用 ESP 有这些缺点,但使用 EBP 作为帧指针通常并不更好(就性能而言),因为它使用了 8 个 GP 寄存器中的一个额外的堆栈,剩下 6 个而不是 7 个,您实际上可以将其用于堆栈以外的东西。现代编译器在启用优化时默认为 -fomit-frame-pointer

编译器很容易跟踪 ESP 相对于他们存储内容的位置移动了多少,因为他们知道sub esp,28 移动了堆栈指针多少。即使在 push 函数 arg 之后,他们仍然知道他们在函数早期存储在堆栈中的任何内容的正确 ESP 相对偏移量。

人类也可以这样做,但是当您修改函数以保留一些额外空间而忘记更新从 ESP 到本地变量和堆栈参数(如果有)的所有偏移量时,很容易出错。 (不过,通常不值得手动编写无法将大部分变量保存在寄存器中的大型函数。将其留给编译器,只花时间在 asm 中编写热循环,如果有的话。)

例外情况是,如果您的函数分配了可变数量的堆栈空间(如 C alloca 或 C99 可变长度数组,如 int arr[n];在这种情况下,编译器将使用 EBP 制作传统的堆栈帧。或者在手写 asm 中,如果你 push 在循环中使用调用堆栈作为 Stack 数据结构。


例如 x86 MSVC 19.14 编译这个 C

int foo() {
    volatile int i = 0;  // force it to be stored to memory
    return i;
}

进入这个 MASM 汇编。 (See it yourself on the Godbolt compiler explorer)

;;; MSVC -O2
_i$ = -4                                                ; size = 4
int foo(void) PROC                                        ; foo, COMDAT
        push    ecx
        mov     DWORD PTR _i$[esp+4], 0           ; note this is actually [esp+0] ; _i$ = -4
        mov     eax, DWORD PTR _i$[esp+4]
        pop     ecx
        ret     0
int foo(void) ENDP                                        ; foo

请注意,它使用push 而不是sub esp, 4i 保留空间,因为这样可以节省代码大小并且通常具有相同的性能。前端的微指令数量相同,没有额外的堆栈同步微指令,因为push 位于对esp 的任何显式引用之前,而pop 位于最后一个之后。

(如果它保留超过 4 个字节,我认为它只会使用普通的sub esp, 8 或其他。)

这里有一个明显遗漏的优化; push 0 将存储它实际想要的值,而不是 ECX 中的任何垃圾。 (What C/C++ compiler can use push pop instructions for creating local variables, instead of just increasing esp once?)。而pop eax 将清理堆栈加载i 作为返回值。

对比this 禁用了优化。 请注意,_i$ = -4 与“堆栈帧”的偏移量相同,但优化后的代码使用 esp+4 作为基础,而 this 使用 ebp。这主要只是 MSVC 内部的一个有趣事实,它似乎是根据 EBP 的位置来考虑的,如果它没有优化掉帧指针的创建。选择一个参考点是有道理的,并且与其支持帧指针的选择对齐是显而易见的选择。

;;; MSVC -O0
_i$ = -4                                                ; size = 4
int foo(void) PROC                                        ; foo
        push    ebp
        mov     ebp, esp                     ; make a stack frame
        push    ecx
        mov     DWORD PTR _i$[ebp], 0
        mov     eax, DWORD PTR _i$[ebp]
        mov     esp, ebp
        pop     ebp
        ret     0
int foo(void) ENDP                                        ; foo

有趣的是,它仍然使用 push/pop 来保留 4 字节的堆栈空间。这一次它确实会在 Intel CPU 上导致一个额外的堆栈同步 uop,因为在 mov ebp,esp 之后的 push ecxmov esp, ebp 之前重新弄脏了堆栈引擎。但这很微不足道。

【讨论】:

    猜你喜欢
    • 2015-02-03
    • 2014-01-08
    • 2012-01-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-01-31
    • 2023-01-31
    • 2016-06-07
    相关资源
    最近更新 更多