为什么使用 ebp 寄存器比 esp 寄存器在堆栈上定位参数更好?

Why is it better to use the ebp than the esp register to locate parameters on the stack?

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

谢谢

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

第一次 [esp + disp8] 在 push 或 pop 之后,或者在 call 之后使用,在 Intel CPU 上需要一个 stack-sync uop。 (What is the stack engine in the Sandybridge microarchitecture?)。当然,mov ebp, esp 首先创建一个堆栈帧也会触发一个 stack-sync uop:在 out-of-order 核心(不仅仅是寻址模式)中对 ESP 的任何显式引用都会导致 stack-sync uop 如果堆栈引擎可能有一个 out-of-order 后端不知道的偏移量。


带有 ebp 的传统 stack-frame 设置创建了一个 linked-list 堆栈帧(每个保存的 EBP 指向 parent 保存的 EBP,就在 [= =113=] 地址),如果您的代码没有备用元数据让您的调试器展开堆栈以显示堆栈回溯,则方便进行分析和有时调试。


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

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

人类也可以做到,但是当您修改函数以保留一些额外的 space 并且忘记将 ESP 的所有偏移量更新到您的局部变量和堆栈参数时,很容易出错,如果任何。 (不过,通常情况下,hand-writing 不能将大部分变量保存在寄存器中的大型函数是不值得的。把它留给编译器,只花时间在 asm 中编写热循环,如果有的话。)

如果您的函数分配可变数量的堆栈space(如 C alloca 或 C99 可变长度数组,如 int arr[n];在那种情况下,编译器将使用 EBP 制作一个传统的堆栈框架。或者在hand-written 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

请注意,它为 i 保留 space,使用 push 而不是 sub esp, 4,因为这样可以节省 code-size 并且通常性能大致相同。 front-end 的微指令数相同,没有额外的 stack-sync 微指令,因为 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 作为 return 值。

对比这禁用了优化。 请注意,_i$ = -4 与 "stack frame" 的偏移量相同,但优化代码使用 esp+4 作为基数,而此代码使用 ebp.这主要只是 MSVC 内部结构的 fun-fact,它似乎在考虑如果 EBP 没有优化掉 frame-pointer 创建的情况。选择一个参考点是有道理的,并且与它的 frame-pointer 启用的选择对齐是显而易见的选择。

;;; 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字节栈space。这次它确实在 Intel CPU 上导致了一个额外的 stack-sync uop,因为 mov ebp,esp re-dirties 之后的 push ecxmov esp, ebp 之前的堆栈引擎。但这很微不足道。