函数调用如何工作?

时间:2016-12-19 21:54:00

标签: function assembly x86 calling-convention

我正在考虑函数调用如何在汇编程序中工作。 目前我认为它的工作原理如下:

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指针设置到正确的位置,但这是如何工作的?

2 个答案:

答案 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标记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的返回地址。