【问题标题】:How to avoid using PUSH without POP?如何避免在没有 POP 的情况下使用 PUSH?
【发布时间】:2021-09-08 01:32:09
【问题描述】:

我目前正在手动编写 x86 程序集 (FASM),我经常犯的一个典型错误是 push 堆栈上的一个参数,但在 pop 执行之前返回。

这会导致调用者的堆栈偏移量发生变化,从而导致程序崩溃。

这是一个粗略的例子来演示它:

proc MyFunction
    ; A loop:
    mov     ecx, 100
.loop:
    push    ecx

    ; ==== loop content
    ...
    ; Somewhere, the decision is made to return, not just to exit the loop
    jmp    .ret
    ...
    ; ==== loop content

    pop     ecx
    loop    .loop

.ret:
    ret
endp

现在,显而易见的答案是在发出ret 之前从堆栈中弹出适当数量的元素。但是,在 1000 多条手工组装线中很容易忽略一些东西。

我也在考虑使用 pushad / popad 总是,但我不确定这个约定是什么。

问题:我可以遵循什么模式来避免这个问题?

【问题讨论】:

  • 使用标准堆栈帧(push ebp; mov ebp, esp 开头,mov esp, ebp; pop ebpleave 结尾)。但是,如果您无法跟踪您的推送,则无论如何您都无法使用这些东西,因此您应该更加注意:)
  • 可以使用普通的function prologue and epilogue恢复esp。但是你推送的寄存器不会被恢复。在汇编中你必须做正确的事情,实际上没有任何高级技巧。
  • 一种可能性是首先将所有被调用者保存的寄存器压入堆栈,然后建立堆栈帧。虽然不符合标准约定,但即使您忘记了推送的内容,也可以正确清理堆栈。
  • 我已经用 macros 解决了这个问题,它替换了原生的 PROC/ENDP。它们隐藏了调用约定的序言和尾声,还允许为过程的参数、本地堆栈变量和返回结果值的寄存器提供正式的描述性名称,请参阅Procedure
  • 如果您无法跟踪您的推送和弹出,请不要在汇编程序中编程。

标签: assembly x86 stack-memory fasm stack-frame


【解决方案1】:

通常不要在循环内使用push/pop;像编译器一样使用mov,这样您就不会不必要地移动ESP。 (如果/当您为其他本地人明确引用 ESP 时,这可能会导致额外的 stack-sync uops。)

或者在这种情况下,只需为两个不同的循环选择一个不同的寄存器,或者在保留一些空间后将外部循环计数器完全保留在内存中。 (sub dword [esp], 1 / jnz .outer_loop。或者 [ebp-4],如果您将 EBP 设置为帧指针,而不是仅将其用作另一个调用保留寄存器。)

围绕循环内的某些内容溢出/重新加载寄存器是低效的。释放寄存器的第一步应该是在内存中保留只读的东西,如果它们不是非常经常需要的话。例如像inc edx / cmp edx, [esp+12] / jbe .outer_loop 这样的外部循环计数器避免了存储/重新加载。只有在寄存器用完时才将可变的东西保留在内存中,然后当然更喜欢不经常更改的东西。


在编译器生成的代码中,您通常只会在序言中看到推送,并沿着通向ret 的路径弹出。这使得它们很容易匹配。如果您需要保存另一个 call-preserved register 以在函数内部使用,或者为本地人保留更多堆栈空间,您可以更改函数顶部的推送顺序,然后更改返回路径中的结尾。

(您可以有多个方法退出函数,尤其是在不需要太多清理的情况下,尾部复制可能比在结尾的另一个副本中使用 jmp 更好。)

您不必像编译器那样严格自律(或死脑筋),毕竟,您在 asm 中手动编写以获得更好的性能。 (对吗?否则,只需让编译器为您进行微优化,以生成“数千行”的 asm!中到大量代码是编译器真正发挥其快速分析数据流和编写相当不错代码的能力的地方。 )

因此,您可以例如使用 asm 堆栈作为堆栈数据结构;你无法说服编译器去做的事情。 (尽管Using the callstack to implement a stack data structure in C? 是不安全的尝试。)像pushpop,通过指针比较进行“空”检测。在这种情况下,如果您对堆栈内存有任何其他需求,您可能希望使用 EBP 作为帧指针。

【讨论】:

  • 阅读您的帖子和您链接的帖子,我认为我最大的错误是将循环计数器始终存储在 ecx 中,而不仅仅是在堆栈上。我只会为紧密的循环这样做。我还发现了一些地方,我将 API 返回值推送到堆栈上,以便在另一个 API 调用后弹出它。我会将这些值存储在堆栈中,并坚持在序言/尾声中专门使用 push/pop。感谢您的解释和您提供的链接!读起来很有趣:)
  • @bytecode77:loop 指令是slow anyway, except on AMD,因此使用 ECX 没有任何优势(除非优化代码大小而不是速度,或者在 ECX 方便的情况下专门针对 AMD)。在包含函数调用的循环中使用像 EBX 这样的调用保留寄存器作为循环计数器,这就是调用保留寄存器的要点。 (除非您需要所有 regs 甚至“更热”的变量)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多