被调用者保存寄存器和调用者保存寄存器是什么?

81

我对调用者保存寄存器和被调用者保存寄存器之间的区别以及何时使用它们存在一些困惑。

我正在使用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是调用者保存的。我的理解是,被调用者保存的寄存器不是“全局”的,即在过程中更改其值不会影响过程外部的值。这就是为什么你必须在开始时将新值保存到被调用者寄存器的原因。

R12,调用者保存的寄存器是“全局”的,缺乏更好的措辞。过程的操作对R12产生持久的影响。

我的理解正确吗?我有没有漏掉其他东西?

6个回答

159

调用者保存寄存器(又称为易失性寄存器或调用破坏)用于保存临时量,这些量不需要跨调用进行保留。

因此,如果调用方想要在过程调用后恢复该值,它就需要将这些寄存器推入堆栈或将它们复制到其他地方。

当然,在这些寄存器中,让call销毁临时值是很正常的。

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

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


26
我喜欢使用的另一个术语是“被调用破坏(call-clobbered)”和“调用保存(call-preserved)”。caller与callee之间只有一个字母的区别,而编译器实际上并不会在调用间保存/恢复call-clobbered寄存器(它们只是在call-preserved寄存器中放置值)。volatile和non-volatile可能会与C语言的volatile关键字混淆。因此,“call-clobbered”准确地描述了一个函数需要假定关于其他函数的内容,而不是如何实现调用约定/ ABI。 - Peter Cordes

24
调用者保存/被调用者保存的术语是基于一个简化的寄存器变量模型,其中调用者实际上在每次调用时保存/恢复(回到相同的寄存器),对于任何调用破坏的寄存器,他们保留了他们想要的值。(而不是将长期有用的值保存在其他地方,或者重新开始使用寄存器分配按需重新加载)。
你必须理解,“调用者保存”意味着“以某种方式保存,如果你以后需要这个值”。并不是所有的寄存器在每个函数调用时都被保存/恢复,而是由调用者或被调用者来保存。
实际上,高效的代码会在不再需要时销毁值。编译器通常会在函数的开头(序言)保存一些调用保留的寄存器(并在结尾处恢复它们)。在函数内部,它们使用这些寄存器来保存需要在函数调用之间保留的值。(如果它们用完了调用保留的寄存器,它们可能确实需要在调用之前溢出一些东西,但通常不需要立即重新加载,可能要等到几次调用后,或者在循环之后,也可能不一定重新加载到之前的同一个寄存器中。)
我更喜欢使用“call-preserved”和“call-clobbered”,它们在你了解基本概念后就变得清晰明了,并且无需费力地从调用者或被调用者的角度进行思考。(这两个术语都是从同一个角度来看的)。
此外,这两个术语不仅仅是一个字母的差异。
术语“volatile / non-volatile”也不错,类比于存储器在断电时是否丢失值(如DRAM和Flash)。但是C语言中的“volatile”关键字有完全不同的技术含义,所以在描述C语言调用约定时,这就是“(non)-volatile”这个术语的一个缺点。
  • 被调用者保存,也称为调用者保存易失性寄存器,适用于在下一个函数调用之后不再需要的临时值。

从被调用者的角度来看,您的函数可以自由地覆盖(即破坏)这些寄存器,而无需保存/恢复。

从调用者的角度来看,call foo会破坏(即破坏)所有被调用者保存的寄存器,或者至少您必须假设它会这样做。

您可以编写具有自定义调用约定的私有辅助函数,例如,您知道它们不会修改某个寄存器。但是,如果您所知道的(或者希望假设或依赖的)仅仅是目标函数遵循正常的调用约定,那么您必须将函数调用视为破坏了所有被调用者保存的寄存器。这就是字面上的意思:调用会破坏这些寄存器。

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

  • 保留调用,也称为被调用者保存非易失性寄存器,在函数调用之间保持它们的值。这对于在进行函数调用的循环中的循环变量或者一般情况下的非叶函数中的任何内容都很有用。

从被调用者的角度来看,除非你将原始值保存在某个地方以便在返回之前恢复它,否则这些寄存器是不能被修改的。或者对于像栈指针这样的寄存器(几乎总是保留调用的),你可以在返回之前减去一个已知的偏移量,然后再加回来,而不是实际上在任何地方保存旧值。也就是说,你可以通过死算来恢复它,除非你分配了一个运行时可变量大小的栈空间。然后通常你会从另一个寄存器中恢复栈指针。

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


“caller-saved”这个名字有些误导人,你并不需要特别保存或恢复它们。通常,你会安排你的代码,将需要在函数调用中保留的值放在调用保留寄存器中,或者放在栈上的某个位置,或者放在其他地方可以重新加载的位置。让一个“call”破坏临时值是正常的。
ABI或调用约定定义了哪些是哪些。
例如,查看x86-64 System V ABI的What registers are preserved through a linux x86-64 function call
此外,我所了解的所有函数调用约定中,参数传递寄存器总是会被调用破坏。请参阅Are rdi and rsi caller saved or callee saved registers?
但是,系统调用的调用约定通常会保留除返回值之外的所有寄存器。(通常包括条件码/标志位。)请参阅What are the calling conventions for UNIX & Linux system calls (and user-space functions) on i386 and x86-64

1
显然,这种命名法在教育系统中被使用。我花了一些时间才弄清楚为什么“saved”场景不是反过来的。这是违反直觉的,因为对我来说,“caller saved”一直被解释为“由调用者保存在一个不会在调用中被破坏的寄存器中”。 - Lewis Kelsey
没错。同样的道理,它不会盲目地保存所有被调用者保存的东西,仅仅因为它是一个被调用者。有什么区别呢?如果被调用者想将其用作寄存器变量,它也是“被调用者保存”。 - undefined
1
我并不是说我喜欢“来电显示”这个术语,我只是想展示我是如何与之和解的。 - undefined
@doug65536:这是支持使用术语“保留调用”的一个论点。保留值的一种方法是根本不触碰它,而不是将其保存/恢复到内存中。(尽管选择不同的术语的一个重要原因是确保两个术语与我不喜欢的经典“调用者保存”有所区别。不触碰寄存器作为“被调用者保存”的方式更加明显。) - undefined
@doug65536:好的,这很公平。我认为你是对的,我应该更新答案,不要错误地描述背后的思维过程,因为这个术语仍然不幸地广泛使用,所以人们会遇到它。我更新后会通知你。 - undefined
显示剩余8条评论

23
被调用方寄存器保存和调用方寄存器保存是一种约定,用于确定谁负责在函数调用过程中保存和恢复寄存器的值。所有寄存器都是“全局”的,因为任何地方的代码都可以查看(或修改)寄存器的内容,并且这些修改后续的任何代码都将看到。寄存器保存约定的目的在于,代码不应该修改某些寄存器的值,因为其他代码可能会假定该值未被修改。
在你的示例代码中,没有一个寄存器是被调用方保存的,因为它没有尝试保存或恢复寄存器的值。然而,它似乎不是一个完整的过程,因为它包含一个跳转到未定义标签(`l$loop`)的分支语句。所以它可能是来自某个过程中间的代码片段,该过程将一些寄存器视为被调用方保存的,只是缺少了保存/恢复指令。

C语言中的静态变量是否像MIPS中的t一样使用寄存器? - NAND
2
@NAND:静态变量通常存储在内存中。在MIPS上,它们需要被加载到寄存器中(临时)才能使用,但通常“驻留”在内存中。 - Chris Dodd

3

调用者保存(也称易失或被调用破坏)寄存器

  • 调用者保存寄存器中的值是短期的,不会从一个函数调用到另一个函数调用而得以保留
  • 它保存临时(即短期)数据

被调用者保存(也称非易失或被调用保留)寄存器

  • 被调用者保存寄存器保存函数调用之间的数值,是长期的
  • 它保存非临时(即长期)数据,可以在多个函数/调用中使用

1
它在调用函数之前保存值 - 不是的。易失性寄存器中的值会被破坏。实际上浪费指令将数据复制到其他地方并在调用之前/之后再次复制回来的整个想法是“调用者保存”术语的愚蠢之处。真正的代码不是这样工作的。 - Peter Cordes
1
此外,您所表达的“它保存”暗示着寄存器本身会自动为您完成这个过程,当然实际情况并非如此。(同样适用于描述调用保留寄存器时的“它保存”。在使用之前,必须保存调用者的值。)如果删除这两个“它保存”的要点,那么这将是对这些术语的完整准确概括。 - Peter Cordes
1
感谢您的澄清,@PeterCordes。我将通过删除两个寄存器的第二个项目来进行修改。 - Abu Shoeb

2

我不确定这是否有所帮助,但是:

Caller saved意味着调用者必须保存寄存器,因为它们在调用中将被破坏,并且在调用返回后只能保留在破坏的状态下(例如,在cdecl中,返回值在eax中。让调用方恢复到调用之前的返回值没有任何意义,因为它是一个返回值)。

Callee saved意味着被调用者必须保存寄存器,然后在调用结束时恢复它们,因为它们对调用方有相同值的保证,并且即使在调用过程中被破坏,也可以恢复它们。

上述定义的问题在于,例如在维基百科的cdecl中,它说eaxecxedx是caller saved,其余的都是callee saved,这意味着调用方必须保存这3个寄存器中的所有内容,即使在调用中从未使用过这些寄存器。在这种情况下,caller 'saved'变得不准确了,但“call clobbered”仍然正确适用。与此类似,“the rest”被称为callee saved。这意味着所有其他x86寄存器都将由被调用者保存和恢复,但如果在调用中从未使用某些寄存器,则不会发生这种情况。在cdecl中,eax:edx可能用于返回64位值。我不确定为什么ecx也是caller saved(如果需要的话),但它确实是。


1
ecx 是一个很好的第三个寄存器选择,因为它需要用于变量计数移位,以及 rep stos/movs。在 EAX 和 EDX 之后,它很容易成为第三个最常用的寄存器(用于 div、扩展乘法和 AL/EAX 的紧凑编码)。通常你需要平衡保留调用和破坏调用的寄存器,这样叶子函数就不需要太多的 push/pop 来获得一些临时空间来处理,而使用 call 循环的函数则不需要在循环内部进行太多的溢出/重载。 - Peter Cordes

0

在问题提出近3年后,我要补充一些内容。简而言之,调用者保存以下寄存器--> rdi、rsi、rdx、rcx、r8、r9。被调用者保存以下寄存器--> rbx、rbp、r12、r13、r14。

注意:我从x86-64架构的角度回答了这个问题。

参考资料:《计算机系统:程序员的视角》(作者:Randal Bryant,David O'Hallaron)


x86-64有16个GP整数寄存器,您只提到了其中的11个。 RAX、R10和R11也是调用破坏的。 R15与R12-R14一起被调用保留。 RSP(堆栈指针)也是调用保留的。UNIX和Linux系统调用(以及用户空间函数)在i386和x86-64上的调用约定是什么?(https://dev59.com/Q3E85IYBdhLWcg3w8IXK) / 哪些寄存器通过linux x86-64函数调用被保留?(https://dev59.com/4WMl5IYBdhLWcg3w5aeQ) / x86-64 System V ABI在哪里记录?(https://dev59.com/J2Ml5IYBdhLWcg3wyJYe) - Peter Cordes
我要补充一点,rsp被“保留”是另一种说法,意思是你应该保留他们在栈上推送的任何参数,而不是像stdcall那样使用带有立即值的ret指令。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接