【问题标题】:What are callee and caller saved registers?什么是被调用者和调用者保存的寄存器?
【发布时间】:2012-03-05 07:56:36
【问题描述】:

我无法理解调用方和被调用方保存的寄存器之间的区别以及何时使用什么。

我正在使用 MSP430 :

程序:

mov.w #0,R7 
mov.w #0,R6 
add.w R6,R7 
inc.w R6 
cmp.w R12,R6 
jl l$loop 
mov.w R7,R12
ret

上面的代码是一个被调用者,并在教科书示例中使用过,因此它遵循约定。 R6 和 R7 被调用者保存,R12 被调用者保存。我的理解是,被调用者保存的 regs 不是“全局的”,因为在过程中更改其值不会影响它在过程之外的值。这就是为什么您必须在开始时将新值保存到被调用者 reg 中。

R12,保存的调用者是“全局的”,因为缺少更好的词。该过程的作用在调用后会对 R12 产生持久影响。

我的理解正确吗?我错过了其他东西吗?

【问题讨论】:

    标签: assembly cpu-registers calling-convention abi


    【解决方案1】:

    调用者保存的(AKA volatile 或 call-clobbered)寄存器

    • 调用者保存的寄存器中的值是短期的,不是 从调用到调用保存
    • 它保存临时(即短期)数据

    被调用者保存(AKA 非易失性或调用保留)寄存器

    • 被调用者保存的寄存器在调用中保存值并且是长期的
    • 它保存通过多个函数/调用使用的非临时(即长期)数据

    【讨论】:

    • 它在调用函数之前保存值 - 不,它没有。 volatile aka call-clobbered 寄存器中的值会被破坏。 实际上浪费指令将数据复制到其他地方并在呼叫之前/之后返回的整个想法是“呼叫者保存”术语的脑死亡的一部分。真正的代码不是这样工作的。
    • 另外,您所说的“它可以节省”意味着收银机本身会自动为您执行此操作,当然事实并非如此。 (在描述保留调用的 regs 时,“它保存”也是如此。必须在使用它时保存调用者的值。)删除这两个“它保存”要点后,它将是这些条款的完整和准确的摘要。
    • 感谢@PeterCordes 的澄清。我将通过删除两个寄存器的第二个要点来修改它。
    【解决方案2】:

    我不确定这是否会增加任何东西,

    调用者已保存意味着调用者必须保存寄存器,因为它们将在调用中被破坏,并且别无选择,只能在调用返回后处于破坏状态(例如,返回值在eax对于cdecl。返回值恢复到被调用者调用之前的值是没有意义的,因为它是一个返回值)。

    被调用者保存意味着被调用者必须保存寄存器,然后在调用结束时恢复它们,因为它们向调用者保证在函数返回后包含相同的值,并且可以恢复它们,即使他们在通话过程中在某个时间点受到重创。

    上述定义的问题在于,例如在维基百科 cdecl 上,它说 eaxecxedx 是调用者保存的,其余的是被调用者保存的,这表明调用者必须保存所有 3 个这些寄存器,如果调用者一开始没有使用这些寄存器,则可能不会。在这种情况下,呼叫者“已保存”成为用词不当,但“呼叫破坏”仍然正确适用。这与被称为被调用者保存的“其余”相同。这意味着如果某些寄存器从未在调用中使用过,则所有其他 x86 寄存器将由被调用者保存和恢复。使用 cdecl,eax:edx 可用于返回 64 位值。我不知道为什么 ecx 在需要时也会保存调用者,但确实如此。

    【讨论】:

    • ecx 是调用破坏的第三个寄存器的不错选择,因为变量计数移位和rep stos/movs 都需要它。它很容易成为 EAX 和 EDX 之后的第三大需求(对于 div,扩展乘法和 AL/EAX 的紧凑编码)。一般来说,您需要调用保留和调用破坏寄存器的平衡,因此叶函数不需要太多的推送/弹出来获得一些临时空间来使用,而在循环中使用 call 的函数也不需要循环内有很多溢出/重新加载。
    【解决方案3】:

    调用者保存/被调用者保存的术语基于一个相当脑残的低效编程模型,调用者实际上保存/恢复所有调用破坏的寄存器(而不是在其他地方保存长期有用的值),而被调用者实际上保存/恢复所有调用保留的寄存器(而不是仅仅不使用它们中的一些或任何一个)。

    或者你必须明白“调用者保存”的意思是“以某种方式保存如果你以后想要这个值”。

    实际上,高效的代码会让值在不再需要时被销毁。编译器通常会生成在函数开头保存一些调用保留寄存器的函数(并在结尾处恢复它们)。在函数内部,他们将这些 reg 用于需要在函数调用中存活的值。

    我更喜欢“call-preserved”和“call-clobbered”,一旦你听说了基本概念,它们就很明确并且可以自我描述,并且不需要任何认真的心理从调用者的角度或被调用者的角度考虑的体操。 (这两个术语都是从相同的角度来看的)。

    此外,这些术语的不同之处不止一个字母。

    易失性/非易失性这两个术语非常好,类似于存储是否会因断电而失去价值(例如 DRAM 与闪存)。但是 C volatile 关键字具有完全不同的技术含义,因此在描述 C 调用约定时,这是“(非)易失性”的缺点。


    • Call-clobbered,又名 caller-savedvolatile 寄存器适用于下一次函数调用后不需要的临时值/临时值。

    从被调用者的角度来看,您的函数可以随意覆盖(也称为破坏)这些寄存器,而无需保存/恢复。

    从调用者的角度来看,call foo 会破坏(又名 clobbers)所有被调用破坏的寄存器,或者至少您必须假设它确实如此。

    您可以编写具有自定义调用约定的私有辅助函数,例如你知道他们不会修改某个寄存器。但是,如果您只知道(或想要假设或依赖)目标函数遵循正常的调用约定,那么您必须将函数调用视为它确实破坏了所有调用破坏的寄存器。字面意思就是这个名字的由来:一个调用破坏了这些寄存器。

    一些进行过程间优化的编译器还可以使用自定义调用约定为不遵循 ABI 的函数创建仅供内部使用的定义。

    • Call-preserved,又名 callee-savednon-volatile 寄存器在函数调用中保持其值。这对于进行函数调用的循环中的循环变量或基本上非叶函数中的任何内容很有用。

    从被调用者的角度来看,这些寄存器不能被修改,除非你将原始值保存在某个地方,这样你就可以在返回之前恢复它。或者对于像堆栈指针这样的寄存器(几乎总是保留调用),您可以减去一个已知的偏移量并在返回之前再次将其添加回来,而不是在任何地方实际保存旧值。即您可以通过航位推算来恢复它,除非您分配运行时可变数量的堆栈空间。然后通常您从另一个寄存器恢复堆栈指针。

    可以从使用大量寄存器中受益的函数可以保存/恢复一些保留调用的寄存器,以便它可以将它们用作更多的临时寄存器,即使它不进行任何函数调用。通常,您只会在用完调用破坏寄存器后才执行此操作,因为保存/恢复通常会在函数的开始/结束时花费推送/弹出。 (或者,如果您的函数有多个退出路径,则每个路径中都有一个 pop。)


    “调用者保存”的名称具有误导性:您没有专门保存/恢复它们。通常,您将代码安排为具有需要在调用保留寄存器中或堆栈上的某个位置或您可以重新加载的其他位置中的函数调用后生存的值。让call 破坏临时值是正常的。


    ABI 或调用约定定义了哪些是哪些

    例如,请参阅 What registers are preserved through a linux x86-64 function call 以获取 x86-64 System V ABI。

    此外,在我知道的所有函数调用约定中,传递参数的寄存器总是被调用破坏。见Are rdi and rsi caller saved or callee saved registers?

    但系统调用调用约定通常会保留除返回值调用之外的所有寄存器。 (通常包括偶数条件代码/标志。)见What are the calling conventions for UNIX & Linux system calls on i386 and x86-64

    【讨论】:

    • 一个新的术语怎么样:caller-preserved/callee-preserved(保存可能隐含地包括不使用)?我喜欢声明负责操作的人,但是声明谁可以在没有特殊操作的情况下使用寄存器可能没问题。 Call-clobbered 可能微弱地暗示保存发生在调用站点(这对于 XTensa 和某些 ISA 包括堆栈指针调整指令也是如此,这些指令也可以保存/恢复寄存器)。命名是困难。提到过程间优化可以绕过 ABI可能是值得的。
    • @PaulA.Clayton:我真的很喜欢可以从调用者被调用者的角度来看待每个术语 call-preserved 与 clobbered。它们更适合编译器实际用于代码生成的模型:在函数的开始/结束处保存/恢复一些调用保留的 reg,并将这些用于需要在调用中存活的任何变量。 callee vs. caller-preserved 避免了“已保存”这个词,并解决了我对传统术语的一个反对意见,但不是关于能够直接应用该术语的更基本的一点,无论您从哪个方向思考。
    • @PaulA.Clayton:他们的关键点是调用破坏寄存器通常不会被保存根本;它们倾向于用于计算调用的参数,而这些值只是在函数调用中消失(就调用者而言)。为其贴上“保留”标签实际上与“调用者保存”一样糟糕。 (我什至不得不在逻辑错误并在最后一句中写下“callee-saved”之后编辑此评论。这些术语太愚蠢了,因为当你谈论调用者的观点时,你必须翻转其中一个。 )
    • 显然,这个命名法被用于教育系统。我花了一段时间才弄清楚为什么“保存”的场景没有回到前面。这是违反直觉的,因为对我来说“保存的调用者”一直被解释为“调用者保存在一个不会在调用中被破坏的寄存器中”
    【解决方案4】:

    调用者保存的寄存器(又名 volatile 寄存器,或 call-clobbered)用于保存需要的临时数量 不会跨调用保留。

    因此,调用者有责任将这些寄存器压入堆栈或将它们复制到其他地方如果它想在过程调用后恢复此值。

    不过,让call 破坏这些寄存器中的临时值是正常的。

    被调用者保存的寄存器(AKA 非易失性寄存器,或调用保留)用于保存长期存在的值,这些值应该 跨调用保留。

    当调用者进行过程调用时,可以预期这些寄存器在被调用者返回后将保持相同的值,这使得被调用者有责任在返回给调用者之前保存它们并恢复它们。或者不碰它们。

    【讨论】:

    • 我喜欢使用的另一个术语是“call-clobbered”与“call-preserved”。 caller 与 callee 仅相差一个字母,并且编译器实际上不会在调用之间保存/恢复调用破坏的 regs(它们只是将值放入调用保留的 regs 中。) volatile 与 non-volatile 可能会导致与 C 的 @ 混淆987654322@ 关键字。所以“call-clobbered”准确地描述了一个函数需要对 other 函数做出什么假设,而不是它如何实现调用约定/ABI。
    【解决方案5】:

    被调用者与调用者保存的约定是关于谁负责在调用中保存和恢复寄存器中的值的约定。所有寄存器都是“全局的”,因为任何地方的任何代码都可以看到(或修改)一个寄存器,并且这些修改将被任何地方的任何后续代码看到。寄存器保存约定的要点是代码不应该修改某些寄存器,因为其他代码假定值没有被修改。

    在您的示例代码中,没有一个寄存器是被调用者保存的,因为它不尝试保存或恢复寄存器值。但是,它似乎不是一个完整的过程,因为它包含一个未定义标签 (l$loop) 的分支。因此,它可能是来自将某些寄存器视为被调用者保存的过程中间的一段代码;您只是缺少保存/恢复说明。

    【讨论】:

    • C 中的静态变量是否使用 MIPS 中的 t 之类的寄存器?
    • @NAND:静态变量一般存储在内存中。在 MIPS 上,它们需要(临时)加载到寄存器中才能使用,但通常“活”在内存中
    猜你喜欢
    • 2019-08-18
    • 1970-01-01
    • 2020-11-19
    • 1970-01-01
    • 1970-01-01
    • 2021-04-28
    • 1970-01-01
    • 2021-02-15
    • 2021-01-20
    相关资源
    最近更新 更多