我是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
之前重新弄脏堆栈引擎。 但这是微不足道的。