堆栈中的 MIPS 函数和变量
MIPS functions and variables in stack
我已经接触了 MIPS-32,我提出了一个问题,如果一个变量,例如 $t0 声明在一个函数中具有值,是否可以被另一个函数更改,以及这与堆栈有什么关系,这是变量在内存中的位置。我所说的一切都是汇编语言。而且,我想要一些关于这种用法的例子,这是一个函数是否改变,另一个函数的变量值,以及这个变量如何“生存”或不“生存”,如果变量是作为副本或参考。
(我希望我们能创造一个环境,让上面的概念性问题可以被更多地探索)
$t0
declared having the value in one function can be altered by another
$t0
被称为调用破坏寄存器。就硬件而言,它与其他寄存器没有什么不同——调用破坏与调用保留是软件约定的一个方面,调用调用约定,它是应用程序二进制接口 (ABI) 的子集。
调用约定在遵循时允许函数 F
调用另一个函数 G
,只知道 G
的签名——名称、参数及其类型, return 类型。如果 G
改变,函数 F
也不必改变,只要两者都遵循约定。
Call clobbered 并不意味着它必须被破坏,并且在编写自己的代码时,您可以按照自己喜欢的方式使用它(当然,除非您的课程要求遵循 MIPS32 调用约定)。
按照惯例,调用破坏的寄存器可以放心使用:使用它时只需将值放入其中!
如果需要,也可以使用调用保留寄存器,但是应该假定它们已经被某个调用者使用(可能不是直接调用者,而是某个远程调用者),它们包含的值必须在之前恢复退出函数回到 return 给它的调用者。当然,这只有在将寄存器重新用于新用途之前保存原始值才有可能。
两组寄存器(调用clobbered/preserved)服务于两个常见的用例,即便宜的临时存储和长期变量。前者不需要 preserve/restore 的努力,而后者都需要这种努力,尽管它为我们提供了在函数调用后仍然存在的寄存器,这很有用,例如,当循环具有函数调用时。
当我们需要先保留,然后恢复调用保留的寄存器时,堆栈就会发挥作用。如果出于某种原因我们想使用调用保留寄存器,那么我们需要保留它们的原始值以便以后恢复它们。最合理的方法是将它们保存在堆栈中。为此,我们从堆栈中分配一些 space。
为了分配一些本地内存,堆栈指针递减以保留一些函数space。由于堆栈指针在 return 到调用者时必须具有相同的值,因此此 space 必须在 return 时释放。因此堆栈非常适合本地存储。保留寄存器的原始值也必须在 return 时恢复给调用者,因此使用本地存储是合适的。
https://www.dyncall.org/docs/manual/manualse11.html — 搜索“MIPS32”部分。
我们也来区分一下逻辑概念变量和物理概念存储。
在高级语言中,变量被命名并具有作用域(有限的生命周期)。在机器码中,我们有寄存器和内存等物理硬件(存储)资源;这些只是存在:它们没有生命周期的概念。这些硬件资源本身不是变量,而是我们可以用来为它们 lifetime/scope.
保存变量的地方
作为汇编语言程序员,我们在心理上(甚至是书面上)保留了我们的逻辑变量到物理资源的映射。编译器做同样的事情,知道程序变量的 scope/lifetime 并创建变量到机器代码存储的“心理”映射。当然,具有重叠生命周期的变量不能共享相同的硬件资源,但是当变量超出范围时,它的(映射到的)物理资源可以重新用于其他目的。
逻辑变量也可以移动到不同的物理资源。作为参数的逻辑变量可以在 CPU 寄存器中传递,例如$a0
,但随后被移入 $s
寄存器或(堆栈)内存位置。这就是机器代码的业务。
要为高级语言(或伪代码)变量分配一些硬件存储空间,我们只需初始化存储空间!硬件资源必然不断地被重新利用以容纳不同的逻辑变量。
另请参阅:
— 用于讨论变量分析。
What's the difference between caller-saved and callee-saved in RISC-V
我已经接触了 MIPS-32,我提出了一个问题,如果一个变量,例如 $t0 声明在一个函数中具有值,是否可以被另一个函数更改,以及这与堆栈有什么关系,这是变量在内存中的位置。我所说的一切都是汇编语言。而且,我想要一些关于这种用法的例子,这是一个函数是否改变,另一个函数的变量值,以及这个变量如何“生存”或不“生存”,如果变量是作为副本或参考。
(我希望我们能创造一个环境,让上面的概念性问题可以被更多地探索)
$t0
declared having the value in one function can be altered by another
$t0
被称为调用破坏寄存器。就硬件而言,它与其他寄存器没有什么不同——调用破坏与调用保留是软件约定的一个方面,调用调用约定,它是应用程序二进制接口 (ABI) 的子集。
调用约定在遵循时允许函数 F
调用另一个函数 G
,只知道 G
的签名——名称、参数及其类型, return 类型。如果 G
改变,函数 F
也不必改变,只要两者都遵循约定。
Call clobbered 并不意味着它必须被破坏,并且在编写自己的代码时,您可以按照自己喜欢的方式使用它(当然,除非您的课程要求遵循 MIPS32 调用约定)。
按照惯例,调用破坏的寄存器可以放心使用:使用它时只需将值放入其中!
如果需要,也可以使用调用保留寄存器,但是应该假定它们已经被某个调用者使用(可能不是直接调用者,而是某个远程调用者),它们包含的值必须在之前恢复退出函数回到 return 给它的调用者。当然,这只有在将寄存器重新用于新用途之前保存原始值才有可能。
两组寄存器(调用clobbered/preserved)服务于两个常见的用例,即便宜的临时存储和长期变量。前者不需要 preserve/restore 的努力,而后者都需要这种努力,尽管它为我们提供了在函数调用后仍然存在的寄存器,这很有用,例如,当循环具有函数调用时。
当我们需要先保留,然后恢复调用保留的寄存器时,堆栈就会发挥作用。如果出于某种原因我们想使用调用保留寄存器,那么我们需要保留它们的原始值以便以后恢复它们。最合理的方法是将它们保存在堆栈中。为此,我们从堆栈中分配一些 space。
为了分配一些本地内存,堆栈指针递减以保留一些函数space。由于堆栈指针在 return 到调用者时必须具有相同的值,因此此 space 必须在 return 时释放。因此堆栈非常适合本地存储。保留寄存器的原始值也必须在 return 时恢复给调用者,因此使用本地存储是合适的。
https://www.dyncall.org/docs/manual/manualse11.html — 搜索“MIPS32”部分。
我们也来区分一下逻辑概念变量和物理概念存储。
在高级语言中,变量被命名并具有作用域(有限的生命周期)。在机器码中,我们有寄存器和内存等物理硬件(存储)资源;这些只是存在:它们没有生命周期的概念。这些硬件资源本身不是变量,而是我们可以用来为它们 lifetime/scope.
保存变量的地方作为汇编语言程序员,我们在心理上(甚至是书面上)保留了我们的逻辑变量到物理资源的映射。编译器做同样的事情,知道程序变量的 scope/lifetime 并创建变量到机器代码存储的“心理”映射。当然,具有重叠生命周期的变量不能共享相同的硬件资源,但是当变量超出范围时,它的(映射到的)物理资源可以重新用于其他目的。
逻辑变量也可以移动到不同的物理资源。作为参数的逻辑变量可以在 CPU 寄存器中传递,例如$a0
,但随后被移入 $s
寄存器或(堆栈)内存位置。这就是机器代码的业务。
要为高级语言(或伪代码)变量分配一些硬件存储空间,我们只需初始化存储空间!硬件资源必然不断地被重新利用以容纳不同的逻辑变量。
另请参阅:
What's the difference between caller-saved and callee-saved in RISC-V