caller-saved / callee-saved 术语基于一个相当脑残的低效编程模型,其中调用者实际上保存/恢复所有调用破坏的寄存器(而不是在其他地方保留长期有用的值),而被调用者实际上保存/restore 所有调用保留的寄存器(而不是不使用其中的一些或任何一个)。
或者您必须了解“调用者保存”的意思是“如果您以后想要该值,则以某种方式保存”。
实际上,高效的代码会让值在不再需要时被销毁。编译器通常会生成在函数开头保存一些调用保留寄存器的函数(并在结尾处恢复它们)。在函数内部,他们将这些 regs 用于需要在函数调用中生存的值。
我更喜欢 "call-preserved" 与 "call-clobbered",一旦你听说了基本概念,它们就很明确并且可以自我描述,并且不需要任何严肃的心理体操来从呼叫者的角度或被调用者的观点。(这两个术语来自相同的角度)。
另外,这些术语的不同之处不止一个字母。
易失性/非易失性这两个术语非常好,类似于存储是否会因断电而失去其价值(例如 DRAM 与闪存)。但是 Cvolatile
关键字具有完全不同的技术含义,因此在描述 C 调用约定时,这是“(非)易失性”的缺点。
- Call-clobbered,又名调用者保存或易失性寄存器适用于下一次函数调用后不需要的临时/临时值。
从被调用者的角度来看,您的函数可以在不保存/恢复的情况下自由覆盖(又名破坏)这些寄存器。
从调用者的角度来看,call foo
销毁(又名 clobbers)所有调用破坏的寄存器,或者至少您必须假设它确实如此。
您可以编写具有自定义调用约定的私有辅助函数,例如,您知道它们不会修改某个寄存器。但是,如果您只知道(或想要假设或依赖)目标函数遵循正常的调用约定,那么您必须将函数调用视为它确实破坏了所有调用破坏的寄存器。这就是名字的由来:一个呼叫破坏了这些寄存器。
一些进行过程间优化的编译器还可以使用自定义调用约定创建不遵循 ABI 的函数的仅供内部使用的定义。
- Call-preserved,也就是callee-saved或non-volatile寄存器在函数调用中保持它们的值。这对于进行函数调用的循环中的循环变量或基本上非叶函数中的任何内容很有用。
从被调用者的角度来看,这些寄存器不能被修改,除非您将原始值保存在某处,以便您可以在返回之前恢复它。或者对于像堆栈指针这样的寄存器(几乎总是保留调用),您可以减去一个已知的偏移量并在返回之前再次将其添加回来,而不是实际将旧值保存在任何地方。即您可以通过航位推算来恢复它,除非您分配运行时可变数量的堆栈空间。然后通常您从另一个寄存器恢复堆栈指针。
可以从使用大量寄存器中受益的函数可以保存/恢复一些保留调用的寄存器,以便它可以将它们用作更多的临时寄存器,即使它不进行任何函数调用。通常,您只会在用完调用破坏寄存器后才执行此操作,因为保存/恢复通常会在函数的开始/结束时花费推送/弹出。(或者,如果您的函数有多个退出路径,则pop
每个都有一个。)
“调用者保存”的名称具有误导性:您不必专门保存/恢复它们。通常,您将代码安排为具有需要在调用保留寄存器中或堆栈上的某个位置或您可以重新加载的其他位置中的函数调用后生存的值。让一个call
破坏临时值是正常的。
ABI 或调用约定定义了哪些是哪些
例如,请参阅通过x86-64 System V ABI的 linux x86-64 函数调用保留哪些寄存器。
此外,在我知道的所有函数调用约定中,arg 传递寄存器总是被调用破坏。请参阅是否保存了 rdi 和 rsi 调用方或被调用方保存的寄存器?
但是系统调用调用约定通常会保留除返回值调用之外的所有寄存器。(通常包括偶数条件代码/标志。)请参阅i386 和 x86-64 上 UNIX 和 Linux 系统调用的调用约定是什么