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

时间:2013-03-27 09:34:04

标签: c optimization assembly x86

前段时间我正在尝试编写程序集 例程并将其与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更快地解决什么?

3 个答案:

答案 0 :(得分: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

答案 1 :(得分:9)

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

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

EBP+4

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

此外,由于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都可能会覆盖这些地址的数据。您应该在堆栈上保留空间,以供您的功能通常使用:

call

在此之后,初始sub esp, [number of bytes to reserve] ESP之间的堆栈区域可以安全使用。 在退出函数之前,必须使用匹配:

释放保留的堆栈空间
ESP - [number of bytes reserved]

答案 2 :(得分:4)

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