目标:展示一个遵循硬件简单性的指令系统,展示其硬件表示及与高级语言的关系。
参考书籍:《计算机组成与设计:软/硬件接口 RISC-V》(原书第2版)
操作数
处理器的功能就是处理数据,而数据的来源主要是寄存器和内存。
寄存器:RISC-V中有32个寄存器,使处理器可以快速定位数据,进行算术运算。
内存:供数据传输、指令访问,同时保存寄存器中无法保存的数据结构(数组、链表……)
算术运算的操作数必须由寄存器提供,而寄存器很有可能数量不够或无法存储一些数据结构
因此,内存需要通过数据传输指令向寄存器传输数据
lw x9, 8(x22) // 将内存中取出的数据放到x9寄存器中
// 取数据的地址 = x22寄存器中存储的地址 + 8
// x9是要放入的寄存器,8是偏移量,x22是基址寄存器
sw x5, 40(x6) // 将x5中的数据放到内存(存放地址 = x6 + 40)中
我们由这两条指令发现,指令均为3个操作数【偏移量,基址寄存器,目标寄存器】
这是因为处理固定的操作数会降低硬件的复杂程度。原则1:简单源于规整
寄存器的数量为什么是32呢?
和程序变量相比,寄存器肯定是有限的。而过多的寄存器会导致时钟周期变长,运行速度变慢。而过少的寄存器又无法保存数据。经过权衡,得出32这个值。原则2:更少则更快
为什么算术指令的操作数由寄存器提供呢?
-
访问寄存器的时间比访问内存短
-
算术指令1次即可读取两个寄存器,而数据传输指令中1次只能传一个操作数(寄存器吞吐率高)
几乎所有的体系结构都是按照字节来寻址的,RISC-V使用的是32位电脑,即1字 = 4字节 = 32位 所以内存中连续字地址相差4
通过上图,我们可以发现按字取值时,字节中的值并不重要。比如我们取0字地址中的数据,只要取出的数据是1就可以,而0字节、1字节、2字节、3字节地址中的值并不值得我们关注。
事实上,每个字内存中如何存储值分为两个模式:大端模式和小端模式
假设现有16进制数0x1234
大端模式下,内存是按如下格式存储的:
地址 | 值 |
---|---|
1 | 0x34 |
0 | 0x12 |
小端模式:
地址 | 值 |
---|---|
1 | 0x12 |
0 | 0x34 |
RISC-V属于小端模式,按字取时一个字中字节存储顺序并不重要,除非访问单个字节。
立即数操作
addi x22, x22, 4 // x22寄存器的值 = x22寄存器的值 + 4
使用立即数从而避免从内存中取值。
x0寄存器中硬接线至0,因此总是为0
因为0在指令系统中经常用到,设置寄存器始终为0是加速经常性事件思想的体现
符号扩展:假设寄存器中的位数大于要存的二进制数,那么按照符号位填充二进制数的左端
而对于无符号数而言,二进制数左侧填充0即可
lbu x6, 30(x22) // 无符号数载入
指令表示
RISC-V中指令位数均为32位。
如上图所示
R型用于两个源操作数寄存器、目的操作数寄存器完整的指令(add)
I型用于一个源操作数寄存器的指令(lw)
S型用于没有目的操作数寄存器的指令(sw)
可以看到,即使指令类型不同,相同功能的模块仍然处于相同位置,S型甚至将立即数分成两部分,前7位保存立即数的前7位值,后5位保存立即数的后5位值。简单源于规整
计算机的两个关键原则
-
指令由数字表示
-
程序和数据一样保存在存储器中进行读写
计算器:输入数据,人来操作,得出结果
计算机:输入数据和程序,运行程序,处理数据,得出结果
程序的执行是计算机自动处理数据的关键所在。
逻辑操作
目的:用于简化打包和拆包。
sll x5, x6, x7 // x5 = x6 << x7 逻辑左移
srl x5, x6, x7 // x5 = x6 >> x7 逻辑右移
and x5, x6, x7 // x5 = x6 & x7 按位与
or x5, x6, x7 // x5 = x6 | x7 按位或
xor x5, x6, x7 // x5 = x6 ^ x7 按位异或
由于NOT只有一个操作数,因此RISC-V中用XOR异或来代替(3个操作数=>简单源于规整)
AND操作:只取需要的位上的值,其余位置均为0
用于决策的指令
beq rs1, rs2, L1 // if rs1 == rs2, go to L1
bne rs1, rs2, L2 // if rs1 != rs2, go to L2
blt rs1, rs2, L3 // if rs1 < rs2, go to L3
bge rs1, rs2, L4 // if rs1 >= rs2, go to L4
L1代表语句前的显示标签
L1: sub x19, x20, x21
边界检查
当检查arr[i]中的i是否越界时,一般采用0 ≤ i < arr.length
假设arr.length放在x11中,i放在x20中,指令中可以用:
bgeu x20, x11, IndexOutOfBounds
若i为负数,则在无符号的情况必然大于arr.length,因为负数补码第1位为1;若i超出范围,也会大于arr.length
计算机硬件对函数的支持
-
放置参数到寄存器中
-
将控制权交给函数
-
获取函数所需的资源
-
执行任务
-
放置返回值到寄存器中
-
将控制权交给初始点
RISC-V将x10 ~ x17用于传递参数和返回值;x1作为返回地址寄存器,用于返回初始点
jal x1, ProcedureAddress // 跳转至ProcedureAddress,x1 = PC + 4即x1保存初始点的下一行函数地址
// 用于调用函数
jalr x0, 0(x1) // 跳转至初始点的下一行函数,因为x0常为0,丢弃PC + 4
// 函数使用完毕后,返回原过程
用栈来存储数值
-
函数需要超过8个参数寄存器时
-
换出寄存器中的值到内存中
-
以栈的形式存储
-
运算结束
-
弹出栈的值到寄存器
-
返回调用点
x2寄存器:栈指针寄存器,又称sp
翻译程序
// 编译以下函数,g放在x10, h放在x11, i放在x12, j放在x13:
int leaf-example(int g, int h, int i, int j) {
int f;
f = (g + h) - (i + j);
return f;
}
addi sp, sp, -12 // 压栈
sw x5, 8(sp)
sw x6, 4(sp)
sw x20, 0(sp)
add x5, x10, x11 // 计算f
add x6, x12, x13
sub x20, x5, x6
addi x10, x20, 0 // 保留返回值
lw x20, 0(sp) // 弹栈
lw x6, 4(sp)
lw x5, 8(sp)
addi sp, sp, 12
jalr x0, 0(x1) // 返回初始点