汇编中的函数结尾

时间:2016-07-29 00:26:48

标签: assembly x86

我试图跟随我的书的作者,他给我们带有序言和结语的示例函数(函数中没有局部变量)

1:    push ebp
2:    mov ebp, esp
3:    ...
4:    movsx eax, word ptr [ebp+8]
5:    movsx eax, word ptr [ebp+0Ch]
6:    add eax, ecx
7:    ...
8:    mov esp, ebp
9:    pop ebp
10:   retn

调用
push eax     ; param 2
push ecx     ; param 1
call addme
add esp, 8   ; cleanup stack

在此示例中,第8行不是冗余指令吗?我的意思是,EBP在这种情况下还不等于ESP吗?自从。

以来,堆栈中没有PUSHPOP

我的假设是,如果我们将局部变量推入堆栈,那么这条线只是必需的,这是一种清除那些局部变量堆栈的方法吗?

我想澄清一下情况

2 个答案:

答案 0 :(得分:4)

你是正确的,它是多余的如果你知道esp已经指向你推动来电者{{1}的位置}}

当gcc使用ebp编译一个函数时,实际上它确实会在知道-fno-omit-frame-pointer已经指向正确的位置时执行优化而只是弹出ebp。< / p>

这在使用调用保留寄存器(如esp)的函数中很常见,这些寄存器也必须像ebx一样保存/恢复。在为C99可变大小数组保留空间之前,编译器通常会在序言/结尾中执行所有保存/恢复。因此ebp始终会pop ebx指向esp的正确位置。

e.g。在Godbolt compiler explorer上为此函数输出3.8的输出(pop ebp)。通常情况下,编译器并没有做出最佳代码:

-O3 -m32

当然是一个人(借用gcc的技巧)

void extint(int);   // a function that can't inline because the compiler can't see the definition.
int save_reg_framepointer(int a){
  extint(a);
  return a;
}

    # clang3.8
    push    ebp
    mov     ebp, esp                     # stack-frame boilerplate
    push    esi                          # save a call-preserved reg
    push    eax                          # align the stack to 16B
    mov     esi, dword ptr [ebp + 8]     # load `a` into a register that will survive the function call.
    mov     dword ptr [esp], esi         # store the arg for extint.  Doing this with an ebp-relative address would have been slightly more efficient, but just push esi here instead of push eax earlier would make even more sense
    call    extint
    mov     eax, esi                     # return value
    add     esp, 4                       # pop the arg
    pop     esi                          # restore esi
    pop     ebp                          # restore ebp.  Notice the lack of a mov  esp, ebp here, or even a  lea esp, [ebp-4]  before the first pop.
    ret

由于堆栈需要在# hand-written based on tricks from gcc and clang, and avoiding their suckage call_non_inline_and_return_arg: push ebp mov ebp, esp # stack-frame boilerplate if we have to. push esi # save a call-preserved reg mov esi, dword [ebp + 8] # load `a` into a register that will survive the function call push esi # replacing push eax / mov call extint mov eax, esi # return value. Could mov eax, [ebp+8] mov esi, [ebp-4] # restore esi without a pop, since we know where we put it, and esp isn't pointing there. leave # same as mov esp, ebp / pop ebp. 3 uops on recent Intel CPUs ret 之前对齐16(根据SystemV i386 ABI的规则,请参阅标签wiki中的链接),我们不妨保存/恢复一个额外的注册,而不仅仅是call然后(在通话后)push [ebp+8]。编译器倾向于保存/恢复调用保留的寄存器,而不是多次重新加载本地数据。

如果不是当前版本的ABI中的堆栈对齐规则,我可能会写:

mov eax, [ebp+8]

gcc实际上会使用leave而不是mov / pop,如果它需要在弹出# hand-written: esp alignment not preserved on the call call_no_stack_align: push ebp mov ebp, esp # stack-frame boilerplate if we have to. push dword [ebp + 8] # function arg. 2 uops for push with a memory operand call extint # esp is offset by 12 from before the `call` that called us: return address, ebp, and function arg. mov eax, [ebp+8] # return value, which extint won't have modified because it only takes one arg leave # same as mov esp, ebp / pop ebp. 3 uops on recent Intel CPUs ret 之前修改esp。例如,flip Godbolt to gcc (instead of clang), and take out -m32所以我们正在为x86-64编译(其中args在寄存器中传递)。这意味着在呼叫之后不需要从堆栈弹出args,因此ebx被正确设置为只弹出两个regs。 (push / pop使用8个字节的堆栈,但rsp仍需要在SysV AMD64 ABI中的rsp之前进行16B对齐,因此gcc实际上会执行call并且相应的{{围绕sub rsp, 8。}

另一个错过的优化:使用add,变量长度数组函数在调用后使用call / gcc -m32add esp, 16完全没用。 (将-m32添加到godbolt上的gcc args)。

答案 1 :(得分:3)

你不知道第3行和第7行是什么。所以我假设第8行在一般情况下并不是多余的。通常它应该在没有第8行的情况下工作,因为函数末尾的ESP值通常与函数开头的值相同。但我可以想象一些肮脏的场景,其中第8行用于清理某些东西,例如,如果你进行按键调用序列并省略最终的ADD ESP,n行。然后你可以简单地使用MOV ESP,EBP在你的功能结束时修复ESP。肮脏但工作。