从零开始打造我的计算机系统
处理器设计
某种意义上而言如今的CPU模型的设计是为了更方便的设计操作系统,比如操作系统的安全是内核空间和用户空间的概念出现,进程模型使TSS模型出现,虚拟内存使MMU出现等等,如今我们要设计一个CPU的时候,实际上是考虑两点:
一:它是否能很好的实现C语言。
二:它是否能很好的实现一个操作系统。
只要这两点满足,它已经成为一个可以用的CPU模型。
在我想从零开始设计我的计算机以来,我就设计了几种CPU模型,当然,最开始选择的是支持虚拟内存的CPU,并且支持一些系统指令,然后我发现高估了自己的编程能力,所以决定从最简单实模式CPU做起。仍然保持它的可扩充性,以便以后升级。
然而仍然保留原始的几个要求:
1.不考虑兼容性。
2.不考虑性能。
3.不考虑移植。
4.仅仅逻辑上可行。
仍按照前面的想法,我将阐释一下几个关键点:分支,调用,中断和异常。
现在我们按照一个简单的计划进行:
单核实模式CPU
单核保护模式CPU
多核保护模式CPU
2.1关键点
2.1.1 分支
分支分为条件分支和无条件分支,在条件分支中,共支持一下几种条件,或许用C语言的方式更好表达些,那就是等于跳转,不等于跳转,小于跳转,大于跳转,小于等于跳转,大于等于跳转。这些跳转模式,除了==和!=之外,都支持无符号数比较和有符号数比较。(注意:不是无符号数和有符号数比较。)
条件分支都是相对PC偏移的。
无条件分支采用简单的寄存器跳转指令,抛弃了立即数跳转,只是因为我们用软件模拟,不必要考虑性能,而这样以来,编程人员的压力大大减轻。所以一个无条件分支指令看起来是这个样子。
LOAD $S0, 32位立即数。
JUMP $S0
LOAD指令看起来已经超过了4字节也就是32位大小,怎么实现它曾经在我的脑海里思索了好久,目前而言,有两种方法。
最重要的是,我们知道,当程序分支的时候,我们仅仅修改的是PC指,不进行堆栈保存。
附录:怎么设计一条加载32位立即数的指令?
最开始实现的时候,是把LOAD当作伪指令,由两条指令ORI以及LUI指令组成,ORI指令是或立即数,LUI是把一个十六位立即数加载到寄存器的高16位。
所以一条LOAD $R0,#HELLO会被汇编成以下两条指令。
ORI $R0,#HELLO_低16位。
LUI $R0,#HELLO_高16位。
这样看起来是解决了问题,但是怎么对这两条指令重载一直是我心中的疑问?
有两个问题:第一,LOAD指令不是仅仅能加载地址。
LOAD $R0,10000可能就是指10000而已,可以代表绝对地址,也可以不代表。
在苦苦思索好久之后,我认为汇编器是无法决定这样的一条指令是否引用了内存,除非根据上下文,但是那样又太复杂了。
于是我加上了一条汇编器规则:
规则 $0_0 引用内存地址必须使用标号,否则会产生一个运行时错误。
这样一来,LOAD $R0,#10000和LOAD $R0,label是两条截然不同意义的指令,汇编器知道第一个10000不需要重载,而label可能值也是10000,但是需要重载。
第一个问题解决之后,第二个问题迎面而来,怎么对一个32位的地址进行重载而不会引起错误。在这里假设HELLO的基地址变了,怎么跟着改变呢?
ORI $R0,#HELLO_低16位。
LUI $R0,#HELLO_高16位。
上下文信息已经丢失了,假如ORI和LUI都加上重定位常量的话,就出错了。【在设计过程中我一度抛弃了LOAD指令,决定直接使用JMP LABEL指令,这样的话整个LABEL是26位,又由于PC总是四字节对齐,所以其实寻址可以达到2^28次方,也就是256M的程序,我认为我永远也写不了那么大的程序,所以似乎是可以接受的,但是我后来又放弃了这个想法。】
解决方法如下,上面两条指令已经被标记为可以重载,而ORI会和重定位常量的低16位相加,而LUI会和重定位常量的高16位相加。
如上看来这是一个完美的解决方案。
还有另外一种方法,就是在整个32位的指令集里,设计一条64位的指令,无须细说你也想到了它的样子,LOAD $R0,#32位立即数里,LOAD 和$R0占用低内存地址的32位,而32位立即数占用高32位,看起来是这个样子:
目前采用第一种解决方案,也就是分散成两条指令。
2.1.2 调用
程序调用有两条指令,一条是call,一条是ret,现在我们来想一想当程序调用时保存了什么,仅仅保存返回地址到栈顶。对了,就是这样。
无论任何时候我们不考虑一个调用是否是叶调用,因为我们不考虑性能。调用call的指令形式和JUMP类似,比如call $0,我在设计的时候就决定了这是一个全局的跳转,所以我们不再关心局部跳转和远程跳转,那是Intel的事。
C语言调用时堆栈如图所示,为了更方便的实现C语言,我们添加SP寄存器和FP寄存器。
2.1.3 中断和异常
不管怎么说,以前我对中断和异常有误解,中断结束后,程序的流程应该回到原来的地方接着执行,然而异常却不是这样,比如说一个除以0的异常,很明显,程序不应该继续下去。但是从另外一个角度来看,缺页异常,当异常结束后,很明显应该回到原来的地方继续执行。
这么说起来,中断其实是一种异常,而异常是一个更广泛的概念。
中断向量号支持0-255,其中0-31留着系统使用,但是并不是每个都被使用了。
中断向量号 |
中断功能 |
解释 |
0 |
除以0异常 |
默认停止程序 |
1 |
单步调试 |
Flag置位 |
2 |
不可屏蔽中断 |
|
3 |
设置断点 |
|
中断可以嵌套吗?
答:当一个设备发生中断的时候,设备会发起一个我发生中断了的标志,在CPU指令执行结束后,CPU检测所有发生中断的设备,检测到优先级最高的那个,假如检测得到的优先级比正在执行中断子程序的中断优先级还高,或者没有中断,则执行中断过程,即保存状态寄存器和PC地址,复写中断向量到中断寄存器,假如检测到的中断优先级没有正在执行的中断程序的优先级高,则CPU简单的抛弃这些中断请求。
当设备发现中断没有被请求的时候,它们间隔一段时间再次发送中断。
当中断完成后,CPU把中断发生标志清零,代表可以发生中断了。
无论如何,一个32位的PSW足以保存这些东西了。
软中断总是可以嵌套的,假如一个中断子程序使用了一个低优先级的软中断,我们必须完成它,所以软中断可以嵌套。
2.2 寄存器布局
我们讨论一下寄存器布局吧,首先从数量来说,我们会设置32个32位的寄存器,编号从$0到$31,另外4个浮点型寄存器,编号从$32到$35(在我想不到更好的符号之前暂定)。
然后我们首先讨论那些重要的寄存器并进行分配编号。
重要的寄存器:PC,BP,SP,IDTR,PSW
编号 |
描述 |
0 |
zero寄存器,任何时候读取都是0,任何时候写它都无效,任何时候用它间接寻址产生错误。 |
1 |
中断表基址寄存器,允许你挪动表地址,不过暂时设为0 |
2 |
程序状态寄存器,目前只放置三个状态,TF(trace flag),中断许可,和中断发生,高八位存放中断发生时中断向量,再八位存放优先级数。 |
3 |
Sp栈顶寄存器 |
4 |
bp 帧寄存器 |
5 |
PC |
6-31 |
通用寄存器(随着以后的扩展,通用寄存器会越来越少。) |
2.3 指令集
应当无疑问,所有指令遵守opcode rs,rt,rd指令的格式,这是rs = rt (opcode) rd,我们遵守这一规定,就是目的寄存器在前。(请忽略rs,rd,rt原来的含义吧。)
一共有
l 7条整数运算指令
l 4条浮点运算指令
l 9条逻辑运算指令
l 11条分支指令
l 10条数据传输指令(只有他们和内存关联吧。)
l 7条系统指令
从我设置指令的格式来说,无论如何也不会超过64条吧?假如超过的话,又要改架构了。
opcode(6) |
5 |
5 |
5 |
11 |
|
|
CPU指令集 |
||||||
算术运算指令 |
||||||
整数运算指令(7) |
||||||
0 |
rs |
rt |
rd |
保留 |
add rs,rt,rd |
|
1 |
rs |
rt |
imm |
addi,rs,rt,imm |
||
10 |
rs |
rt |
rd |
保留 |
sub rs,rt,rd |
|
100 |
rs |
rt |
rd |
保留 |
mul rs,rt,rd |
|
101 |
rs |
rt |
rd |
保留 |
div rs,rt,rd |
|
110 |
rs |
rt |
rd |
保留 |
mod rs,rt,rd |
|
浮点数运算指令(4) |
||||||
111 |
rs |
rt |
rd |
保留 |
fadd rs,rt,rd |
|
1000 |
rs |
rt |
rd |
保留 |
fsub rs,rt,rd |
|
1001 |
rs |
rt |
rd |
保留 |
fmul rs,rt,rd |
|
1010 |
rs |
rt |
rd |
保留 |
fdiv rs,rt,rd |
|
逻辑运算指令(9) |
||||||
1011 |
rs |
rt |
rd |
保留 |
and rs,rt,rd |
|
1100 |
rs |
rt |
rd |
保留 |
or rs,rt,rd |
|
1101 |
rs |
rt |
保留 |
保留 |
not rs,rt |
|
1110 |
rs |
rt |
rd |
保留 |
xor rs,rt,rd |
|
1111 |
rs |
rt |
imm |
andi rs,rt,imm |
||
10000 |
rs |
rt |
imm |
ori rs,rt,imm |
||
10001 |
rs |
rt |
imm |
xori rs,rt,imm |
||
10010 |
rs |
rt |
shamt |
sll rs,rd,shamt |
||
10011 |
rs |
rt |
shamt |
slr rs,rd,shamt |
||
比较转移指令(11) |
||||||
10100 |
rs |
rt |
lable |
le rs,rt,lable |
||
10101 |
rs |
rt |
lable |
ga rs,rt,lable |
||
10110 |
rs |
rt |
lable |
nle rs,rt,lable |
||
10111 |
rs |
rt |
lable |
nga rs,rt,lable |
||
11000 |
rs |
rt |
lable |
leu rs,rt,lable |
||
11001 |
rs |
rt |
lable |
gau rs,rt,lable |
||
11010 |
rs |
rt |
lable |
nleu rs,rt,lable |
||
11011 |
rs |
rt |
lable |
ngau rs,rt,lable |
||
11100 |
rs |
rt |
lable |
eq rs,rt,lable |
||
11101 |
rs |
rt |
lable |
ueq rs,rt,lable |
||
11110 |
rs |
保留 |
jmp rs |
|
||
数据传输指令(10) |
||||||
11111 |
rs |
rt |
|
|
mov rs,rt |
|
100000 |
rs |
rt |
rd |
保留 |
lword rs,rt,rd |
|
100001 |
rs |
rt |
rd |
保留 |
sword rs,rt,rd |
|
100110 |
rs |
保留 |
imm |
lui rs,imm |
||
100111 |
f1 |
rt |
rd |
保留 |
ldoub $f1,$r2,$r3 |
|
101000 |
f1 |
rt |
rd |
保留 |
sdoub $f1,$r2,$r3 |
|
系统指令(7) |
||||||
101001 |
rs |
保留 |
call rs |
|
||
101010 |
保留 |
ret |
|
|||
101011 |
rs |
保留 |
push rs |
|
||
101100 |
rs |
保留 |
pop rs |
|
||
101101 |
保留 |
halt |
|
|||
101110 |
imm |
int imm |
|
|||
101111 |
保留 |
IRET |
|
2.4 系统架构
本系统,采用一下参数:
l 一个屏幕,用800x600的窗口,采用EGE娘的EGE库。
l 一块内存,内存暂定32MB。
l 一块硬盘,用文件作为模拟,64MB。
l 一个键盘
l 一个交叉编译器
l 一个程序加载器
屏幕的缓冲区映射到内存的0xb8000处(为什么?)采用80X25模型,每行80个,共25行。所以需要80字节X25=2000字节。最终为b8d70-1。
键盘的缓冲区映射到内存的0xb8d70处的16字节。接外部中断16。
那么,开始吧。