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而部分用到了jr即jump 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
| Bits | 31-25 | 24-20 | 19-15 | 14-12 | 11-7 | 6-0 |
|---|---|---|---|---|---|---|
| Field | Imm[11:5] | rs2 | rs1 | funct3 | imm[4:0] | opcode |
| Width | 7 | 5 | 5 | 3 | 5 | 7 |