清理堆栈时,RET可以使用动态值(寄存器或存储器)吗?

时间:2016-07-14 11:26:31

标签: assembly x86 stack return calling-convention

我使用NASM,我对ret指令有疑问。我知道我应该在ret我发送到堆栈的变量之后指定,但我该如何动态地执行此操作?我试过了:

ret eax

ret dword[var]

但这些都不起作用。有没有办法做到这一点?

2 个答案:

答案 0 :(得分:3)

没有操作码可以做到这一点,唯一可用的操作码要求数字为常数。

理论上你可以将返回值弹出到一个寄存器中,然后根据你的动态值调整堆栈指针,推送返回地址和RET但是它有点复杂。

你需要这样做的原因是什么?通常,返回地址之前的堆栈中的任何值都属于调用函数,并且它们的工作就是处理它们。

答案 1 :(得分:2)

如果您不知道当函数被称为作为汇编时常量时被压入堆栈的字节数,那么您需要使用不同的调用约定。具体来说,一个是调用者清理,而不是被调用者清理。

为了让被调用者清理,它需要准确知道调用者推入堆栈的字节数。在处理常规参数化函数时,这不是问题。函数签名可以准确地告诉您它所需的参数类型以及数量。例如:

int FrobWidget(void* pWidget, int timesToFrob);

假设指针是64位且ints在假设架构上是32位,则此函数知道调用者在调用它之前将12个字节压入堆栈。因此,FrobWidget函数将以ret 12结束,从堆栈中弹出这12个字节并返回给调用者。

最常见的被调用者清理约定称为stdcall,它在Windows编程中被大量使用。几乎所有Windows API函数都是stdcall。原因主要是历史性的,但如果你是一个微优化爱好者,今天仍然很重要。基本上,这个想法是一个函数被调用的次数比它定义的多很多次,所以让函数本身清理堆栈将导致代码比每个调用者在每次调用函数后清理堆栈要少得多。鉴于这一事实以及事实上的平台标准,我使用stdcall作为我编写的所有Windows程序的默认调用约定。

但是,正如您所见(并且Sami已经指出),动态无法清理堆栈。 ret instruction有两种形式(忽略远近变体):一个不带参数,一个带立即参数。您不能使用已注册的值或内存操作数。这就是为什么函数必须知道从堆栈弹出多少字节作为汇编时常量。

与callee清理约定一样酷,它们确实有这个显着的限制,这表明它无法对 variadic 函数使用这种调用约定。变量函数是具有不确定arity的函数;简单来说,它们采用可变数量的参数,并且在汇编时不知道精确的数字。实际上,参数的实际数量和类型只能由调用者知道。这就是可变函数需要调用者清理惯例的原因。

C中可变函数的标准示例是printf

int printf(const char* pFormat, ...);

与之前的案例不同,函数签名并没有告诉我们任何确定的内容。我的意思是,它有一个friggin'省略!该函数接受调用者决定传递的参数。这意味着只有调用者知道需要从堆栈中弹出多少字节才能在调用后清理它。因此printf函数只返回ret,并让调用者处理清理。

最常见的调用者清理约定称为cdecl,因为它是传统的C调用约定。它广泛用于Linux和其他非Windows平台,它在Windows上使用必须,用于可变函数,如{{3 API)。

这两个调用约定(stdcallcdecl)从右到左传递堆栈上的参数,因此主要区别在于被调用者或调用者是否清理堆栈。让被调用者清理堆栈需要它在汇编时确切地知道堆栈上传递了多少字节。如果不知道,必须使用cdecl之类的来电清理惯例。