9

前段时间我在尝试编写汇编例程并将其与 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更快的地址还是什么?

4

3 回答 3

11

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

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

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

sub esp, [number of bytes to reserve]

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

add esp, [number of bytes reserved]
于 2013-03-27T09:58:59.157 回答
10

在调试代码时使用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

于 2013-03-27T09:47:52.320 回答
4

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

于 2013-03-27T09:35:44.070 回答