CPU构成 & 功能
组成计算机的五个经典部件是:输入、输出、存储器、运算器和控制器(合并一块儿称CPU)。
CPU的组成部件为:
- 运算器:进行数据运算,最处的运算器只有ALU
- 指令控制器:指令解析,控制运算器运算,数据传输等。
- 寄存器:分为通用寄存器和状态寄存器,状态寄存器用于保存计算机的目前状态,通用寄存器可以供正在执行的程序自行分配使用。
- cache:为了缓解DRAM内存跟不上CPU速度而添加的缓存SRAM,现代CPU中的cache一般都是多层的,分为L1 cache,L2 cache等。
CPU和操作系统之间的这个“接口”被称为指令系统体系结构:也叫体系结构,是低层次软件和硬件之间的抽象接口,包含编写正确运行的机器语言程序所需要的全部信息,包括指令、寄存器、存储器访问和I/O等。常见的体系结构分为复杂指令集CISC和精简指令集RISC两个流派,CISC主要代表为x86,RISC主要代表为RISC-V,MIPS,Arm。
RISC-V指令集架构
RISC-V是一种开源硬件指令集架构,目标是成为通用指令集架构,即:支持从最小的嵌入式设备到高性能计算处理器的所有处理器。
过去传统ISA的设计方式为增量ISA,即在已有的ISA的基础上增加新的指令,新处理器不仅需要支持新的ISA扩展,还需要支持过去的所有扩展,他们是紧密联系的,目的是为了保持向后的二进制兼容性。这样的问题可想而知,ISA越来越复杂。
RISC-V的设计方式是模块化ISA,将核心指令和扩展指令分成模块,核心是RVxxI的基础ISA(xx是指系统位数32,64等,I是指整数),是固定不变的。具体的指令集名称是把代表扩展的字母附加到指令集名称之后作为指示。例如RV32IMFD,RV32I是基础ISA,M是指RV32M乘法扩展,F是指RV32F浮点数扩展,D是指RV32D双精度浮点数扩展。
所以在官方把RISC-V定位为ISA原料库,人们可以根据自己的需要搭建出自己的ISA,官方文档里原句:
Although it is convenient to speak of the RISC-V ISA, RISC-V is actually a family of related ISAs, of
which there are currently four base ISAs
对于基础指令集,还有RVxxE系列(Reduced Integer)针对嵌入式系统使用的,将通用寄存器数量降低到了16,RVxxI中是32个通用寄存器
官方文档对于通用组合G的定义:
we define a combination of a base ISA (RV32I or RV64I) plus selected standard extensions
(IMAFD, Zicsr, Zifencei) as a “general-purpose” ISA, and we use the abbreviation G for the
IMAFDZicsr_Zifencei combination of instruction-set extensions.
模式 & 指令
RV支持多种特权组合模式,如下图所示的三种:
1、直接裸机运行。ABI是指application binary interface,AEE是指application execution environment。
2、操作系统运行,操作系统担负起AEE的工作,以及应用调度,IO调度等工作。这时的AEE就是操作系统中安装的各种runtime。SBI是指supervisor binary interface,SEE是指 supervisor execution environment,supervisor是指操作系统的核心态。SBI是固定的接口,SEE可以是bootloader和BIOS中的程序,也可以是虚拟机。
3、“元操作系统”【实在不知道翻译成什么好】,其实就是又添加了一层,HBI和HEE的意思根据上面俩应该也能猜出来。Hypervisor分为两种,一种是直接和内核混在一起,自己就既面向硬件,又面向上面的OS,这种的代表就是KVM,另一种是运行在操作系统上,自己向下面向OS,向上面向自己管理的OS,代表就是QEMU。也就是说,下图的HEE既可以是操作系统提供的,也可以是硬件提供的。但是不管如何,总归是要有一层是直面硬件的,总不能一直OS套OS无限套娃。
黑底白字是抽象接口,并不是真实的实例。白底黑字是真实存在的实例,担负相应的工作。就像AEE,就是提供应用运行所需要的二进制静态库/动态库(只有一个应用,应该也不需要动态库),
RV提供了三种特权模式:
U:User模式
S:Supervisor模式
M:Machine模式
其中,RV强制要求实现M模式。
RV32I 指令集有47条指令,能够满足现代操作系统运行的基本要求,47条指令按照功能可以分为如下几类。
1)整数运算指令:实现算术、逻辑、比较等运算。
2)分支转移指令:实现条件转移、无条件转移等运算,并且没有延迟槽。
3)加载存储指令:实现字节、半字、字的加载、存储操作,采用的都是寄存器相对寻址方式。
4)控制与状态寄存器访问指令:实现对系统控制与状态寄存器的原子读-写、原子读-修改、原子读-清零等操作。
5)系统调用指令:实现系统调用、调试等功能。
RISC-V的指令分为四个格式:R/I/S/U,都是32位的,指令需要进行四字节对齐,否则运行时会跳异常。当添加的扩展中有16位的指令时,对齐要求放宽到两字节对齐。
RISC-V的指令中的目标地址rd,源地址rs1,rs2是始终固定的,这样更方便解码。imm立即数始终在最高位部分而且是有符号的且符号在31位。
如下图所示,不管那种格式的指令,其rs,rd位置不变。
官方文档里还提到了B格式和J格式,这两种是S型和U型格式的变种,是为了“最大化与其他格式指令的重合程度”。
感兴趣可以自己研究。
寄存器
RV32I的通用寄存器:
寄存器 | 编程接口名称 (ABI) | 描述 | 使用 |
---|---|---|---|
x0 | zero | Hard-wired zero | 硬件零 |
x1 | ra | Return address | 常用于保存(函数的)返回地址 |
x2 | sp | Stack pointer | 栈顶指针 |
x3 | gp | Global pointer | — |
x4 | tp | Thread pointer | — |
x5-7 | t0-2 | Temporary | 临时寄存器 |
x8 | s0/fp | Saved Register/ Frame pointer | (函数调用时)保存的寄存器和栈顶指针 |
x9 | s1 | Saved register | (函数调用时)保存的寄存器 |
x10-11 | a0-1 | Function argument/ return value | (函数调用时)的参数/函数的返回值 |
x12-17 | a2-7 | Function argument | (函数调用时)的参数 |
x18-27 | s2-11 | Saved register | (函数调用时)保存的寄存器 |
x28-31 | t3-6 | Temporary | 临时寄存器 |
除了上面的32个,还有一个PC寄存器,用于存储pc指针,也就是目前欲执行代码地址。
如果不加使用特权模式架构的话,就上面这33个寄存器,还有一些timer之类用的寄存器不过目前已经不是强制实现部分。
这个“特权模式架构是我胡乱翻译的”,我也不知道怎么翻译。原文是:privileged architecture
如果使用特权模式架构,RV为每一个特权模式实现了一套CSRs【Control and Status Registers,控制状态寄存器】:
根据文档上写的,最多有4096个CSR寄存器,大部分的CSRs都是供特权等级架构使用的,但是也有一些是非特权等级可以使用的,官方文档的描述:
While CSRs are primarily used by the privileged architecture, there are several uses in
unprivileged code including for counters and timers, and for floating-point status.
The counters and timers are no longer considered mandatory parts of the standard base
ISAs, and so the CSR instructions required to access them have been moved out of Chapter
2 into this separate chapter.
具体都是什么CSRs就不展开描述了,用到什么学什么吧,反正今天用不到。
最后,如果加入新的扩展ISA模块,也有可能会引进新的寄存器组,就比如添加了浮点数扩展,就会引入浮点数通用寄存器。具体可以看手册中的要求。
模拟CPU
我们要模拟一个最基础版本的RISC-V架构的CPU,只支持ADD一条指令。
ADD有两个,一个是I格式的,立即数+寄存器rs1中的数,还有一个是R格式的,rs1+rs2的数。
我们先实现虚拟的CPU,只包括通用寄存器,ALU单元,内存,三个部分【虽然内存不应该在CPU中,但是第一版嘛,能跑就行】
cpu.rs文件内容:
#[allow(dead_code)]
pub mod cpu{
pub struct ALU {
//算术逻辑单元
}
impl ALU {
pub fn new() -> ALU {
ALU {}
}
pub fn add(&self, a: u32, b: u32) -> u32 {
a.wrapping_add(b)
}
pub fn sub(&self, a: u32, b: u32) -> u32 {
a.wrapping_sub(b)
}
}
pub struct RegisterFile {
//寄存器文件
registers: [u32; 32],
}
impl RegisterFile {
pub fn new() -> RegisterFile {
RegisterFile { registers: [0; 32] }
}
pub fn read(&self, register: u32) -> u32 {
self.registers[register as usize]
}
pub fn write(&mut self, register: u32, value: u32) {
if register != 0 {
self.registers[register as usize] = value;
}
}
pub fn debug(&self) {
for i in 0..32 {
println!("x{:02}: {:08X}", i, self.registers[i]);
}
}
}
pub struct Memory {
//内存
data: [u8; 4096],
}
impl Memory {
pub fn new() -> Memory {
Memory { data: [0; 4096] }
}
pub fn read(&self, address: u32) -> u8 {
self.data[address as usize]
}
pub fn write(&mut self, address: u32, value: u8) {
self.data[address as usize] = value;
}
}
pub struct CPU {
zero:[u8; 1],
//0寄存器,用于存储0
pub registers: RegisterFile,
//32个通用寄存器,RISC-V使用小端序,小端序是指低位字节存放在低地址,高位字节存放在高地址,字节内部的位的顺序与大小端序无关
pub memory: Memory,
//4KB内存,用于存储指令和数据
pub pc: u32,
//程序计数器,指向当前指令
pub alu: ALU,
}
impl CPU {
pub fn new() -> CPU {
CPU {
zero: [0; 1],
registers: RegisterFile { registers: [0; 32] },
memory: Memory::new(),
pc: 0x200, //RISC-V默认初始化地址我也不知道,我就随便写了一个
alu: ALU::new(),
}
}
pub fn read_instruction(&mut self) -> u32 {
let pc = self.pc;
let byte1 = self.memory.read(pc) as u32;
let byte2 = self.memory.read(pc + 1) as u32;
let byte3 = self.memory.read(pc + 2) as u32;
let byte4 = self.memory.read(pc + 3) as u32;
self.pc += 4;
byte4 << 24 | byte3 << 16 | byte2 << 8 | byte1
//小端序,将4个字节拼接成一个32位整数
}
pub fn run(&mut self,step:bool) {
loop {
let instruction = self.read_instruction();
let opcode = instruction & 0x7F;
match opcode {
0x33 => {
let rs1 = (instruction >> 15) & 0x1F;
let rs2 = (instruction >> 20) & 0x1F;
let rd = (instruction >> 7) & 0x1F;
let funct7 = (instruction >> 25) & 0x7F;
match funct7 {
0x00 => { //add
println!("add");
let result = self.alu.add(self.registers.read(rs1), self.registers.read(rs2));
self.registers.write(rd, result);
}
0x20 => { //sub
println!("sub");
let result = self.alu.sub(self.registers.read(rs1), self.registers.read(rs2));
self.registers.write(rd, result);
}
_ => {
println!("Unknown funct7: {:02X}", funct7);
break;
}
}
}
0x13 => {
println!("addi");
let rs1 = (instruction >> 15) & 0x1F;
let rd = (instruction >> 7) & 0x1F;
let imm = ((instruction >> 20) & 0xFFF) | ((instruction >> 31) << 11);
let result = self.alu.add(self.registers.read(rs1), imm);
println!("rs1:{},rd:{},imm:{},result:{}",rs1,rd,imm,result);
self.registers.write(rd, result);
}
_ => {
println!("Unknown opcode: {:02X}", opcode);
break;
}
}
if step{
break;
}
}
}
pub fn debug(&self) -> (){
self.registers.debug();
}
}
}
main.rs文件
pub mod cpu;
fn main() {
println!("Hello, world!");
let mut cpu = cpu::cpu::CPU::new();
let instruction:u32 = 0b00010000000000000000000010010011;
cpu.memory.write(0x200, (instruction & 0xFF) as u8);
cpu.memory.write(0x201, ((instruction >> 8) & 0xFF) as u8);
cpu.memory.write(0x202, ((instruction >> 16) & 0xFF) as u8);
cpu.memory.write(0x203, ((instruction >> 24) & 0xFF) as u8);
cpu.run(true);
}
相关代码已经上传到GitHub:链接
欢迎star,后续会不断更新该系列并完善该项目。
如果感觉对您有帮助,欢迎点赞收藏+关注,Thanks!