汇编中的函数结语

X33*_*X33 2 x86 assembly

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

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
Run Code Online (Sandbox Code Playgroud)

push eax     ; param 2
push ecx     ; param 1
call addme
add esp, 8   ; cleanup stack
Run Code Online (Sandbox Code Playgroud)

在这个例子中,第 8 行不是冗余指令吗?我的意思是EBPESP在这种情况下是不是已经等于?没有一直PUSHPOP在由于堆栈。

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

我想澄清一下,情况确实如此

Pet*_*des 5

你是对的,如果你知道它esp已经指向你推送调用者的ebp.


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

这在使用调用保留寄存器(如ebx)的函数中很常见,这些寄存器也必须像ebp. 编译器通常在序言/尾声中进行所有保存/恢复,然后再为 C99 可变大小数组保留空间。所以pop ebx总会离开esp指向正确的地方pop ebp

例如-O3 -m32,在Godbolt 编译器资源管理器上,此函数的clang 3.8 输出(带有)。通常,编译器不会生成最佳代码:

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
Run Code Online (Sandbox Code Playgroud)

当然是人类(借用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
Run Code Online (Sandbox Code Playgroud)

由于堆栈需要在 a 之前对齐 16 call(根据 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
Run Code Online (Sandbox Code Playgroud)

gcc 将实际使用leave而不是 mov / pop,在它确实需要esp在 popping 之前修改的情况下ebx。例如,将Godbolt 翻转到 gcc(而不是 clang),然后取出,-m32以便我们针对 x86-64 进行编译(其中 args 在寄存器中传递)。这意味着调用后无需从堆栈中弹出 args,因此rsp正确设置为仅弹出两个 reg。(push/pop 使用 8 个字节的堆栈,但在 SysV AMD64 ABI 中的rspa 之前仍然必须是 16B 对齐的call,因此 gcc 实际上在.周围做了 asub rsp, 8和对应。)addcall

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