【问题标题】:Usage of pusha/popa in function prologue/epilogue?在功能序言/尾声中使用 pusha/popa?
【发布时间】:2021-12-30 11:21:18
【问题描述】:

阅读汇编教程,我看到“功能”序言/尾声包含在:

push bp
mov bp, sp  
---
pop bp

但我也看到了一些其他教程,使用pusha 然后popa 来保留寄存器。 那么,为什么除了设置 bp 之外,函数 prologue/epilogue 不执行 pusha/popa 来保存寄存器上下文?

【问题讨论】:

  • pusha 和 popa 可用于满足这些调用约定。但唯一的优势似乎是代码大小:stackoverflow.com/questions/48449166/…
  • 序言/尾声只需履行约定即可。例如。 bp 寄存器不一定必须用于访问堆栈帧,您可以使用任何其他寄存器,但 bp 在用于寻址时具有默认为 SS 段的优势。
  • @8HoLoN 编译器知道它编译的每个函数使用哪个寄存器。所以它可以通过只保存它计划使用的那些寄存器来节省大量时间。此外,有些寄存器是调用者保存的,根本不需要保存。这比总是保存所有寄存器要好得多。
  • 这个问题只是关于 pusha/popa 还是为什么除了 bp 没有保存任何寄存器?根据调用约定,不必保留其他寄存器。根据功能实现,不会修改其他寄存器。所示的结尾/序言创建了一个标准的堆栈帧来访问堆栈上的参数和局部变量,以便相对于 bp 进行寻址。必须额外/手动完成寄存器的保存和恢复。
  • 所以你自己的语言 - 然后你可以创建你自己的规则!您的每个函数都可以有自己的调用约定,也可以没有。

标签: assembly x86-16 bootloader


【解决方案1】:

他们不保存所有寄存器,因为您并不总是需要保存所有寄存器。保存和恢复它们很慢。是的,这是一条小的单指令,看起来很节省,但需要时间和堆栈空间。要了解保存的内容,请查看调用约定。

https://en.wikipedia.org/wiki/X86_calling_conventions

PUSHA/PUSHAD - 推送所有通用寄存器

这些是说明。在 Skylake 上,PUSHA 需要 19 uop 和 8 个周期的吞吐量。 POPA 需要 18 uops 和 8c 的吞吐量。

另外,PUSHA/PUSHAD 在 64 位中无效。当 x86 扩展到 64b 时,它们被 AMD 和 Intel 正确清除。

现代编译器则相反,尽可能避免保存寄存器。 LLVM 执行称为收缩包装的分析,其中 prolog 被向前推进以允许快速提前退出。

https://llvm.org/doxygen/ShrinkWrap_8cpp_source.html

这些是可怕的、可怕的、不好的、非常糟糕的指示。

【讨论】:

    【解决方案2】:

    阅读汇编教程,我看到了“功能”序言/尾声...

    对于真正的汇编语言(您不必遵守不同语言的调用约定),“函数序言/结尾”这句话没有意义。

    对于“旨在遵守某些其他语言的调用约定的汇编语言”;您只需要保存/恢复一些寄存器(可能没有)。

    举个例子;对于 CDECL,EAX、ECX 和 EDX 的内容可以被被调用者丢弃,并且永远不需要被调用者保存/恢复(调用者需要保存它们,如果他们关心的话);如果一个函数不使用任何其他寄存器,则被调用者也不需要保存或恢复任何其他寄存器。另请注意,“EBP 作为帧指针”是过时的垃圾(它的存在是因为调试器不是很好,并且在改进调试信息时变得毫无意义 - 例如 DWARF 调试信息等)。这些东西结合起来意味着这样的事情对于 CDECL 来说是可以接受的序幕和尾声:

        myFunction:
                mov eax,12345      ;eax = returned value
                ret
    

    如果确实需要保存和恢复“大量”寄存器; pusha 很慢(微编码),一系列多条push 指令也很慢(存储的地址取决于最近修改的 ESP 中的值)。典型的方法是自己做,比如:

                            ;Don't bother saving EAX, ECX, EDX.
        sub esp,16          ;Create space to save 4 registers (but maybe more for local variables)
    
        mov [esp],ebx
        mov [esp+4],esi
        mov [esp+8],edi
        mov [esp+12],ebp
    

    但是;成本是空间。在代码大小非常有限(例如“512 字节的一部分”)的引导加载程序中,聪明的程序员将使用真正的汇编语言(其中“函数序言/结尾”没有意义),初学者可能会使用 pusha 来节省空间(没有意识到他们没有理由关心其他编程语言的调用约定)。

    【讨论】:

    • 所以你说,最好(从 cpu 性能 pov)手动为堆栈腾出空间 sp 然后 mov 而不是使用 push 操作码?并且不依赖 bp 来获取参数或 var,而是使用 sp 然后使用 add esp, 16 恢复 sp ?
    • BTW SP 用于 16 位(见标签),尤其是 32 位
    • @Sebastian 我知道,谢谢
    • 您打算使用什么(16、32、64 位)?实模式/保护模式?
    • @8HoLoN:在像引导加载程序这样的 16 位模式下,您通常希望使用 bp 作为帧指针,因为 [sp+2] 不是有效的寻址模式。 (虽然您可以使用[esp+2],但地址大小前缀和 SIB 字节需要额外的代码大小,并且代码大小在传统 BIOS 引导加载程序中通常非常宝贵。)改用纯 mov多个 push 也需要更多的代码大小,并且在 CPU 上 since Pentium-M which have a "stack engine" 以绕过堆栈操作对堆栈指针的串行依赖性甚至不会更快。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-03-17
    • 1970-01-01
    • 2011-01-17
    • 1970-01-01
    • 2021-12-21
    • 2023-03-10
    • 1970-01-01
    相关资源
    最近更新 更多