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

时间:2012-02-13 21:52:00

标签: assembly cpu-registers calling-convention abi

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

我正在使用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不是“全局的”,因为在过程中改变它的值不会影响它在程序之外的值。这就是你必须在开始时将新值保存到被调用者注册表中的原因。

R12,由于缺乏更好的词汇,保存的来电者是“全球性的”。通过该程序后,R12会对R12产生持久影响。

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

6 个答案:

答案 0 :(得分:83)

  

来电者保存的寄存器(AKA 易失性寄存器或 call-clobbered )用于保存需要的临时数量   不得在电话中保留。

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

call在这些寄存器中销毁临时值是正常的。

  

Callee保存的寄存器(AKA 非易失性寄存器或调用保留)用于保存长寿命值   保持呼叫。

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

答案 1 :(得分:12)

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

在您的示例代码中,NONE的寄存器是callee save,因为它不会尝试保存或恢复寄存器值。但是,它似乎不是一个完整的过程,因为它包含一个未定义标签的分支(l$loop)。所以它可能是一个程序中间的代码片段,它将一些寄存器视为callee save;你只是缺少保存/恢复说明。

答案 2 :(得分:3)

保存调用者/保存被调用者的术语基于一种非常糟糕的编程模型,其中调用者实际上确实保存/恢复了所有被调用者寄存器(而不是将长期使用的值保留在其他地方),以及被调用者实际上确实保存/恢复了所有保留调用的寄存器(而不是不使用其中的任何一个)。

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

实际上,高效的代码使值在不再需要时被破坏。编译器通常会生成一些函数,这些函数在函数开始时保存一些保留调用的寄存器(并在结束时恢复它们)。在函数内部,他们将这些reg用于需要在函数调用之间保留的值。

我更喜欢“保留呼叫”而不是“拥挤的呼叫” ,一旦您了解了基本概念,它们就毫不含糊且自我描述,并且不需要任何认真的头脑从呼叫者或被呼叫者的角度思考的体操。 (这两个术语都是从相同角度来看的。)

此外,这些术语的区别不止一个字母。

易失性/非易失性一词非常好,类似于存储是否在功耗方面失去价值的存储(例如DRAM vs. Flash)。但是C volatile关键字具有完全不同的技术含义,因此在描述C调用约定时这是“(非)易失性”的缺点。


  • 呼叫密集型,又名保存呼叫者易失性寄存器,对于下一次之后不需要的临时/临时值很有用函数调用。

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

从调用者的角度来看,call foo销毁(又称clobbers)所有被调用者占据的寄存器,或者至少您必须假设这样做。

您可以编写具有自定义调用约定的私有帮助器函数,例如您知道他们不会修改某个寄存器。但是,如果您仅知道(或想要假设或依赖)目标函数遵循正常的调用约定,那么您就必须将函数调用视为确实销毁了所有被调用满满的寄存器。从字面上看这就是这个名字的源头:一个调用破坏了这些寄存器。

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

  • 呼叫保留,又称为被呼叫者保存非易失性寄存器,在函数调用之间保留其值 。这对于进行函数调用的循环中的循环变量很有用,或者通常对非叶函数中的任何东西都有用。

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

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


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


ABI或调用约定定义了哪些

例如,参见What registers are preserved through a linux x86-64 function call中的x86-64 System V ABI。

此外,在我所知道的所有函数调用约定中,传递arg的寄存器总是被调用。参见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

答案 3 :(得分:1)

保存了来电者(又称易失性或呼叫者)的寄存器

  • 保存在调用者中的寄存器中的值是短期的,而不是短期的 从通话中保留
  • 它保存临时(即短期)数据

被叫方保存的(又称为非易失性或呼叫保留)寄存器

  • 保存有被调用方的寄存器可保存所有调用中的值,并且是长期的
  • 它保存通过多个功能/调用使用的非临时(即长期)数据

答案 4 :(得分:0)

除了上面的答案外,最好将被调用方保存的寄存器用于变量的值,这些变量的值在函数调用后需要保留,因为可能不必在被调用函数中备份寄存器值。否则,如果在函数调用之后不需要保存变量的内容,则使用调用者保存的寄存器,因为在调用者函数或被调用者函数中都不需要备份。

答案 5 :(得分:0)

我不确定是否可以添加任何内容,

保存的呼叫者意味着呼叫者必须保存寄存器,因为寄存器将在调用中被破坏,并且在调用返回后别无选择,只能保持为破坏状态(例如,返回值处于{{1 }}对于cdecl。将返回值恢复为被叫方调用之前的值是没有意义的,因为它是一个返回值)。

已保存被调用方”表示被调用方必须保存寄存器,然后在调用结束时将其恢复,因为它们可以保证在函数返回后,调用方可以包含相同的值,并且可以恢复它们,即使在通话过程中的某个时刻造成了破坏。

上述定义的问题是,例如在Wikipedia cdecl上,它说eaxeaxecx是保存呼叫者的,其余的是保存被呼叫者的,这表明调用者必须保存所有这三个寄存器,如果调用者最初没有使用这些寄存器中的任何一个,则调用者必须保存所有这三个寄存器。在这种情况下,呼叫者“被保存”变成了错误的称呼,但“呼叫被破坏”仍然正确适用。这与“其余”称为被调用者保存的相同。这意味着,如果某些寄存器始终未在调用中使用,则并非如此,所有其他x86寄存器将由被调用者保存和恢复。对于cdecl,edx可用于返回64位值。我不确定为什么eax:edx也会在需要时保存呼叫者,但确实如此。