调用者保存/被调用者保存的术语基于一个相当脑残的低效编程模型,调用者实际上保存/恢复所有调用破坏的寄存器(而不是在其他地方保存长期有用的值),而被调用者实际上保存/恢复所有调用保留的寄存器(而不是仅仅不使用它们中的一些或任何一个)。
或者你必须明白“调用者保存”的意思是“以某种方式保存如果你以后想要这个值”。
实际上,高效的代码会让值在不再需要时被销毁。编译器通常会生成在函数开头保存一些调用保留寄存器的函数(并在结尾处恢复它们)。在函数内部,他们将这些 reg 用于需要在函数调用中存活的值。
我更喜欢“call-preserved”和“call-clobbered”,一旦你听说了基本概念,它们就很明确并且可以自我描述,并且不需要任何认真的心理从调用者的角度或被调用者的角度考虑的体操。 (这两个术语都是从相同的角度来看的)。
此外,这些术语的不同之处不止一个字母。
易失性/非易失性这两个术语非常好,类似于存储是否会因断电而失去价值(例如 DRAM 与闪存)。但是 C volatile 关键字具有完全不同的技术含义,因此在描述 C 调用约定时,这是“(非)易失性”的缺点。
-
Call-clobbered,又名 caller-saved 或 volatile 寄存器适用于下一次函数调用后不需要的临时值/临时值。
从被调用者的角度来看,您的函数可以随意覆盖(也称为破坏)这些寄存器,而无需保存/恢复。
从调用者的角度来看,call foo 会破坏(又名 clobbers)所有被调用破坏的寄存器,或者至少您必须假设它确实如此。
您可以编写具有自定义调用约定的私有辅助函数,例如你知道他们不会修改某个寄存器。但是,如果您只知道(或想要假设或依赖)目标函数遵循正常的调用约定,那么您必须将函数调用视为它确实破坏了所有调用破坏的寄存器。字面意思就是这个名字的由来:一个调用破坏了这些寄存器。
一些进行过程间优化的编译器还可以使用自定义调用约定为不遵循 ABI 的函数创建仅供内部使用的定义。
-
Call-preserved,又名 callee-saved 或 non-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