为什么在功能序言/尾声中使用ebp?

use*_*913 6 c optimization x86 assembly

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

    push ebp
    mov ebp, esp
    (sub esp, 4
    ...
    mov esp, ebp)
    pop ebp
Run Code Online (Sandbox Code Playgroud)

只是跳过这一切,然后点击esp,就像

    mov eax, [esp+4]          ;; take argument
    mov [esp-4], eax          ;; use some local variable storage
Run Code Online (Sandbox Code Playgroud)

它似乎工作得很好.为什么使用这个ebp - 可能通过ebp更快或更快地解决?

Mic*_*ael 9

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


Iri*_*ium 9

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

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

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);
}
Run Code Online (Sandbox Code Playgroud)

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

此外,因为EBP除非你明确地更新它ESP,否则不会改变(不像,当你push/ 时会改变pop),通常更容易引用堆栈上的数据EBP,而不是相对于ESP,因为对于后者,你必须知道在函数的开头和引用之间可能已调用的任何push/ pop指令.

正如其他人所提到的,你应该避免使用下面的 堆栈地址,ESP因为call你对其他函数所做的任何操作都可能会覆盖这些地址的数据.您应该通过以下方式在堆栈上保留空间以供您的函数使用:

sub esp, [number of bytes to reserve]
Run Code Online (Sandbox Code Playgroud)

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

add esp, [number of bytes reserved]
Run Code Online (Sandbox Code Playgroud)

  • @ user2214913正如DCoder所说,你可能不会通过编写程序集来生成更快的代码,而且我在使用它之前省去堆栈帧而不保留堆栈空间所带来的任何速度提升都会因你引入的低效率而相形见绌.您提出这个非常基本的问题的事实表明,您的代码可能不是为避免缓存未命中缓慢而编写的,或者利用特定于CPU的指令排序来实现并行执行.一个体面的编译器将意识到这些事情,并且通常会产生比手动汇编更快的代码. (3认同)
  • @ user2214913:你正在进入过早的优化.在一个非平凡的程序中,你的代码所做的任何有意义的工作都会使你以这种方式削减的周期相形见绌.需要调试代码的人不会喜欢这个. (2认同)
  • 很容易尝试使用C/C++代码库,让编译器不使用帧指针(`-fomit-frame-pointer`等与gcc样式编译器一起使用),看看它有什么样的真实性能差异.使用i386,通过正确的代码库可以获得高达一位数的性能提升,主要是因为有一个额外的通用寄存器可用(不是因为序言/尾声中的额外指令) - i386寄存器很差.使用x86-64 ISA,寄存器数量较多,搞乱这些东西几乎没有什么好处. (2认同)