Function 函数调用是如何工作的?

Function 函数调用是如何工作的?,function,assembly,x86,calling-convention,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 e

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

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(堆栈指针)会下降,如果我回到主函数,汇编程序必须将esp指针设置到正确的位置,但这是如何工作的

看看维基百科上的页面

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     |
...... - |            |
...... - +------------+ 
调用前堆栈:
0x8100-+-----------+很难找出缺少了什么,但我认为缺少的是调用方必须在被调用函数返回后修复堆栈调用者知道在调用之前推送了多少,因此可以
调用
指令之后添加esp,一些常量
,以清除堆栈中的参数,将esp放回第一次推送之前的位置



ESP在所有调用约定中都保留调用。被调用的函数不允许返回与
调用之前不同的ESP。如果它们以
ret
返回,则只有在运行
ret
之前将返回地址复制到堆栈上的其他位置时,才会发生这种情况!所以这是一个非常明显的限制,一些调用约定的描述没有提到

无论如何,这意味着调用方可以假定ESP未被修改,因此它可以使用PUSH/POP保存/恢复任何其他内容

在我所知道的所有调用约定中,EBP也是保留调用的。有关调用约定/ABI文档的信息,请参见(标记wiki)

也用于简短的总结


另外,函数调用的伪代码非常奇怪和混乱(在我编辑问题之前)。它没有清楚地显示调用方代码和被调用方代码之间的界限。在这个答案的前一个版本中,我以为您是说调用方的代码正在推EBP,因为这是在函数
行中的
工作之前

EIP不能直接访问,只能通过跳转指令进行修改。呼叫推送一个返回地址,然后跳转(请注意,它会推送下一条指令的地址,因此返回时不会再次运行。可以说,在执行指令期间,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以获取实际函数,如下所示:

请尝试使用不同的编译器选项,或在以下位置更改源代码:

使用gcc6.2
-m32-O3-fno省略帧指针编译,以生成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指向返回地址,以便重试。

我使用的体系结构没有这样的寄存器。C标准也不需要堆栈。你的问题太广泛了。阅读编译器生成的汇编代码并重新编译如何阅读一本关于编译器构造的书?即使在编译器中,也可能有。ESP在所有调用约定中都保留调用,因此代码可以假定它没有被调用修改。EBP在我所知道的所有调用约定中也保留调用。请参阅可能会有所帮助。Re:您上次编辑删除了的伪代码中的my
和jump
de>call
:按下寄存器只读取它。例如,
push eax
不会修改eax,因此IDK why you's think
push eip
也会将eip设置为要调用的函数的地址。即使在将程序计数器公开为普通寄存器的类似ISA的ARM中,模拟调用也需要两条指令:一条指令将返回某个地方的地址,并跳转一个单独的地址。(请参阅。)谢谢,但是没有关于esp保存在何处的任何信息,因为我无法想象它保存在寄存器中,因为这样递归函数就无法工作。@assemblerMan:在函数项下,
esp
的当前值通常保存到
ebp
(基指针).
ebp
在函数的生命周期内不会发生变化。此链接仅用于回答问题。是的,我知道,但它会在函数结束时重置,但堆栈上也有函数的参数,在我的汇编源代码中,它们从未从堆栈中弹出。是的,我知道这是b但问题是esp在函数的末尾使用esp=ebp重置,然后pop ebp和pop eip获取executet,但是函数的函数参数永远不会被pop!因此它们应该在堆栈上,但不应该在堆栈上,因为esp应该回到旧状态,而这是在我的本地variables@assemblerMan:更新我的answer为您完全中断的步骤序列进行修复。这可能就是您感到困惑的原因。@assemblerMan:我不知道您不理解的部分是什么。当您或编译器生成调用函数的代码时,您总是知道这对ESP有何影响,因此您知道在
调用
之后要清除哪些代码。如果不行,你做错了。
    add     eax, esi
    add     esp, 4
    pop     esi
    pop     ebp
    ret