调用者保存/被调用者保存的术语是基于一个简化的寄存器变量模型,其中调用者实际上在每次调用时保存/恢复(回到相同的寄存器),对于任何调用破坏的寄存器,他们保留了他们想要的值。(而不是将长期有用的值保存在其他地方,或者重新开始使用寄存器分配按需重新加载)。
你必须理解,“调用者保存”意味着“以某种方式保存,
如果你以后需要这个值”。并不是所有的寄存器在每个函数调用时都被保存/恢复,而是由调用者或被调用者来保存。
实际上,高效的代码会在不再需要时销毁值。编译器通常会在函数的开头(序言)保存一些调用保留的寄存器(并在结尾处恢复它们)。在函数内部,它们使用这些寄存器来保存需要在函数调用之间保留的值。(如果它们用完了调用保留的寄存器,它们可能确实需要在调用之前溢出一些东西,但通常不需要立即重新加载,可能要等到几次调用后,或者在循环之后,也可能不一定重新加载到之前的同一个寄存器中。)
我更喜欢使用“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。
volatile
关键字混淆。因此,“call-clobbered”准确地描述了一个函数需要假定关于其他函数的内容,而不是如何实现调用约定/ ABI。 - Peter Cordes