c - 为什么要在函数序言/结语中使用 ebp?

标签 c optimization assembly x86

前段时间我在尝试写汇编 例程并将其与 C 程序链接,我发现 我可以跳过标准的 C 调用序言结语

    push ebp
    mov ebp, esp
    (sub esp, 4
    ...
    mov esp, ebp)
    pop ebp

直接跳过,直接用esp地址,比如

    mov eax, [esp+4]          ;; take argument
    mov [esp-4], eax          ;; use some local variable storage

看起来效果不错。为什么使用这个 ebp - 也许是 通过 ebp 寻址更快还是什么?

最佳答案

没有要求使用堆栈框架,但肯定有一些优点:

首先,如果每个函数都使用相同的过程,我们可以利用这些知识通过反转过程轻松确定调用序列(调用堆栈)。我们知道在 call 指令之后,ESP 指向返回地址,被调用函数要做的第一件事就是 push 当前EBP 然后将ESP 复制到EBP。因此,在任何时候我们都可以查看 EBP 指向的数据,这将是之前的 EBPEBP+4 将是最后一次函数调用的返回地址。因此,我们可以使用类似(请原谅生锈的 C++)的方式打印调用堆栈(假设为 32 位):

void LogStack(DWORD ebp)
{
    DWORD prevEBP = *((DWORD*)ebp);
    DWORD retAddr = *((DWORD*)(ebp+4));

    if (retAddr == 0) return;

    HMODULE module;
    GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, (const char*)retAddr, &module);
    char* fileName = new char[256];
    fileName[255] = 0;
    GetModuleFileNameA(module, fileName, 255);
    printf("0x%08x: %s\n", retAddr, fileName);
    delete [] fileName;
    if (prevEBP != 0) LogStack(prevEBP);
}

然后这将打印出整个调用序列(好吧,它们的返回地址)直到那一点。

此外,由于 EBP 不会更改,除非您明确更新它(与 ESP 不同,它会在您 push/ 时更改pop),相对于 EBP,而不是相对于 ESP,通常更容易引用堆栈上的数据,因为对于后者,您必须注意可能在函数开始和引用之间调用的任何 push/pop 指令。

正如其他人所提到的,您应该避免使用下方 ESP 的堆栈地址,因为您对其他函数所做的任何调用 都可能会被覆盖这些地址的数据。您应该改为在堆栈上保留空间供您的函数按常规使用:

sub esp, [number of bytes to reserve]

在此之后,初始 ESPESP - [保留字节数] 之间的堆栈区域可以安全使用。 在退出您的函数之前,您必须使用匹配释放保留的堆栈空间:

add esp, [number of bytes reserved]

关于c - 为什么要在函数序言/结语中使用 ebp?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/15655553/

相关文章:

python - 如何在 cython 中指定列表和元组类型

c++ - 我是否需要在 1 核机器上明确禁用 OpenMP?

c - 互斥锁定和解锁功能如何防止 CPU 重新排序?

c - 客户端中的 OpenSSL Bio_gets

c - 将二维数组传递给常量参数的函数

c# - 使用 + 的字符串连接是否针对 .NET 中的 StringBuilder 实现进行了优化?

汇编:Y86 堆栈和调用、pushl/popl 和 ret 指令

c - JIT 基础知识

c - strncpy 导致 LPC-2378 挂起/死亡

c - Ubuntu GDB 将设置为 intel