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



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

谢谢

使用[ebp + disp8]编码寻址模式比[esp+disp8]短一个字节,因为使用ESP作为基本寄存器需要一个SIB字节。 看到 RBP 不允许作为 SIB 基础吗?了解详情。 (这个问题的标题是关于[ebp]必须编码为[ebp+0]的事实。

在推送或弹出后或call后首次使用[esp + disp8]将需要在英特尔 CPU 上进行堆栈同步 uop。 (Sandybridge微架构中的堆栈引擎是什么? 当然,首先创建堆栈帧mov ebp, esp也会触发堆栈同步 uop:如果堆栈引擎可能具有无序后端不知道的偏移量,则乱序内核中对 ESP 的任何显式引用(不仅仅是寻址模式)都会导致堆栈同步 uop。


传统的堆栈帧设置ebp创建堆栈帧的链接列表(每个保存的 EBP 都指向父级保存的 EBP,就在返回地址下方),便于分析,有时还可以调试,如果您的代码没有备用元数据,则有时还可以进行调试,这些元数据允许调试器展开堆栈以显示堆栈回溯。


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

Phoronix 在 Zen3 笔记本电脑 CPU 上用 x86-64 GCC12.1 测试了-O2 -fno-omit-frame-pointer的性能缺点,用于多个开源程序,正如 Fedora 37 系统范围所建议的那样,以使调试/性能更好地工作。

他们中的大多数都有几%的性能回归,其中一些非常严重(可能是在关键热点发生的某种坏事,可能不一定那么糟糕,但碰巧是。 没有一个变得更快。几何意味着在没有帧指针的情况下快 14%。

在 32 位模式下,除了堆栈指针之外只有 7 个寄存器,将 EBP 捆绑为帧指针比在 64 位模式下受到的伤害更大,在 64 位模式下,您要从 15 个整数寄存器到 14 个整数寄存器来玩。 因此,与 64 位代码相比,-m32 -O2 -fno-omit-frame-pointer代码的速度会显著降低。


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

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

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


例如,x86 MSVC 19.14 编译此 C

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

进入这个 MASM asm。 (自己在 Godbolt 编译器资源管理器上查看)

;;; 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保留了空间,push而不是sub esp, 4,因为这可以节省代码大小,并且通常具有相同的性能。 前端的 uop 数量相同,没有额外的堆栈同步 uop,因为push在任何显式引用之前esp,而pop在最后一个之后。

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

这里明显遗漏了优化;push 0将存储它实际想要的值,而不是ECX中的任何垃圾。 (哪个 C/C++ 编译器可以使用推送 pop 指令来创建局部变量,而不仅仅是增加一次 esp?pop eax将清理堆栈并将i加载为返回值。

与禁用优化的这个相比。请注意,_i$ = -4与"堆栈帧"的偏移量相同,但优化的代码使用esp+4作为基础,而这使用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 字节的堆栈空间。 这一次,它确实会在英特尔CPU上导致一个额外的堆栈同步uop,因为mov ebp,esp后的push ecx会在mov esp, ebp之前重新弄脏堆栈引擎。 但这是微不足道的。

最新更新