本文在学习了x86和ARM6两种指令集架构之后,以ISA的寄存器、寻址方式、常用指令、堆栈操作等为基础,从程序员的角度介绍RISC-V指令集架构的基础工作流程。对一个指令集架构学习的流程一般分为两个部分:寄存器布局和指令集。
与x86和ARM64不同的是,对于RV32I、RV64I、RV128I,RISC-V都定义了32个通用寄存器,名称以及用法如下图所示。
RISC-V软件将19个寄存器分为两组:x5-x7以及x28-x31是临时寄存器,在过程调用中不被被调用者保存;x8-x9以及x18-x27是保存寄存器,在过程调用中必须被保存。而x10-x17八个参数寄存器,用于传递参数或返回值,x1一个返回地址寄存器,用于返回到起始点。
与x86和ARM64不同的是,RISC-V的指令是定长的,都为32位,且只有6种类型,这样做简化了指令解码。具体六种指令格式如下,其中,
R类型用于寄存器-寄存器操作;
I类型用于短立即数和访存load操作;
S类型用于访存store操作;
B类型用于条件跳转操作;
U类型用于长立即数;
J类型用于无条件跳转。
其中R、I、S和U类型是四种基础指令,B和J类型都只包含一条指令,具体指令格式如下:
接下来介绍RISC-V中一些常用指令,后面将给出一个简单的C语言程序与其对应的RISC-V汇编语言伪代码。
内存和寄存器之间传输数据的指令,称为数据传输指令。
取双字
取内存中的A[8]至寄存器x9,字节寻址8×8偏移地址为64
ld x9, 64(x22) //x22存放内存中数组的基址,偏移64,取出的数据A[8]存放至寄存器x9
存双字
将寄存器x9中数据存至内存的A[12],字节寻址8×12偏移地址为96
sd x9, 96(x22) //x22存放内存中数组的基址,偏移96,寄存器x9的数据存至A[12]
立即数加法
addi x22, x22,4 //x22 = x22 + 4
寄存器加法
add x10,x5,x6 //x10=x5+x6,结果保存在x10中
在使用需要保存的寄存器时进行参数传递等操作时(例如x1用来保存函数的返回地址),需要先把寄存器的当前值换出到栈中保存,再用寄存器进行函数调用,调用结束后,换回寄存器的旧值。
RISC-V中,寄存器x2用来保存栈指针,别名也叫做sp。栈的增长方向是由高地址向低地址方向增长。
//例如现在需要将x1和x10两个寄存器的值保存在栈中
addi sp,sp,-16 //sp增长16位,用来保存x1和x10的旧值
sd x1,8(sp) //保存x1的值
sd x10,0(sp) //保存x10的值
ld x10,0(sp) //恢复x10的值
ld x1,8(sp) //恢复x1的值
addi sp,sp,16 //恢复栈针
无条件跳转
jalr x0, 0(x1) //返回x1存储的地址
jal x0, func //跳转到函数func,返回地址保存在x1中
C语言程序
int g(int x)
{
return x+3;
}
int f(int x)
{
return g(x);
}
int main(void)
{
return f(8)+1;
}
RISC-V汇编语言伪代码
g:
addi x10,x10,3 //x10=x10+3,传入的参数加3
jalr x0,0(x1) //函数返回
f:
jal x1,g //调用函数g
jalr x0,0(x1) //返回
main:
addi sp,sp,-16 //sp增长16位,用来保存x1和x10的旧值
sd x1,8(sp) //保存x1的值
sd x10,0(sp) //保存x10的值
addi x10,x0,8 //x10=0+8,将立即数8放入寄存器x10中
jal x1,f //调用函数f,返回地址存在x1
addi x10,x10,1 //x10=x10+1,即f(8)+1
ld x10,0(sp) //恢复x10的值
ld x1,8(sp) //恢复x1的值
addi sp,sp,16 //恢复栈针