【问题标题】:Saving Status register when calling a function调用函数时保存状态寄存器
【发布时间】:2018-06-17 04:59:13
【问题描述】:

据我了解,当我根据 GCC 调用约定调用函数时,会发生以下情况:

调用者保存 AX、CX 和 DX 寄存器的值。参数和返回地址被压入堆栈。此外,calle 必须保留 SI、DI、BX 和 BP 寄存器的值。

但是,状态寄存器呢?谁来拯救它?

另外,压栈的返回地址的值实际上是指令寄存器的值吗?

【问题讨论】:

    标签: gcc assembly calling-convention status-register


    【解决方案1】:

    状态寄存器不会在函数调用之间保留。如果状态寄存器中有重要的内容,则需要将其复制到其他地方(通常使用 SETcc),但调用约定不要求调用函数来执行此操作,就像它不需要调用函数来保存和恢复 AX 等。如果它们没有什么重要的东西。

    【讨论】:

    • 但是如果我在调用函数中有一些算术函数,这可以改变状态寄存器中某些位的值(例如符号标志)。调用完成后,我不应该在返回调用函数时以某种方式恢复更改位的先前值吗?或者这种情况就是你说的重要的东西,这种情况我需要使用SETcc?
    • 不,这不是调用约定的一部分,所以调用者函数不会假设你会这样做。
    • @Dusan 状态寄存器非常易变(就像 1/3 的指令集修改它),在调用之间保持它的值会非常麻烦。审查代码也将是一场噩梦,如果编码人员充分利用它,在几次调用甚至内部使用某些状态,当乱序完成时读取 486-586 时代的代码已经足够棘手了主要是手动设置,通常状态是在使用前 2-3 条指令设置的。或者要记住额外的东西,比如inc/dec 不会影响进位标志。不将其视为临时性是非常不切实际的。
    【解决方案2】:

    回答你的第二个问题:

    另外,压栈的返回地址的值实际上是指令寄存器的值吗?

    你的意思是call指令推送的值?是的,这是call 内部执行期间的当前ripeip/ip 在 32/16 位模式下)值(因为rip 指向下一条指令)。

    ret 指令将弹出堆栈顶部的任何值,并将其设置为rip,从而更改下一条指令的代码执行流程(远离ret 之后的下一条指令到地址/value 在堆栈中)。因此,在ret 完成后,堆栈中的值成为ip 寄存器的内容。 ret 类似于(不存在)pop ip,但它有自己的助记符,可以在人类阅读时更好地在源代码中脱颖而出,并且它具有完全不同的操作码,因此晶体管中的硬件实现完全特定于它(这在现代 x86 上是有意义的,其中 ret 实现使用许多额外的技巧来获得更好的性能,但我有点好奇为什么 8086 不会将其编码为 pop ip,就像只是另一个寄存器pop,在当时可能在某些细节上有些特别)。

    【讨论】:

      【解决方案3】:

      GCC 调用约定

      gcc 在它所针对的任何平台上都使用标准调用约定。听起来您在描述 Linux 上使用的 i386 System V 调用约定/ABI,和/或一些 Windows 调用约定。 (其中一些以不同的方式传递 args,但对可以被破坏的寄存器做出相同的选择)。

      您使用的是 16 位寄存器名称,但 gcc 几乎不支持 16 位 x86。它基本上生成 32 位代码,然后用 .code16 汇编它,因此大多数指令都有操作数大小和/或地址大小前缀。

      调用者保存 AX、CX 和 DX 寄存器的值

      不,调用者只有在其中有任何想要在call 中保留的数据时才会这样做。正常情况是调用者让这些值消失。 “caller-saved”与“callee-saved”是一个不好的术语,因为它暗示所有寄存器实际上都保存在某个地方。

      更容易理解,IMO,是

      • call-clobbered:EAX ECX EDX 和条件代码(EFLAGS 的一部分),所有 xmm regs
      • 通话保留:EBX、ESI EDI、EBP、ESP。

      DF 在调用和返回时必须为 0,因此字符串指令向上。 (DF 是 EFLAGS 中的另一个位)。 x87 堆栈在 callret 上必须为空,但返回 FP 值的函数除外(在这种情况下,st0 具有返回值,而 x87 堆栈的其余部分为空)。

      Call-clobbered 意味着在call 之后,调用者必须假设寄存器包含垃圾,无论被调用者是否实际使用了寄存器。 如果该寄存器中有调用者稍后需要的任何内容,它必须将其移动到其他地方。但如果不是,让价值消亡完全没问题。例如要编译像rv = foo(a + b + c) 这样的东西,调用者会在寄存器中计算a+b+c。但如果它在函数调用后也不需要该值,则无需保留它。

      调用保留意味着调用者可以假设寄存器值没有改变,无论被调用者只是避免接触该寄存器,还是被调用者保存/恢复它。 (或者对于 ESP,被调用者通常使用 add esp, 28 或类似名称来恢复它,以逆转它使用 pushsub 所做的任何更改。被调用者如何设法通过调用返回并不重要 -保留的寄存器仍然保存调用者的值,只是它确实如此。这就是为什么“被调用者保存”也不是最清晰的术语:它意味着被调用者显式地保存它们。

      但是,状态寄存器呢?谁来拯救它?

      没有人会保存它,除非在极少数情况下。如果需要,调用者可以保存它,但通常重做比较更容易也更便宜(popf 很慢,而 pushf 首先保存 EFLAGS 不是免费)。

      或者更多情况下,条件代码中没有任何有用的数据,只有整数寄存器中的整数值。大多数指令都会写入 EFLAGS,但大多数时候您从未阅读过这些结果。整数结果通常使用addimul等,而忽略标志结果。

      有趣的事实:64-bit OS X system calls set CF on error, otherwise they clear CF。没有常见的 32 位或 64 位函数调用约定在 EFLAGS 中返回任何内容;他们只是被重创了。 (对于 Linux 系统调用,保留 EFLAGS / RFLAGS。系统调用通常不会破坏除返回值之外的任何寄存器,部分原因是这样可以避免将内核信息泄漏回用户空间。)

      【讨论】:

      • 现在我想起了一些事情……一些 DOS/BIOS 服务有时会返回某些标志(通常是 CF,不确定是否只有 CF)设置为指示“no_value”或“error”状态,以区分它们普通的返回值。所以在极少数情况下,这些标志也被用作返回信息的一部分,但我不记得任何东西,因为 x86 32 位时代也使用它。
      • @Ped7g: OS X 64-bit system leave CF set on error,但没有标准的函数调用约定在标志中返回任何内容。当然,在手写 asm 中,您可以对私有函数使用自定义调用约定,并且可以(并且应该)通过在不内联的情况下返回标志中的条件来优化 setcc/test。
      • 另外,我不知道为什么有人会否决这个答案。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-10-15
      • 1970-01-01
      • 1970-01-01
      • 2023-03-19
      • 2021-12-26
      • 2020-12-31
      相关资源
      最近更新 更多