在一种情况下,它在局部变量之前完成。
_start 不是函数。这是你的切入点。没有返回地址,也没有调用者的值%ebp 可以保存。
i386 System V ABI doc 建议(在 2.3.1 初始堆栈和寄存器状态部分)您可能希望将 %ebp 归零以标记最深的堆栈帧。 (即在您的第一个 call 指令之前,因此当第一个函数推送归零的 ebp 时,保存的 ebp 值的链接列表具有 NULL 终止符。见下文)。
C 在创建机器码时是否有特定的约定?
不,与其他一些 x86 系统不同,i386 System V ABI 不需要太多关于您的堆栈框架布局。 (Linux 使用 System V ABI / 调用约定,而您使用的书 (PGU) 是针对 Linux 的。)
在某些调用约定中,设置ebp 不是可选的,函数入口序列必须将ebp 推到返回地址的正下方。这会创建一个堆栈帧的链表,它允许异常处理程序(或调试器)回溯堆栈。 (How to generate the backtrace by looking at the stack values?)。我认为这在 SEH(结构化异常处理)的 32 位 Windows 代码中是必需的,至少在某些情况下,但 IDK 细节。
i386 SysV ABI 定义了一种用于堆栈展开的替代机制,它使帧指针成为可选的,使用另一个部分中的元数据(.eh_frame and .eh_frame_hdr 包含由.cfi_... 汇编程序指令创建的元数据,理论上你可以根据需要自己编写通过您的函数展开堆栈以使其工作。即,如果您正在调用任何期望 throw 工作的 C++ 代码。)
如果你想在当前的 gdb 中使用传统的 frame-walking,你必须自己定义一个 GDB 函数,如 gdb backtrace by walking frame pointers 或 Force GDB to use frame-pointer based unwinding。或者显然,如果您的可执行文件根本没有 .eh_frame 部分,gdb will use the EBP-based stack-walking method。
如果您使用 gcc -fno-omit-frame-pointer 进行编译,您的调用堆栈将具有此链表属性,因为当 C 编译器 生成正确的堆栈帧时,它们会推送 @987654344先@@。
IIRC,perf 有一种模式用于在分析时使用帧指针链获取回溯,显然这比默认的.eh_frame 更可靠,可以正确计算哪些函数负责使用最多 CPU时间。 (或者导致最多的缓存未命中、分支错误预测,或者其他任何你用性能计数器计算的东西。)
即使我将它们混合在程序的不同功能中,两者都不会起作用吗?一个函数在之前执行,另一个在之后执行,等等。
是的,它可以正常工作。事实上setting up ebp at all is optional,但是当手写时,更容易有一个固定的底座(不像esp,它会在你推/弹出时移动)。
出于同样的原因,在一次推送后(旧的%ebp)更容易坚持mov %esp, %ebp 的约定,因此第一个函数arg 始终位于ebp+8。通常的约定见What is stack frame in assembly?。
但是您可以通过将ebp 指向您保留的一些空间的中间来节省代码大小,因此可以使用ebp + disp8 寻址模式寻址的所有内存。 (disp8 是有符号的 8 位位移:如果我们限制为 4 字节对齐的位置,则为 -128 到 +124)。这节省了代码字节,而不是需要 disp32 才能到达更远的地方。所以你可能会这样做
bigfunc:
push %ebp
lea -112(%esp), %ebp # first arg at ebp+8+112 = 120(%ebp)
sub $236, %esp # locals from -124(%ebp) ... 108(%ebp)
# saved EBP at 112(%ebp), ret addr at 116(%ebp)
# 236 was chosen to leave %esp 16-byte aligned.
或者延迟保存任何寄存器,直到为本地人保留空间之后,这样我们就不会使用我们不想处理的保存值的任何位置(除了 ret addr)。
bigfunc2: # first arg at 4(%esp)
sub $252, %esp # first arg at 252+4(%esp)
push %ebp # first arg at 252+4+4(%esp)
lea 140(%esp), %ebp # first arg at 260-140 = 120(%ebp)
push %edi # save the other call-preserved regs
push %esi
push %ebx
# %esp is 16-byte aligned after these pushes, in case that matters
(记住要小心你如何恢复寄存器和清理。你不能使用leave,因为esp = ebp不正确。使用“正常”堆栈帧序列,你可能会恢复其他推送的寄存器(从在保存的 EBP 附近)使用mov,然后使用leave。或恢复esp 指向最后一次推送(使用add),并使用pop 指令。)
但如果你要这样做,使用ebp 代替ebx 或其他东西没有任何优势。事实上,使用ebp 有一个缺点:0(%ebp) 寻址模式需要 disp8 为 0,而不是没有位移,但 %ebx 不会。所以使用%ebp 作为非指针暂存寄存器。或者至少一个你不会在没有位移的情况下取消引用。 (这个怪癖与真正的帧指针无关:(%ebp) 是保存的 EBP 值。顺便说一句,意味着 (%ebp) 没有位移的编码是 ModRM 字节如何编码没有基址寄存器的 disp32,如 @987654372 @或my_label)
这些例子是相当人为的;除非它是一个数组,否则您通常不需要太多的本地空间空间,然后您将使用索引寻址模式或指针,而不仅仅是相对于ebp 的 disp8。但也许您需要一些 32 字节 AVX 向量的空间。在只有 8 个向量寄存器的 32 位代码中,这是有道理的。
不过,AVX512 压缩的 disp8 在很大程度上击败了 64 字节 AVX512 向量的这个论点。 (但 32 位模式下的 AVX512 仍然只能使用 8 个向量寄存器,zmm0-zmm7,因此您很容易需要溢出一些。您只能在 64 位模式下获得 x/ymm8-15 和 zmm8-31。)