前段时间我正在尝试编写汇编例程并将其与 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
更快地解决还是什么?
不需要使用堆栈帧,但肯定有一些优点:
首先,如果每个函数都使用相同的过程,我们可以使用这些知识通过反转过程来轻松确定调用序列(调用堆栈(。我们知道,在call
指令之后,ESP
指向返回地址,被调用函数要做的第一件事就是push
当前EBP
,然后将ESP
复制到EBP
中。因此,在任何时候,我们都可以查看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: %sn", 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]
之间的堆栈区域可以安全使用。在退出函数之前,您必须使用匹配释放保留的堆栈空间:
add esp, [number of bytes reserved]
在调试代码时,使用 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
它可以工作,但是,一旦您遇到中断,处理器就会将其所有寄存器和标志推送到堆栈中,从而覆盖您的值。堆栈的存在是有原因的,使用它...