5.4 RISC-V寄存器

我们之前看过了汇编语言和RISC-V的介绍。接下来我们看一下之后lab相关的内容。这部分的内容其实就是本节课的准备材料中的内容。

你们现在对于这个表达应该都很熟悉了,这个表里面是RISC-V寄存器。寄存器是CPU或者处理器上,预先定义的可以用来存储数据的位置。寄存器之所以重要是因为汇编代码并不是在内存上执行,而是在寄存器上执行,也就是说,当我们在做add,sub时,我们是对寄存器进行操作。所以你们通常看到的汇编代码中的模式是,我们通过load将数据存放在寄存器中,这里的数据源可以是来自内存,也可以来自另一个寄存器。之后我们在寄存器上执行一些操作。如果我们对操作的结果关心的话,我们会将操作的结果store在某个地方。这里的目的地可能是内存中的某个地址,也可能是另一个寄存器。这就是通常使用寄存器的方法。

寄存器是用来进行任何运算和数据读取的最快的方式,这就是为什么使用它们很重要,也是为什么我们更喜欢使用寄存器而不是内存。当我们调用函数时,你可以看到这里有a0 - a7寄存器。通常我们在谈到寄存器的时候,我们会用它们的ABI名字。不仅是因为这样描述更清晰和标准,同时也因为在写汇编代码的时候使用的也是ABI名字。第一列中的寄存器名字并不是超级重要,它唯一重要的场景是在RISC-V的Compressed Instruction中。基本上来说,RISC-V中通常的指令是64bit,但是在Compressed Instruction中指令是16bit。在Compressed Instruction中我们使用更少的寄存器,也就是x8 - x15寄存器。我猜你们可能会有疑问,为什么s1寄存器和其他的s寄存器是分开的,因为s1在Compressed Instruction是有效的,而s2-11却不是。除了Compressed Instruction,寄存器都是通过它们的ABI名字来引用。

a0到a7寄存器是用来作为函数的参数。如果一个函数有超过8个参数,我们就需要用内存了。从这里也可以看出,当可以使用寄存器的时候,我们不会使用内存,我们只在不得不使用内存的场景才使用它。

表单中的第4列,Saver列,当我们在讨论寄存器的时候也非常重要。它有两个可能的值Caller,Callee。我经常混淆这两个值,因为它们只差一个字母。我发现最简单的记住它们的方法是:

  • Caller Saved寄存器在函数调用的时候不会保存

  • Callee Saved寄存器在函数调用的时候会保存

这里的意思是,一个Caller Saved寄存器可能被其他函数重写。假设我们在函数a中调用函数b,任何被函数a使用的并且是Caller Saved寄存器,调用函数b可能重写这些寄存器。我认为一个比较好的例子就是Return address寄存器(注,保存的是函数返回的地址),你可以看到ra寄存器是Caller Saved,这一点很重要,它导致了当函数a调用函数b的时侯,b会重写Return address。所以基本上来说,任何一个Caller Saved寄存器,作为调用方的函数要小心可能的数据可能的变化;任何一个Callee Saved寄存器,作为被调用方的函数要小心寄存器的值不会相应的变化。我经常会弄混这两者的区别,然后会到这张表来回顾它们。

如果你们还记得的话,所有的寄存器都是64bit,各种各样的数据类型都会被改造的可以放进这64bit中。比如说我们有一个32bit的整数,取决于整数是不是有符号的,会通过在前面补32个0或者1来使得这个整数变成64bit并存在这些寄存器中。

学生提问:返回值可以放在a1寄存器吗?

TA:这是个好问题。我认为理论上是可以的,如果一个函数的返回值是long long型,也就是128bit,我们可以把它放到一对寄存器中。这也同样适用于函数的参数。所以,如果返回值超过了一个寄存器的长度,也就是64bit,我们可以将返回值保存在a0和a1。但是如果你只将返回值放在a1寄存器,我认为会出错。

学生提问:为什么寄存器不是连续的?比如为什么s1与其他的s寄存器是分开的?

TA:我之前提到过,但是也只是我的猜想,我并不十分确定。因为s1寄存器在RISC-V的Compressed Instruction是可用的,所以它才被分开。

学生提问:除了Stack Pointer和Frame Pointer,我不认为我们需要更多的Callee Saved寄存器。

TA:s0 - s11都是Callee寄存器,我认为它们是提供给编译器而不是程序员使用。在一些特定的场景下,你会想要确保一些数据在函数调用之后仍然能够保存,这个时候编译器可以选择使用s寄存器。

Last updated