算术运算指令
ADD指令是R-type类型(寄存器类型,操作数有三个,都是寄存器,最终存放结果的寄存器放在第一个参数)
add rd,rs,rs2:将RS1里取出一个值,再从RS2里取出一个值,两个值相加,放进RD中。
例如:
add x5,x6,x7
GNU编译出来的文件都是ELF格式的。
有符号数在计算中的表示:二进制补码。
RISC-V基础整数指令集
图2.1是RV32I基础指令集的一页图形表示。对于每幅图,将有下划线的字母从左到右连接起来,就可以组成完整的RV32I指令集。
- {}列举了指令的所有变体,变体用加下划线的字母或下划线字符_表示。特别的,下划线字符_表示对于此指令变体不需用字符表示。
上图表示了4个RV32I指令:slt、slti、sltu、sltiu
RV32I指令格式
图2.2展示了六种基本指令格式。
- R类型指令:用于寄存器到寄存器的操作。
- I类型指令:用于短立即数和访存load操作。
- S类型指令:用于访存store操作。
- B类型指令:用于条件跳转操作。
- U类型指令:用于长立即数。
- J类型指令:用于无条件跳转。
图2.3使用图2.2的指令格式列出了图2.1中所出现的所有RV32I指令的操作码。
- RISC-V指令只有六种格式。
- 所有的指令都是32位长,这简化了指令解码。
- RISC-V指令提供了三个寄存器操作数。
- RISC-V中所有指令,要读写的寄存器的标识符总是在同一位置,意味着在解码指令之前,就可以先访问寄存器。
- 指令中立即数字段总是符号扩展,符号位总是在指令中最高位。这意味着可能成为关键路径的立即数符号括号,可以在指令解码之前进行。
- 所有位全部是0是非法的RV32I指令。因此,视图跳转到被清零的内存区域的错误跳转,会立即触发异常,这可以帮助调试。所有位全部是1的指令也是非法指令,它将捕获其它常见的错误,例如未编程的非易失性内存设备、断开连接的内存总线或者是坏掉的内存芯片。
RV32I寄存器
图4列出了RV32I寄存器以及有RISC-V应用程序二进制接口(ABI)所定义的寄存器名称。
RV32I有31个寄存器加上一个值恒为0的x0寄存器。
为常量0单独分配一个寄存器是RISC-V ISA能如此简单的一个很大的因素。
RV32I整数计算
-
简单的算术指令(add,sub)、逻辑指令(and,or,xor)以及移位指令(sll,srl,sra)和其他ISA差不多。他们从寄存器读取两个32位的值,并将32位结果写入目标寄存器。
-
RV32I还提供了这些指令的立即数版本。
-
程序可以根据比较结果生成布尔值。为应对这种使用场景,RV32I提供了一个当小于时置位的指令。如果第一个操作数小于第二个操作数,将目标寄存器设置为1。对于这个指令,有一个符号版本(slt)和无符号版本(sltu),分别用于处理有符号和无符号的整数比较。相应的,上述两条指令也有立即数版本(slti、sltiu)。
-
虽然RV32I分支指令可以检查两个寄存器之间的所有关系,但一些条件表达式涉及多对寄存器之间的关系。对于这些表达式,编译器或汇编语言程序员可以将slt以及与或异或等逻辑指令组合使用来解决更复杂的条件表达式。
-
图2.1剩下的两条整数计算指令主要用于构造大的常量数值和链接。加载立即数到高位(lui(load upper immediate))将20位常量加载到寄存器的高20位。接着就可以使用标准的立即指令来创建32位常量。这样子,仅仅使用两条32位RV32I指令,就可以创造一个32位常量。
-
向PC高位加上立即数(auipc(add upper immediate to pc)),让我们仅使用两条指令,就可以基于当前PC以任意偏移量转移控制流或者访问数据。
-
将auipc(add upper immediate to pc)中的20位立即数与jalr(jump add link register)中12位立即数组合,我们就可以将执行流转移到任意32位PC相对地址。
-
将auipc加上普通加载或存储指令中的12位立即数偏移量,我们就可以访问任何32位PC相对地址的数据。
-
RISC-V操作始终是以完整的寄存器宽度。内存访问需要的能量比算术运算高几个数量级,因此低宽度的数据访问可以节省大量的能量,但低宽度的运算不会。
-
RV32I提供了单独的移位指令,不包含乘法和除法,它们包含在可选的RV32M扩展中。
-
即使处理器没有添加乘除法扩展,完整的RISC-V软件栈也可以运行,这可以缩小嵌入式芯片的面积。
RV32I的Load和Store
- 除了提供32位字的lw,sw(load word,store word),如图2.1中说明,RV32I支持加载有符号和无符号字节和半字(lb、lbu、lh、lhu)和存储字节和半字(sb、sh)。有符号字节和半字符号扩展为32位再写入目的寄存器。
- 即使是自然数据类型更窄,低位宽数据也会被扩展后再处理,这使得后续的整数计算指令能正确处理所有的32位。常用的无符号字节和半字,在写入目标寄存器之前都被无符号扩展到32位。
- 加载和存储支持的唯一寻址模式是符号扩展12位立即数到基地址寄存器。
- RV32I省略了复杂寻址模式。RV32I寻址不会歧视任何数据类型。
- RISV-V没有特殊的堆栈指令。将31个寄存器中的某一个作为堆栈指针,标准寻址模式使用起来和压栈(push)和出栈(pop)类似。
RV32I条件分支
- RV32I可以比较两个寄存器并根据比较结果进行分支跳转。比较可以是:相等beq(branch equal)、不相等(branch not equal)、大于等于bge(branch greater equal)、小于blt(branch less than)。最后两种是有符号比较,RV32I也提供相应的无符号版本比较:bgeu和bltu。
- 剩下的两个比较关系(大于和小于等于)可以通过简单交换两个操作数,完成比较。
- RISC-V指令长度必须是两个字节的倍数。
- 分支指令的寻址方式是12位的立即数乘以2,符号扩展它,然后将得到的值加到PC上作为分支的跳转地址。
RV32I无条件跳转
- 图2.1中的跳转并链接(jal)具有双重功能。若将下一条指令PC+4的地址保存到目标寄存器中,通常是返回地址寄存器ra(return adress),便可以用它来实现过程调用。
- 如果使用零寄存器(x0)替换ra作为目标寄存器,则可以实现无条件跳转,因为x0不能更改。像分支跳转一样,jal将其20位分支地址乘以2,进行符号扩展后再添加到PC上,便到了跳转地址。
- 跳转和链接指令的寄存器版本(jalr)同样是多用途的,它可以调用地址是动态计算出来的函数,或者也可以实现调用返回(ra作为源寄存器,零寄存器(x0)作为目的寄存器)。
- switch和case语句的地址跳转,也可以使用jalr指令,目的寄存器设为x0.RV32I避开了错综复杂的程序调用指令。
RV32I杂项
- 图2.1中的控制状态寄存器指令csrrc(control status register read & clear bit)、csrrs(control status register read & set bit)、csrrw(control status register read & write)、csrrci(control status register read & clear bit immediate)、csrrsi(control status register read and set bit immediate)、csrrwi
(control status register read & write immediate),使我们可以轻松地访问一些程序性能计数器。 - 对于这些64位计数器,我们一次可以读取32位。这些计数器包括了系统时间,时钟周期以及执行的指令数目。
- 在RISC-V指令集中,ecall指令用于向运行时环境发出请求,例如系统调用。调试器使用ebreak指令将控制转移到调试环境。
- fence指令:对外部可见的访存请求,如设备I/O和内存访问等进行串行化。
- 外部可见指对处理器的其它核心、线程,外部设备或协处理器可见。fence.i指令:同步指令和数据流。
RV32I从RISC-I继承了如下特性:
- 32位字节可寻址的地址空间
- 所有指令均32位长
- 31个寄存器,全部32位宽,寄存器0硬连线为0
- 所有的操作都在寄存器之间(没有寄存器到内存的操作)