RISCV减低边界检查的开销方法/硬连线为零的可能实现方式/大立即数的RISC-V编址和寻址问题/分支和跳转指令中立即数字段的PC相对偏移表示问题

RISC-V

Posted by Bruce Lee on 2024-01-03

使用最高有效位来减低边界检查的开销

我们可以至少保证对于数组的边界的值,一定是一个正确的合理的数y(指代y是一个有符号正数).
我们需要检查下标x是否满足0 <= x < y
则仅使用:

1
bgeu x, y, error //伪代码

就可以完成两项检查.
这里利用了最高有效位的双重含义:在有符号整数中,最高有效位为1,表示一个负数,但是在无符号整数中,表示这是一个很大的数.

case/switch语句的支持

对于这类语句,完全可以将其分解为多层if-than-else语句,但是这里有另一种更有效的方法:建立分支地址表.
RISC-V为了支持这种功能,有了一个间接跳转指令(jalr)

过程支持与约定

一般而言,x10到x17作为过程调用时存放参数和返回值的寄存器,x1作为返回地址寄存器
使用jal x1, procedureAddress
和jalr x0, 0(x1)就是完整的跳转和返回

无条件跳转的两种形式

1.条件分支形式

1
beq x0, x0, Lable //其他变体

2.跳转-链接分支

1
jal x0, Lable //unconditionally
1
jalr x0, 0(x1) //寄存器跳转的样例

x0硬连线为零,其效果是丢弃返回地址

关于硬连线为零的三种可能实现方式

硬件实现:在处理器的硬件设计中,x0 寄存器可以被实现为一个常量生成器,总是输出零。这意味着无论任何写操作尝试将何种值写入这个寄存器,寄存器中的值始终保持为零。

解码逻辑:处理器的指令解码逻辑可以识别出任何试图写入 x0 寄存器的操作,并简单地忽略它们。在这种情况下,尽管指令可能试图改变 x0 的值,解码逻辑确保这些操作不会有任何效果。

特殊的寄存器单元:有些处理器可能会为 x0 设置一个特殊的寄存器单元,这个单元内部不包含实际的存储设备,而是直接返回零。这样,无论任何操作,x0 寄存器都会返回零。

现象:由于历史原因,程序计数器的功能,更贴切的名字是指令地址寄存器
现象:RISC-V中的sp寄存器:x2
现象:按照历史惯例,栈按照从高到低的地址增长顺序.

由过程来负责选择临时寄存器和保存寄存器并且维护栈的秩序

x5到x7和x28到x31为临时寄存器
x8到x9和x18到x27为保存寄存器

我们应该尽可能使用临时寄存器,来减少寄存器换出的次数,减少维护栈的压力

负责内容

调用者需要负责将还想使用的临时寄存器和参数寄存器压栈
被调用者需要将使用的保存寄存器压栈,如果该被调用者不是叶子过程,则还需要维护返回地址寄存器,临时寄存器,参数寄存器

编写一个将来预见需要被调用的子程序时,需要站在被调用者角度来处理和维护栈

对于x3寄存器

关于c语言中变量的解释,对于动态的好说,而对于静态(static)的变量,由于其全局可用,为了简化静态变量的访问,RISC-V的编译器保留了一个寄存器x3用作全局指针(global pointer)gp

为局部变量获取空间

另一种寄存器:x8寄存器

这是帧指针寄存器(fp)
通常fp和sp来显式表明一个过程帧,帧内是不仅包含保存的寄存器,还有过程中涉及的局部变量和结构体.
我们对于局部变量的引用,一般是通过fp,因为fp比sp有更多的稳定性

RISC-V编译器在编译没有局部变量的过程时fp变化

如果在过程中栈内没有局部变量,编译器将不设置和不恢复帧指针以节省时间.
现象:RISC-V约定将栈中额外的参数(超过8个)放在fp的上方

RISC-V C编译器只有在改变来栈指针的过程中使用帧指针

可以说如果没有局部变量,则不使用

在堆上获取空间

对于静态变量和动态数据结构,由于数组和链表的特性,其是存放在堆上.
在用户地址空间内,最上层是栈,栈向下增长.
最低下是保留空间,其上是代码段,再上是静态数据段,保留常量和static变量,然后是动态数据段,这里是malloc的地方,向栈方向增长.这个地方就是堆.

悬挂指针和内存泄漏就是关于堆上的两个极端的问题.

对于递归处理和迭代处理

迭代,类比与c++的迭代器,是一种循环处理数据的工具.
如果递归调用为尾调用(tail call)时,可以将其转换为迭代处理,更加高效.

大立即数的RISC-V编址和寻址问题

使用U型指令lui加载32位立即数的高20位(因为低12位可以由addi指令中的12位立即数字段来存储/加载)
此时64位寄存器的31-12被填充,其他位(高位被31位复制填充,低位被0填充)
然后使用addi来填充低12位,但是注意:
addi指令中的立即数字段属于有符号整数,在和rs1进行计算时,其中的立即数被有符号扩展为64位,如果这个12位立即数的最高有效位是0,那么可以按照逻辑正确填充到rd中.如果最高有效位是1,那么就会出现错误.
举一个简单例子:
8位寄存器需要存储一个8位的立即数(假设该8位立即数已经足够大,属于大立即数)
使用一个lui加载了高4位(假设lui只能加载高4位),此时使用addi(假设addi的立即数字段是4位)加载低4位.
如果addi中的立即数字段的最高有效位为1,则这个立即数就会被有符号扩展.
立即数为1110 1010
理想中:

1
lui rd, 1110

高4位1110已经被加载到rd中
rd:1110 0000

1
addi rd, rd, 1010

此时有:1110 0000 + 0000 1010 = 1110 1010
rd:1110 1010
实际上:

1
lui rd, 1110

rd: 1110 0000

1
addi rd, rd, 1010

此时有:1110 0000 + 1111 1010 = 1101 1010
我们想要的是1110 1010,出现的是1101 1010
问题在于addi指令中的立即数字段是12位的,并且被认为是有符号整数,当出现最高有效位为1时,在相加过程中,该立即数的高48位被1扩展.所以减少了2^12.为了避免这个问题,我们在lui的时候,需要加1.

RISC-V架构中分支和跳转指令中立即数字段的PC相对偏移表示问题

现在的所有指令都规定是32位的,并不随着指令的类型的改变而发生改变,但是由于RISC-V 架构师希望支持只有16位的指令,所以PC相对偏移表示分支和目标指令之间的半字数.

SB型指令和UJ型指令

条件分支指令是SB型指令,该指令的立即数字段是12位,表示正负2^11个半字
无条件跳转指令是UJ型指令(jal),该指令的立即数字段是20位.
由于RISC-V的无条件跳转都采用PC相对寻址,所以相对地址范围控制在(正负2^19个半字或1MiB)


If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. All the images used in the blog are my original works or AI works, if you want to take it,don't hesitate. Thank you !