没有要求使用堆栈帧,但肯定有一些优点:
首先,如果每个函数都使用相同的过程,我们可以利用这些知识通过反转过程轻松确定调用序列(调用堆栈)。我们知道在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]
在此之后,初始 ESP 和 ESP - [number of bytes reserved] 之间的堆栈区域可以安全使用。
在退出函数之前,您必须使用匹配释放保留的堆栈空间:
add esp, [number of bytes reserved]