【问题标题】:Function epilogue in assembly装配中的功能结语
【发布时间】:2016-07-29 19:31:46
【问题描述】:

我正在尝试跟随我的书的作者,他给我们提供了带有序言和尾声的示例函数(函数中没有局部变量)

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

我的假设是,只有当我们有被压入堆栈的局部变量时才需要这一行,这将是一种清除这些局部变量堆栈的方法?

我想澄清一下,情况就是这样

【问题讨论】:

  • 是的,它是多余的。但这不是唯一的事情。在这种情况下,不需要设置堆栈框架的整个业务。
  • @Jester 这是真的。我只是假设他正在对我们如何设置堆栈框架进行基本介绍。当示例函数设置为没有局部变量时,我只是想弄清楚为什么我们有第 8 行的指令。谢谢!
  • 相当于 C 语言中的 start 和 end 块,即使没有局部变量,它仍然存在。堆栈必须在函数使用后保持有序。
  • 当你使用局部变量时,通常第3行是sub esp,local_memory_size。在那一刻,第 8 行是如何恢复 esp 的快速简单的方法。但是在您的示例中(不修改 esp)它是多余的,正如您正确怀疑的那样。 (这都是类似 C 的堆栈帧操作,我很少在纯 ASM 中使用它,我宁愿使用寄存器、固定缓冲区和完全配对的 push/pop 堆栈指令,但这些知识在调用外部 ABI/API 时可能会有所帮助,它使用堆栈帧进行参数传递)。

标签: assembly x86


【解决方案1】:

你是对的,这是多余的如果你知道esp已经指向你推送你的呼叫者的ebp的位置。


当 gcc 使用 -fno-omit-frame-pointer 编译函数时,它实际上会执行您建议的优化,即在知道 esp 已经指向正确的位置时弹出 ebp

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

例如在Godbolt compiler explorer 上,此功能的clang 3.8 输出(带有-O3 -m32)。通常,编译器并不能完全生成最佳代码:

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

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

# 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

由于堆栈需要在 call 之前对齐 16(根据 SystemV i386 ABI 的规则,请参阅 标签 wiki 中的链接),我们不妨保存/恢复一个额外的 reg,而不仅仅是push [ebp+8],然后(在通话后)mov eax, [ebp+8]。编译器倾向于保存/恢复调用保留的寄存器而不是多次重新加载本地数据。

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

# 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

gcc 实际上会使用leave 而不是 mov / pop,以防在弹出ebx 之前确实需要修改esp。例如,flip Godbolt to gcc (instead of clang), and take out -m32,所以我们正在为 x86-64 进行编译(其中 args 在寄存器中传递)。这意味着调用后不需要从堆栈中弹出 args,因此 rsp 被正确设置为仅弹出两个 regs。 (推送/弹出使用 8 个字节的堆栈,但 rsp 在 SysV AMD64 ABI 中的 call 之前仍然必须是 16B 对齐的,所以 gcc 实际上在 call 周围做了一个 sub rsp, 8 和相应的 add .)

另一个错过的优化:使用gcc -m32,可变长度数组函数在调用后使用add esp, 16 / leaveadd 完全没用。 (将 -m32 添加到 Godbolt 上的 gcc 参数中)。

【讨论】:

    【解决方案2】:

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

    【讨论】:

      猜你喜欢
      • 2013-02-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-07-14
      • 2011-03-14
      • 2018-01-07
      相关资源
      最近更新 更多