不需要使用堆栈帧,但肯定有一些优点:
首先,如果每个函数都使用相同的过程,我们可以利用这些知识通过反转过程轻松确定调用序列(调用堆栈)。我们知道,在一条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
),通常更容易引用堆栈上相对于 的数据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]