为什么在子程序调用中没有完整的上下文保存?

时间:2015-11-26 04:28:30

标签: assembly call cpu-registers subroutine context-switch

在子程序调用中,我们保存pc的内容,以便重新启动我们的调用例程。但是,如果被调用的子程序改变了通用寄存器的值,会发生什么?如果它必须访问存储在寄存器中的旧值,它是否会对调用子例程造成任何问题?

2 个答案:

答案 0 :(得分:4)

  

但是如果被调用的子程序改变了通用寄存器的值,会发生什么?

这取决于子程序修改的寄存器。根据{{​​3}},有一个寄存器列表,子程序在合同中有义务不修改(以及子程序可以自由修改的另一个寄存器列表)。

如果一个子程序不遵守这个合同并修改它不应该有的寄存器,那么就会发生坏事。

如果子程序想要使用寄存器,则必须不修改,必须先将这些寄存器值保存到堆栈中。保存寄存器值后,可以使用寄存器获取新值。子例程完成后,必须使用堆栈上保存的值来恢复原始寄存器值。这样,子程序可以根据需要使用寄存器,但是对于调用者来说,寄存器没有可观察到的修改。

  

如果必须访问存储在寄存器中的旧值,它是否会对调用子例程造成任何问题?

只要子程序遵循调用约定,否则。如果子程序没有,它会破坏(或者#34; clobbers")"保留"中的原始值。注册,然后是的,它会引起问题。

但并非所有寄存器都必须保留。根据调用约定,子例程可以修改一些寄存器。如果这些寄存器对调用者很重要,那么调用者必须在调用子例程之前将这些寄存器保存到堆栈中,然后在调用子例程之后使用堆栈恢复寄存器值。

答案 1 :(得分:3)

有两个相互矛盾的需求:

  • 被称为函数(被调用者)需要临时寄存器来完成他们的工作。
  • 调用者需要一些状态才能在函数调用中存活。

如果调用者必须保存/恢复它想要保留的所有内容,或者被调用者必须保存/恢复它使用的每个寄存器,那么它会很慢。

冲突需求的解决方案是ABI定义哪些寄存器被callee保存,哪些寄存器可以被破坏。非调用保留的寄存器可能不会被调用的特定函数破坏,但调用者必须假定它们是。

相关:Why not store function parameters in float registers?我在那里的答案考虑了有太多参数传递寄存器和没有足够的调用保留寄存器之间的权衡。

在循环中调用另一个函数的函数通常会将其循环计数器和其他一些东西保存在调用保留的寄存器中。被调用的函数要么根本不使用寄存器,要么保存/恢复它。

如果调用函数的状态多于调用保留寄存器,则必须"溢出"一些状态记忆(即保存/恢复)。理想情况下,它可以溢出在调用之前不需要保存的未修改值,仅在之后重新加载(例如,非静态数组或结构的基址)。这比将内存往返于循环计数器之类的依赖链中更有效。 (如果被调用的函数只需要几个周期,但无法内联,因为它是单独编译的,这很重要。它还只是保存指令/代码大小。)

x86有很多different calling conventions。有关链接,请参阅x86 tag wiki。 Agner Fog对这个主题有很好的指导。

在x86-64 SystemV ABI(由Linux和OS X使用)中:

  • 呼叫保留:RBP,RBX和R12-R15
  • call-clobbered:xmm0-15,flags,所有其他整数寄存器。 (包括r11,它不用于传递任何东西,因此可以通过包装/填充函数用作临时寄存器。)

如果你愿意,可以制作非ABI兼容的功能,其中调用者知道哪个寄存器被调用的函数实际上是clobbers。编译器可以在使用gcc -fwhole-program等选项或链接时优化时执行此操作。通常编译器总是生成符合ABI的功能,因为他们不确定他们发出的定义将是链接时使用的定义。显然,手写的ASM可以做任何事情,但是除了一小部分功能之外,手动执行任何操作都是维护的噩梦。