模块化的指令体系结构设计
RISC-V的体系结构是模块化的。最基础的指令是RV32I,即32位的指令。这个是所有的RISC-V处理器都需要实现的指令。RISC-V体系结构可以在这个基础指令集上进行扩展:RV64I,这是64位的基础指令扩展;RV32M是乘法指令扩展;RV32F是单精度浮点扩展;RV32D是双精度浮点扩展等。RV32IMFD就代表了把对应的模块扩展到基础的RV32I中。
最基本的RV32I指令集
指令格式概述
RV32I有基本的6种指令格式,分别是:
- 用于寄存器和寄存器之间操作的R类型指令
- 用于短立即数和访存load的I型指令
- 用于访存store的S型指令
- 用于条件跳转的B型指令
- 用于长立即数的U型指令
- 用于无条件跳转的J型指令
所有指令的长度是32位长度。指令提供三个寄存器操作数。所有指令编码中,要读写的寄存器标识位置是固定的,在解码指令之前,就可以开始访问寄存器。立即数字段总是符号扩展,符号位总是在指令中的最高位,这意味着可能成为关键路径的立即数符号扩展可以在指令解码之前进行。
指令格式类型,对应的指令编码的位置:
可以看到,B类型和S类型是一样的,J类型和U类型是一样的,就是对于字段的解释不一样。所以,也可以认为是4种类型。
全0的指令码和全1的指令码都是非法指令,这个可以方便程序员调试。
RV32I整数指令概述
算术指令:add, sub
逻辑指令:add, or, xor
移位指令:sll, srl, sra
上述指令还有立即数的版本,立即数总是进行符号扩展.
程序可以根据比较结果生成布尔值。slt和sltu,也有立即数版本,slti, sltiu。(带u后缀的为无符号版本)
加载立即数到高位lui将20位常量加载到寄存器的高20位,接着可以使用标准的立即指令来创建32位常量。
auipc指令将立即数左移12位加到PC上。这样,可以将auipc中的20位立即数与jalr中的12位立即数组合,将执行流程转移到任何32位pc相对地址。而auipc加上普通加载或存储指令中的12位立即数偏移量,可以使得程序访问任何32位PC相对地址的数据。
RV32I的寄存器
一共32个寄存器,x0到x31,其中x0总是0。
寄存器有别名,别名可以帮助记忆关于调用惯例方面的规范。
下面是寄存器及其别名的对应关系:
x0 / Zero
x1 / ra (return address)
x2 / sp (stack pointer)
x3 / gp (global pointer)
x4 / tp (thread pointer)
x5 / t0 (temporary)
x6 / t1
x7 / t2
x8 / s0 / fp (saved register, frame pointer)
x9 / s1
x10 / a0 (function argument, return value)
x11 / a1 (function argument, return value)
x12 / a2
x13 / a3
x14 / a4
x15 / a5
x16 / a6
x17 / a7
x18 / s2 (saved register)
x19 / s3
x20 / s4
x21 / s5
x22 / s6
x23 / s7
x24 / s8
x25 / s9
x26 / s10
x27 / s11
x28 / t3
x29 / t4
x30 / t5
x31 / t6
还有一个是PC寄存器,该寄存器不属于通用寄存器,指向当前正在执行的指令的内存地址。
这里是别名的分类:zero, ra, sp, gp, tp, t0-t6, s0-s11, fp(s0), a0-a7。这里的别名在考虑到RISC-V上面执行过程调用惯例的时候会有意义,包括如何放置参数,如何放置返回值,哪些是调用者保存的,哪些是被调用者保存的等。对于底层的硬件来说,这些寄存器除了x0之外没有任何区别。
RV32I的Load和Store
lw (32bits)
sw
lb (8bits)
lbu
lh (16bits)
lhu
加载和存储支持的唯一的寻址模式是符号扩展12位立即数加上基地址寄存器。RISC-V使用的是小端机结构。
R32I的条件分支
beq
bne
bge
bgeu
blt
bltu
由于RISC-V指令长度必须是两个字节的倍数,分支指令的寻址方式是12位的立即数乘以2,符号扩展,然后加到PC上作为分支的跳转地址。
RISCV指令中没有条件码。
下面是大位宽数据的加法的举例:
add a0, a2, a4
sltu a2, a0, a2
add a5, a3, a5
add a1, a2, a5
RISC-V依赖于软件溢出检查。
检查无符号加法的溢出:
add t0, t1, t2
bltu t0, t1, overflow
检查有符号加法的溢出,一般情况下:
add t0, t1, t2
slti t3, t2, 0 # t3 = (t2<0)
slt t4, t0, t1 # t4 = (t1 + t2 <t1)
bne t3, t4, overflow
无条件跳转
jal将下一条指令PC+4的地址保存到目标寄存器中,通常是返回地址寄存器ra。如果使用x0来替换ra,则可以实现无条件跳转,因为x0不能被更改。
jalr可以调用地址是动态计算出来的函数,或者也可以实现调用返回(ra作为源寄存器,x0作为目标寄存器)。
RV32I杂项
控制状态(Constrol Status)寄存器的相关指令
csrrc, csrrs, csrrw, csrrci, csrrsi, csrrwi,可以用来访问一些程序性能计数器。这些是64位计数器,一次可以读取32位。包括系统时间,时钟周期,以及执行的指令数目。
ecall指令用于向运行时环境发出请求,如系统调用。
ebreak指令将控制转移到调试环境。
fence指令对外部可见的访存请求,例如设备IO和内存访问等进行串行化。
fence.i指令同步指令和数据流。
函数调用惯例
临时寄存器,参数寄存器不必保存。
保存寄存器函数调用前后要保持不变。
RV32I函数的入口
entry_label:
addi sp, sp, -framesize
sw ra, framesize-4(sp)
函数的结尾部分
lw ra, framesize-4(sp)
addi sp, sp, framesize
ret
伪指令:不是真的机器指令,会被汇编器翻译为真实的物理指令。
例如: ret 被汇编为:jalr x0, x1, 0
下表给出了一系列伪指令及其依赖的真实的处理器物理指令(这些伪指令都依赖于x0寄存器,从中可以看到x0寄存器的作用)。
下表是另外一些伪指令及其被汇编之后的物理指令:
下面是汇编器的指示符 assemble directives,可以在汇编语言的源代码中看到,了解这些指示符可以帮助理解汇编语言编写的程序。
.text 进入代码段
.aligh 2 后续代码按照2^2字节对齐
.global main 声明全局符号main
.section .rodata 进入只读数据段
.balign 4 数据段安装4字节对齐
.string "hello, %s!\n" 创建空字符结尾的字符串
上图是RISCV中程序内存不同区域的功能分配图(这里程序代码之后就是静态数据区,不一定就是1000 0000)。另外,PC在模拟器中一开始的时候是0x10000,但是不同的硬件平台可能会不同。
转自:https://lab.cs.tsinghua.edu.cn/cod-lab-docs/labs/4-riscv-inst/