指令系统
指令集
指令集从本质上可以分为复杂指令集(Complex Instruction Set Computing,CISC)和精简指令集(Reduced Instruction Set Computing,RISC)两种。复杂指令集的特点是能够在一条指令内完成很多事情。
指令架构(Instruction Set Architecture, 缩写为ISA),是软件和硬件的接口,不同的应用需求,会有不同的指令架构。
RISC-V指令集使用模块化的方式进行组织,每一个模块使用一个英文字母来表示。最基本也是唯一强制要求实现的指令集部分是I字母代表的基本整数指令集。
RISC-V指令格式
常见的RISC-V指令集如下表所示
基本指令集 | 含义 |
---|---|
RV32I | 32位整数指令集 |
RV32E | RV32I的子集,用于小型嵌入式场景 |
RV64I | 64位整数指令集,兼容RV32I |
RV128I | 128位整数指令集,兼容RV64I和RV32I |
RISC-V有六种基本指令格式:
指令类型 | 操作 |
---|---|
R-type | 用于寄存器-寄存器操作 |
I-type | 用于短立即数和访存 load 操作 |
S-type | 用于访存 store 操作 |
B-type | 用于条件跳转操作 |
U-type | 用于长立即数 |
J-type | 用于无条件跳转 |
在RISC-V中对于所有指令,要读写的寄存器的标识符总是在同一位置,意味着在解码指令之前,就可以先开始访问寄存器,这些格式的立即数字段总是符号扩展,符号位总是在指令中最高位,各种类型的指令构成如下图所示:
字段 | 含义 |
---|---|
opcode(操作码) | 指令的基本操作,这个缩写是它惯用名称 |
rd | 目的操作寄存器,用来存放操作结果 |
funct3 | 一个另外的操作码字段 |
rs1 | 第一个源操作数寄存器 |
rs2 | 第二个源操作数寄存器 |
funct7 | 一个另外的操作码字段 |
imm | 立即数 |
R型指令
R型指令构成如下:
R型的全部指令(RV32I)
R型指令包括加法、减法、逻辑运算、移位运算。
示例:
add a0, a1, a2 //a0 = a1 + a2
sub a0, a1, a2 //a0 = a1 - a2
sll a0, a1, a2 //a0 = a1 << a2(低位补0)
srl a0, a1, a2 //a0 = a1 >> a2(高位补0)
sra a0, a1, a2 //a0 = a1 >> a2 (算术右移,高位补原来的符号位)
slt a0, a1, a2 //a1 < a2 ? a0 = 1 : a0 = 0
xor a0, a1, a2 //a0 = a1 ^ a2
or a0, a1, a2 //a0 = a1 | a2
and a0, a1, a2 //a0 = a1 & a2
I型指令
I型指令构成如下:
I型的全部指令(RV32I)
I型指令包括立即数的运算和load指令。
示例:
addi a0, a1, 0x5 //a0 = a1 + 0x5
subi a0, a1, 0x05 //a0 = a1 - 0x05
slli a0, a1, 0x05 //a0 = a1 << 0x05(低位补0)
srli a0, a1, 0x05 //a0 = a1 >> 0x05(高位补0)
srai a0, a1, 0x05 //a0 = a1 >> 0x05 (算术右移,高位补原来的符号位)
slti a0, a1, 0x05 //a1 < 0x05 ? a0 = 1 : a0 = 0
xori a0, a1, 0x05 //a0 = a1 ^ 0x05
ori a0, a1, 0x05 //a0 = a1 | 0x05
andi a0, a1, 0x05 //a0 = a1 & 0x05
示例:
lb x10, 0(x1) //将x1的值加上0,将这个值作为地址, 取出这个地址所对应的内存中的值, 将这个值赋值给x10(取出的是8位数值)
lh x10, 0(x1) //从内存中取出16位数值
lw x10, 0(x1) //从内存中取出32位数值
lbu x10, 0(x1) //从内存中取出8位无符号数值
lhu x10, 0(x1) //从内存中取出16位无符号数值
S型指令
S型指令构成如下:
S型的全部指令(RV32I)
S型指令包括store指令。
示例:
sb x10, 0(x1) //x1的值加上0,将这个值作为地址, 将x10的值存储到上述地址所对应的内存中去 (只会将x10的值的低8位写入)
sh x10, 0(x1) //只会将x10的值的低16位写入
sw x10, 0(x1) //只会将x10的值的低32位写入
B型指令
B型指令构成如下:
(注:imm[0]被丢弃,因为它始终为零)
///-----------------------------------------------------------------------------------------------------------
分支跳转指令的基址是分支指令所在地址,偏移量offset为0会导致指令卡死,形成无限循环。
bne
指令会比较 x9
和 x0
的值。
如果不相等,程序将跳转到 PC + 0
,即下一条指令。
bne x9, x0, 0
的行为:
- 当
x9
和x0
不相等时,程序计数器(PC)会被设置为PC + (offset << 1)
,其中offset
是一个相对偏移量。如果offset
是0
,则计算结果是PC + 0
,即程序计数器不会改变,指向当前指令地址,这会导致以下行为:- 指令重新执行:程序计数器保持不变,指向当前指令,这意味着处理器将不断地重新执行这条
BEQ
指令。 - 无限循环:由于
rs1
和rs2
的值不变且相等,条件总是满足,导致程序卡在这条指令上,形成无限循环。
- 指令重新执行:程序计数器保持不变,指向当前指令,这意味着处理器将不断地重新执行这条
bne x9 x0 0
0000000,00000,01001,001,0000,0,1100011
imm[4:1]=0000
偏移量offset
是一个12位的值,在指令中存储时,它已经经过左移一位的处理,即存储的偏移量实际上是 offset << 1
。
当需要计算分支目标地址时,处理器会将 offset
左移一位,得到实际的字节偏移量。
bne x9 x0 4
0,000000,00000,01001,001,0010,0,1100011
imm[4:1]=0010
bne x9 x0 8
0000000,00000,01001,001,0100,0,1100011
imm[4:1]=0100
///-----------------------------------------------------------------------------------------------------------
B型的全部指令(RV32I)
B型指令包括条件跳转指令。
示例:
beq a1,a2,Label //if(a1==a2){goto Label;}
bne a1,a2,Label //if(a1!=a2){goto Label;}
blt a1,a2,Label //if(a1< a2){goto Label;}
bgt a1,a2,Label //if(a1> a2){goto Label;}
bge a1,a2,Label //if(a1<=a2){goto Label;}
ble a1,a2,Label //if(a1>=a2){goto Label;}
U型指令
U型指令构成如下:
U型的全部指令(RV32I)
示例“”
lui x10, 0x65432 //得到立即数的高20位,低位补0,立即数范围为:0x00~0xFFFFF
J型指令
J型指令构成如下:
J型的全部指令(RV32I)
示例:
jal ra, symbol // 跳转到Symbol中去, 并把ra设置成返回地址 Symbol 可以是自定义的Label ,也可以是某个函数名
jal ra, 100 // 跳转到pc + 100 * 2的地方中去, 并把ra设置成返回地址 pc相对寻址,对应的是位置无关代码(PIC)
jalr ra, 40(x10) // 跳转到x10+40 的地方中去, 并把ra设置成返回地址x10+40必须是绝对地址,指向内存中某个确定的地方(往往是函数的开头),非PIC
通用寄存器
RV32I有32个通用寄存器,以及一个PC寄存器。其中有一个通过硬件设置的值恒为 0 的 x0 寄存器
注:RISC-V的32个寄存器x0~x31是用0~31这些数字来表示。
参考资料:
RISC-V 指令集介绍