64

我在理解调用者和被调用者保存的寄存器之间的区别以及何时使用什么时遇到了一些麻烦。

我正在使用 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 产生持久影响。

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

4

5 回答 5

132

调用者保存的寄存器(AKA易失性寄存器,或call-clobbered)用于保存不需要在调用之间保留的临时数量。

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

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

被调用者保存的寄存器(AKA非易失性寄存器,或call-preserved)用于保存应该在调用之间保留的长期值。

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

于 2013-04-28T17:25:29.313 回答
19

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

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

于 2012-02-13T22:11:20.083 回答
12

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

或者您必须了解“调用者保存”的意思是“如果您以后想要该值,则以某种方式保存”。

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

我更喜欢 "call-preserved" 与 "call-clobbered",一旦你听说了基本概念,它们就很明确并且可以自我描述,并且不需要任何严肃的心理体操来从呼叫者的角度或被调用者的观点。(这两个术语来自相同的角度)。

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

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


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

从被调用者的角度来看,您的函数可以在不保存/恢复的情况下自由覆盖(又名破坏)这些寄存器。

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

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

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

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

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

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


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


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

例如,请参阅通过x86-64 System V ABI的 linux x86-64 函数调用保留哪些寄存器。

此外,在我知道的所有函数调用约定中,arg 传递寄存器总是被调用破坏。请参阅是否保存了 rdi 和 rsi 调用方或被调用方保存的寄存器?

但是系统调用调用约定通常会保留除返回值调用之外的所有寄存器。(通常包括偶数条件代码/标志。)请参阅i386 和 x86-64 上 UNIX 和 Linux 系统调用的调用约定是什么

于 2019-05-17T00:22:00.747 回答
3

呼叫者保存(AKA 易失性或呼叫破坏)寄存器

  • 调用者保存的寄存器中的值是短期的,不会在调用之间保留  
  • 它保存临时(即短期)数据

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

  • 被调用者保存的寄存器在调用中保存值并且是长期的
  • 它保存通过多个函数/调用使用的非临时(即长期)数据
于 2020-08-22T18:05:18.017 回答
1

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

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

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

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

于 2020-03-28T16:56:08.117 回答