【问题标题】:why to use ebp in function prologue/epilogue?为什么在函数序言/尾声中使用 ebp?
【发布时间】:2013-03-17 08:08:48
【问题描述】:

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

    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 寻址还是什么?

【问题讨论】:

标签: c optimization assembly x86


【解决方案1】:

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

首先,如果每个函数都使用相同的过程,我们可以利用这些知识通过反转过程轻松确定调用序列(调用堆栈)。我们知道在call指令之后,ESP指向了返回地址,被调用的函数首先要做的是push当前的EBP,然后将ESP复制到EBP。因此,我们可以随时查看EBP 指向的数据,这将是之前的EBP,而EBP+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 时会改变),相对于@987654335,引用堆栈上的数据通常更容易@,而不是相对于ESP,因为对于后者,您必须注意可能在函数开始和引用之间调用的任何push/pop 指令。

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

sub esp, [number of bytes to reserve]

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

add esp, [number of bytes reserved]

【讨论】:

  • 哈哈,:) 这很好(我的意思是特别是 LogStack 示例)我很抱歉我可以接受所有三个答案,因为它们都非常好:) 至于这件事,我个人喜欢做一个优化狂人(我想写尽可能快的 asm 例程)所以提示的是,如果这个 esp-4 overvrittens 没有发生,或者他们真的发生了? (上面可能有一些文字?有人吗?)如果是这样,我可以跳过我写的所有序言,但不要使用 esp-4,而是为本地人使用静态缓冲区 - 它应该是最高速度:U ye ?
  • @user2214913:您正在过早地进行优化。你的代码所做的任何有意义的工作,在一个非平凡的程序中,都会使你以这种方式减少的周期相形见绌。需要调试代码的人不会欣赏这一点。
  • @user2214913 正如 DCoder 所说,您可能不会通过编写汇编来生成更快的代码,而且我从省略堆栈帧并且在使用它之前不保留堆栈空间所获得的任何速度提升都将相形见绌由于您引入的效率低下。您提出这个非常基本的问题的事实表明,您的代码可能不是为了避免缓存未命中造成的减速,或者利用 CPU 特定的指令顺序来实现并行执行。一个体面的编译器会意识到这些事情,并且通常会生成比手动汇编更快的代码。
  • 用 C/C++ 代码库进行实验会很容易,让编译器不使用帧指针(-fomit-frame-pointer 和 gcc 样式编译器的类似物),看看什么样的实际性能它带来的不同。使用 i386,使用正确的代码库可以获得高达个位数的性能提升,主要是因为有一个额外的通用寄存器可用(不是因为序言/结尾中的额外指令) - i386 的寄存器很差.使用 x86-64 ISA,寄存器数量更多,搞乱这些东西没有什么好处。
【解决方案2】:

在调试代码时使用EBP 有很大帮助,因为它允许调试器遍历调用链中的堆栈帧。

它[创建]一个单向链表,将每个调用者的帧指针链接到一个函数。从例程的 EBP 中,您可以恢复函数的整个调用堆栈。

http://en.wikibooks.org/wiki/X86_Disassembly/Functions_and_Stack_Frames
特别是它链接到的页面涵盖了您的问题:http://blogs.msdn.com/b/larryosterman/archive/2007/03/12/fpo.aspx

【讨论】:

  • much tnx,这个关于 fpu 的页面确实是对这个问题的回答——但是 mellowcandla 说的事情也很有趣
  • 虽然我可以补充一点,我不明白这个链表是如何工作的,而且似乎没有 ebp 也可以检索此信息 - 如果函数可以返回(并返回并返回)那么你也可以追踪这条路径 - 没有 ebp
  • @user2214913:函数可以返回给它的调用者,因为返回地址存储在压入参数旁边的堆栈中。但是,调试器无法将返回地址与堆栈上的任何其他值区分开来,因此它无法扫描堆栈以查找之后的下一个返回地址。调试器也无法可靠地确定该函数在本地使用了多少堆栈空间并跳过它。认真阅读链接页面。
【解决方案3】:

它可以工作,但是,一旦你得到一个中断,处理器会将它的所有寄存器和标志压入堆栈,覆盖你的值。 堆栈存在是有原因的,使用它...

【讨论】:

  • 嗯,你确定吗?我使用了这个并且没有遇到错误,这只是运气的问题吗?如果是这样,我知道我可以使用它但不应该只使用 [esp-X] 值?或其他方式我可以将 esp 20 加 20 到它?
  • 您确定要从系统中覆盖此内容吗?上面有文字吗?
  • @Iridium 这可能是一个异步信号而不是真正的中断,不管是哪个。 esp 下面的所有内容都不受保护,并且可能被某些事件破坏。
  • 'mov [esp-4], eax' - 非常危险,是的。
  • 即使通过中断门发生堆栈切换,我对访问堆栈指针下方的位置也毫无信心。例如,如果访问产生页面错误,我不确定会发生什么。
猜你喜欢
  • 1970-01-01
  • 2014-05-15
  • 1970-01-01
  • 2012-12-27
  • 2014-11-11
  • 2012-04-03
  • 1970-01-01
  • 2011-07-25
相关资源
最近更新 更多