我正在考虑函数调用如何在汇编程序中工作。 目前我认为它的工作原理如下:
push arguments on stack
push eip register on stack and setting new eip value over jump # call instruction
# callee's code
push ebp register on stack
working in the function
returning from function
pop ebp
pop eip # ret instruction
所以,但现在我在考虑它,汇编程序如何保存当前的堆栈指针?
例如,如果我有一些局部变量,esp(堆栈指针)会关闭,如果我回到main函数,汇编器必须将esp指针设置到正确的位置,但这是如何工作的?
答案 0 :(得分:1)
查看维基百科上的Calling conventions页面。
Stack before call:
0x8100 - +------------+ <- ESP
...... - | |
...... - | |
0x8000 - +------------+ <- EBP
...... - | |
...... - | Cur. Frame |
...... - | |
...... - +------------+
push arguments
push eip register on stack
push ebp register on stack
0x8100 - +------------+ <- ESP
...... - | |
...... - | |
0x8000 - +------------+
...... - | |
...... - | Old Frame |
...... - | |
...... - +------------+ <- EBP
...... - | Arguments |
...... - | EIP |
...... - | 0x8000 | <- Old EBP
...... - +------------+
pop ebp
pop eip
0x8100 - +------------+ <- ESP
...... - | |
...... - | |
0x8000 - +------------+ <- EBP
...... - | |
...... - | Frame | <- Current again frame!
...... - | |
...... - +------------+
...... - | |
...... - | Popped |
...... - | |
...... - +------------+
答案 1 :(得分:1)
很难弄清楚你错过了什么,但我认为你所缺少的是调用者必须在被调用函数返回后修复堆栈。 调用者知道它在调用之前推了多少,所以它可以在add esp, some_constant
指令之后call
清除堆栈中的args,将ESP恢复到第一次推送之前的位置< /强>
ESP在所有调用约定中都被调用保留。 ESP不允许使用不同于call
之前的ESP返回被调用的函数。如果它们以ret
返回,则只有在运行ret
之前将返回地址复制到堆栈中的其他位置时才会发生这种情况!所以这是一个非常明显的限制,一些调用约定描述没有提及。
无论如何,这意味着调用者可以假设ESP未被修改,因此它可以使用PUSH / POP保存/恢复其他任何内容。
EBP也在我所知的所有调用约定中被调用保留。有关调用约定/ ABI文档的信息,请参阅https://stackoverflow.com/tags/x86/info(x86标记wiki)。
还有calling conventions on Wikipedia个简短摘要。
此外,您的函数调用伪代码非常奇怪和令人困惑(在我编辑问题之前)。它没有清楚地显示调用者代码和被调用者代码之间的边界。在这个答案的先前版本中,我以为你说调用者的代码正在推动EBP,因为它出现在working in the function
行之前。
EIP无法直接访问,只能通过跳转指令进行修改。 CALL按下一个返回地址,然后跳转(注意它会推送 next 指令的地址,因此它在返回时不会再次运行。执行指令期间的EIP可以说是指向下一条指令,因为相对跳转是使用指令末尾的位移进行编码的。对于x86-64 RIP相对地址也是如此。)
RET弹出到EIP。为了使它返回正确的位置,代码必须恢复ESP以指向调用者推送的返回地址。
假设有一个32位的堆栈args调用约定,如System V i386,我会把你的伪代码编写为:
(optional) push ecx or whatever call-clobbered registers you want to save
push arguments on stack
CALL function (pushes a return address, i.e. the addr of the insn after the call)
# code of the called function
(optional) push ebp (and any other call-preserved regs the function wants to use)
working in the function
(optional) pop ebp (and any other regs, in reverse order of pushing)
RET (pops the return address into EIP)
add esp, 8 (for example) to clear args from the stack
(optional) pop ecx or whatever other volatile regs you want to restore
有时候看看编译器生成的asm是否有真正的函数,如下所示:
尝试使用不同的编译器选项或更改the Godbolt compiler explorer上的来源:
int extern_func(int a);
int foo() {
int a = extern_func(2);
int b = extern_func(5);
return a+b;
}
使用gcc6.2 -m32 -O3 -fno-omit-frame-pointer
进行编译,以制作32位代码,使用EBP,而不是默认的省略帧指针模式。我本来可以使用-O0
,但是未经优化的asm是如此臃肿以至于阅读起来很糟糕,gcc在这里做的事情并没有什么困惑。还使用-fverbose-asm
来标记操作数上的变量名。
foo:
push ebp
mov ebp, esp # standard prologue
push ebx # save ebx so we have a call-preserved register
sub esp, 16 # reserve space for locals
push 2 # the arg for the first function call
call extern_func
mov ebx, eax # a, # stash the return value where it won't be clobbered by the next call
mov DWORD PTR [esp], 5 # just write the new arg to the stack, instead of add esp, 4 and push 5
call extern_func #
add eax, ebx # tmp90, a # this is a+b as the return value
mov ebx, DWORD PTR [ebp-4] #, ESP isn't pointing to where we pushed EBX, so restore it with a normal MOV load.
leave # and set esp=ebp and pop ebp
# at this point, ESP is back to its value on entry to the function
ret
clang对如何做事做出了一些不同的选择(包括使用esi
而不是ebx
),然后使用
add eax, esi
add esp, 4
pop esi
pop ebp
ret
所以这是一个更“正常”的序列:恢复ESP指向序言中推送的寄存器并弹出它们,再次让ESP指向准备好RET的返回地址。