571
社区成员
发帖
与我相关
我的任务
分享RISC-V(发音为“risk-five”)是一个基于精简指令集(RISC)原则的开源指令集架构(ISA)。
RISC-V 被设计成可以支持丰富的定制化和特殊化。基本整数 ISA 可被一个或者多个可选 指令集扩展进行增强,但是基本整数指令集不能被重新定义。最基础的指令是RV32I,即32位的指令。这个是所有的RISC-V处理器都需要实现的指令。RISC-V体系结构可以在这个基础指令集上进行扩展:RV64I,这是64位的基础指令扩展;RV32M是乘法指令扩展;RV32F是单精度浮点扩展;RV32D是双精度浮点扩展等。RV32IMFD就代表了把对应的模块扩展到基础的RV32I中。本文只讨论RV32I。
RV32I 基础指令集总共定义了 32 个 32 位的通用寄存器。它们分别被标记为x0 ~ x31。其中零号寄存器 x0 是只读寄存器,其值永远为零。在用汇编语言编写时,这 32 个寄存器的名称也根据其在调用约定中的职能而被重新命名。具体如下表
| 寄存器名称 | 汇编名称 | 功能描述 | 调用返回后其值是否会保证不变 |
|---|---|---|---|
|
x0 | zero | 零寄存器 | 未定义 |
| x1 | ra | 返回地址 | 否 |
| x2 | sp | 栈指针 | 是 |
| x3 | gp | 全局指针 | 未定义 |
| x4 | tp | 线程指针 | 未定义 |
| x5 | t0 | 临时寄存器 | 否 |
| x6 | t1 | 临时寄存器 | 否 |
| x7 | t2 | 临时寄存器 | 否 |
| x8 | s0/fp | 该寄存器需要被调函数予以保存,或也可用作调用栈的帧指针 | 是 |
| x9 | s1 | 该寄存器需要被调函数予以保存 | 是 |
| x10~x11 | a0~a1 | 函数参数或返回值 | 否 |
| x12~x17 | a2~a7 | 函数参数 | 否 |
| x18~x27 | s2~s11 | 该寄存器需要被调函数予以保存 | 是 |
| x28~x31 | t3~t6 | 临时寄存器 | 否 |
除32个通用寄存器外,RSIC-V还定义一个PC寄存器,该寄存器指向当前正在执行的指令的内存地址。
RISC-V汇编指令分类主要有以下几种:算术运算指令、逻辑运算指令、位移运算指令、内存读写指令、分支与跳转指令。如下图所示,汇编语言的操作与指令是一一对应的,汇编程序可以很简单快速的翻译成机器指令。

在基本ISA中,有四种核心指令格式(R/I/S/U),如下图所示。所有的指令都是固定 32位长度的,并且在存储器中必须在4字节边界对齐。当发生一个条件分支或者无条件转移 而且目标地址不是对齐到4字节时,将会产生一个指令地址不对齐的异常。如果条件分支没 有发生(not taken),那么将不会产生一个取指不对齐异常。
下面我们对常用指令进行介绍。
以上运算发生溢出时会自动截断高位,将低32位写入寄存器中。
左移会在右边补0,逻辑右移会在最高位添0,算数右移在最高位添加符号位。
区分算数右移和逻辑右移,是从计算的角度考虑的:左移一位等于乘2,右移一位等于除2是算数的规律;无论正数负数,在右边补0都等于乘2;而负数进行逻辑右移的结果不等于除以2,需要用算数右移;而若只有算术右移,则无符号数的运算又会受影响。
现代计算机以字节(byte,1byte=8bits)为基本单位,而内存本身可被视作由byte组成的一维数组,地址从0开始。字(word)则是存取数据的另一个单位,在RISC-V中1word=4Bytes=32bits,在其他体系结构中可能会发生变化。
有符号数:
无符号数:
blt和bge也有无符号版本bltu和bgeu。
我们对下面的程序的汇编代码进行分析。
int g(int x)
{
return x + 3;
}
int f(int x)
{
return g(x);
}
int main(){
return f(8) + 1;
}
其汇编代码为
.file "hello.c"
.option nopic
.text
.align 1
.globl g
.type g, @function
main:
addi sp,sp,-16 //栈指针-16,构造栈空间
sd ra,8(sp) //把ra放到mem(sp + 8)中
sd s0,0(sp) //把s0放到mem(sp)中
//以上两步保存寄存器的值
addi s0,sp,16 //把sp+16放到s0中,s0保存的是栈底指针
li a0,8 //a0 = 8
call f //调用f
mv a5,a0 //a5 = a0,a0保存的是调用函数的返回值
addiw a5,a5,1 //a5 = a5 + 1
sext.w a5,a5
mv a0,a5 //a0 = a5,a0保存返回值
ld ra,8(sp) //ra = mem(sp + 8)
ld s0,0(sp) //s0 = mem(sp)
//以上两步恢复寄存器的值
addi sp,sp,16 //sp = sp + 16,释放栈空间
jr ra
.size main, .-main
.ident "GCC: (GNU) 10.2.0"
f:
addi sp,sp,-32 //栈指针-32,构造栈空间
sd ra,24(sp) //把ra放到mem(sp + 24)中
sd s0,16(sp) //把s0放到mem(sp + 16)中
addi s0,sp,32 //把sp+16放到s0中,s0保存的是栈底指针
mv a5,a0 //a0 = a5
sw a5,-20(s0) //mem(s0 - 20) = a5
lw a5,-20(s0) //a5 = mem(s0 - 20)
mv a0,a5 //a5 = a0
call g //调用g
mv a5,a0 //a5 = a0
mv a0,a5 //a0 = a5
//以上几行除了调用g之外在做无用功,猜测可能程序太简单了😂
ld ra,24(sp) //ra = mem(sp + 24)
ld s0,16(sp) //s0 = mem(sp + 16),返回现场
addi sp,sp,32 //sp = sp + 32,退栈
jr ra
.size f, .-f
.align 1
.globl main
.type main, @function
g:
addi sp,sp,-32 //栈指针-32,构造栈空间
sd s0,24(sp) //把s0放到mem(sp + 24)中
//因为函数g没有调用函数,可能因此就没有保存栈指针
addi s0,sp,32 //新的栈底
mv a5,a0 //又是一串无用功
sw a5,-20(s0)
lw a5,-20(s0)
addiw a5,a5,3 //参数从a0最后传到a5,a5 = a5 + 3
sext.w a5,a5
mv a0,a5 //返回结果放到a0
ld s0,24(sp)
addi sp,sp,32 //恢复现场
jr ra
.size g, .-g
.align 1
.globl f
.type f, @function
由上面的程序和汇编代码,我们可以猜测出RSIC-V的函数调用栈的框架。
call指令是一条伪指令,由auipc和jlar两条指令组成,作用就是调用子过程。

https://zhuanlan.zhihu.com/p/496767749
https://blog.csdn.net/Cerman/article/details/122020984
https://whycan.com/files/members/7090/riscv-spec-v2.1%E4%B8%AD%E6%96%87%E7%89%88.pdf
https://blog.csdn.net/qq_41976613/article/details/90550181
331