Post

RISC-V

RISC-V的学习记录

Assembly Instructions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
add rd, rs1, rs2	//将rs1+rs2的结果存储在rd这个寄存器中
sub rd, rs1, rs2	//将rs1-rs2的结果存储在rd这个寄存器中

addi rd, rs1, imm	//将rs1+imm的结果存储在rd中,这是特地为常量加法设计的语法,
			//而RISC-V中只有addi没有subi
			//因为本着reduced的简单指令,subi的效果可以通过addi达到故而没有

lw rd, imm(rs1)		//load word,一个字(word)的长度一般不太一样,在这里是32个bits,也即8bytes
			//从内存中加载值存在rd中,因为寄存器数量有限,只能暂存临时变量
			//且偏移量只能是4的倍数
lb rd, imm(rs1)		//load byte,偏移量都是byte,但这个可以访问字与字之间的byte
lbu rd, imm(rs1)	//将内存中的值作为无符号变量加载出来
sw rs2, imm(rs1)	//store word,将rs2的字存在偏移rs1地址imm个字节的地方
sb rs2, imm(rs1)	//store byts,与上一个一样,只不过存的是对应的一个字节

beq rs1, rs2, Label	//branch if equal,如果rs1和rs2相等,则跳转到Label标签所在
bne rs1, rs2, Label	//branch if not equal
bge rs1, rs2, Label	//branch if greater or equal
blt rs1, rs2, Label	//branch if less than
bgeu rs1, rs2, Label	//branch if greater or equal(unsigned)
bltu rs1, rs2, Label	//branch if less than(unsigned)
j Label			//jump to

//logical operations
and x5, x6, x7		//x5 = x6 & x7
andi x5, x6, 3		//x5 = x6 & 3

instructions are very lean in RISC-V, so we don't have unneccessary instructions
so we don't have not in RISC-V, it can be represented by XOR it with 111111(two)

slli x11, x12, 2	//shift left logical, x11 = x12 << 2
srli...

//pseudo-instructions 伪指令
//RISC-V中并没有太多的指令,但有很多的伪指令,其与普通指令的区别在于
//普通的指令是定义在ISA中的,能被CPU直接识别并执行,但伪指令只是汇编器提供的一种便利
//本质是由一或多条普通指令构成的
mv rd, rs		//addi rd, rs, 0(move)
li rd, 13		//addi rd, x0, 13(load immidiate)
nope			//addi x0, x0, 0(没有进行任何操作)

Assembly Directives

汇编语言中除了基本的指令和伪指令这些在运行时执行的语言外,还有一些初始化的汇编指示符,如常见的.text.data等,虽然这类指示符挺多,但大致分为.text段用于表示代码段,这里的代码都是实际运行的代码,以及.data段用于初始化一些东西,如n: .word 9就是为n开辟了一个字的空间并将其初始化为9

而这存储的地方与局部变量不一样,局部变量一般是存储在栈中,在最高的地址中向下增长,与之对应的较低地址处即是向上增长的堆空间,再往下就依次是.data段和.text段,至于硬件层面对应的什么期待后续

About Machine Program

汇编代码在被汇编程序转换成.o文件后,会和一些预先创建好的库的.o文件链接起来,最后生成机器代码可执行.out文件

这种machine code executable code位于哪里呢,因为这个文件不够小,所以自然不能存放在寄存器中,必须存放在内存memory中,内存中每个RISC-V指令都占据32bits,而这个存放项目的地址和数据的地址也是在内存中相互分开的

在处理器中的数据路径中有一个特殊的寄存器叫做程序计数器(proguame counter),它保存的是下一条要执行的指令的字节地址,由于内存中的指令都是字节可寻址的,所以通过这个可以找到内存中对应的指令

整个过程可以概括位处理器中的控制单元(control unit) 从内存中获取指令,并使用数据路径和内存系统来执行,再更新PC来获取下一条执行的指令地址,一般是4个字节后的下一个地址,虽然也有可能通过branch进行跳跃,因此我们要把PC增加4个字节,而如果是有了branch,则直接将新地址加载到PC中

RISC-V Function Calls

我们选择使用寄存器而不是使用内存,因为寄存器往往更快,对于寄存器中的几个有一些别称可以直接使用,如

1
2
3
4
5
6
7
8
9
ra    : x1		//return address register,存返回地址
sp    : x2		//stack pointer,栈指针
gp    : x3		//全局指针,用于访问全局变量
tp    : x4		//线程指针,用于线程局部存储
t0-t2 : x5-x7		//临时寄存器
s0-s1 : x8-x9		//保存寄存器
a0-a7 : x10-x17		//八个参数寄存器用来传参,其中a0和a1用来存返回值
s2-s11: x18-x27
t3-t6 : x28-x31

这里t寄存器表示临时变量,一般不用特意修改,而s寄存器如果在函数中需要调用或修改,则必须在一开始先存在栈上以便于后面变回之前的数据,但这只是软件层面的规定而已,二者实际上都是通用寄存器(但这样区分的目的是什么还没有明确的感受)

而RISV-V中的指令也是存在内存中的,每一条指令占据4个字节,下面模拟一个,其中的地址用十进制形式模拟

1
2
3
4
5
6
7
8
1000 mv a0, s0		//x = a
1004 mv a1, s1		//y = b
1008 addi ra, zero, 1016//ra = 1016
1012 j sum
1016 ...
...
2000 sum : add a0, a0, a1
2004 jr ra

可以发现部分代码用到了j而部分用到了jrjump register,这是因为j对应跳转的是立即数地址,而jr对应的是寄存器中存放的地址,而我们的2000-2004这两行代码可以算作一个函数,则必然会在代码段中被多次调用,所以不能指向一个固定的地址,而是每次使用的时候更改ra的值再调用函数以达到返回地址的目的

还有一些关于函数跳转的新指令

1
1008 jal sum		//jump and link

这会跳转到sum标签同时将这一地址的下一地址作为返回地址,即做了这样的事情

1
2
1008 addi ra, zero, 1016	//ra = 1016
1012 j sum 			//goto sum

相当于把ra的地址更新了,且占用更少的地址,这在汇编代码中用的很频繁

注意:j才是伪指令,而jal不是

1
jal rd, offset

这是jal命令的语法,rd是目标寄存器,也就是把当前指令的下一个指令的地址传入进去,也就是PC + 4,而offset是一个偏移量,是一个立即数,如果其值为-4,那么就是跳转到上一条指令所在位置,它同时也可以是一个标签,汇编器会计算出其真实的偏移量

进一步了解函数调用,在每次调用函数时,寄存器的个数还是有限,如果函数需要多个临时变量来存储,则必然会出现覆盖原来旧值的情况,所以我们就需要有一个地方用于存放这些旧值,以便调用完函数后的更新,这就是我们的栈stack

stack只是用来存数据的,所以我们会先将栈指针移动从而留出足够的空间来存放这些数据

1
2
3
4
5
6
7
8
9
10
11
12
addi sp, sp, -8
sw s1, 4(sp)
sw s0, 0(sp)		//这三行指令做的是调用序言,即函数调用中的第一步prologue

add s0, a0, a1
add s1, a2, a3
sub a0, s0, s1		//函数调用的第二步

lw s0, 0(sp)
lw s1, 4(sp)
addi sp, sp, 8		//函数调用的第三步,epilogue结语
jr ra

但是若嵌套调用的话,总会出现寄存器不够用的情况,当我们进行函数调用的时候该怎么存储旧的数据,又该怎么恢复他们呢

RISC-V Instruction Formats

比汇编语言再低一层的是机器语言,也就是0和1组成的语言,我们要做的就是将汇编语言改成机器语言

RISC-V中每个指令占据32bits的宽度,而每个指令对应的对象和操作不一样,所以会用二进制编码来分类,RISC-V中大致可以分为

1
2
3
4
5
6
R-format for register-register arithmetic operations
I-format for register-immediate arithmetic operations and loads
S-format for stores
B-format for branches
U-format for 20-bit upper immediate instructions
J-format for jumps

这里介绍如下

R-format

I-format

addi举例,它和前面add的区别在于把rs2换成了立即数,但是5个bits最多32个数的范围显然不够涵盖立即数,所以将func7也用于存储立即数,而load操作也是要求一个源寄存器、一个目标寄存器和一个立即数,所以load命令延用I-format就够了

S-format

Bits31-2524-2019-1514-1211-76-0
FieldImm[11:5]rs2rs1funct3imm[4:0]opcode
Width755357

Lab03

This post is licensed under CC BY 4.0 by the author.