【打造自己的虚拟机】编程模型
学习《操作系统概念》这本书很久了,但其实写一个CPU一直是我的梦想,在学硬件电路设计的时候,我们的课程设计就是设计一个简易的CPU,8条指令。而我自己也用Verilog设计一个简单的CPU,29条指令。但是感觉要想深入了解计算机,还是要知道当今CPU所提供的编程模型。所以我准备写一个虚拟机,来看看这个CPU到底是怎么和外界合作运行的。
在编写这个CPU模型的时候我主要参考了三本书。
《操作系统概念》
《链接与加载》
《计算机组成与设计-硬件/软件接口》
由于hennessy的影响,我在设计指令的时候参照一下几点:
然后我自己考虑了几点:
在我们宏观观察一个CPU的运行流程的时候,我们看到一个CPU和一块内存,当CPU启动或者重启的时候,它从某一个固定地址取出一条指令,然后翻译它,然后执行。所以我们的CPU采用简易的取值-执行模型,对虚拟机来说,这是足够的。
然后再微观一些我们看到,我们在设计一套指令的时候实际上要考虑下面几个东西。
l 程序跳转
l 程序调用
l 中断和异常
l 进程切换
为了支持现代的CPU的运行,我们支持进程切换,当然支持虚拟内存的模型。我们首先考虑程序的跳转,程序跳转的标准说法是分支,所以分为条件分支和无条分支。
条件分支采用小于转移,大于转移,无符号小于转移,无符号大于转移,等于转移,不等于转移,我认为这六条指令已经足够,所以没有提供小于等于转移和大于等于转移。
无条件分支采用简单的寄存器跳转指令,抛弃了立即数跳转,只是因为我们用软件模拟,不必要考虑性能,而这样以来,编程人员的压力大大减轻。所以一个无条件分支指令看起来是这个样子。
LOAD $S0, 32位立即数。
JUMP $S0
LOAD指令看起来已经超过了4字节也就是32位大小,因而不能放到一条指令里,所以LOAD指令实际是伪指令,它由两条指令组成:lui和roi,以后会讨论。
最重要的是,我们知道,当程序分支的时候,我们仅仅修改的是PC指,不进行堆栈保存。
程序调用,程序调用有两条指令,一条是call,一条是ret,现在我们来想一想当程序调用时保存了什么,仅仅保存返回地址。对了,就是这样。
无论任何时候我们不考虑一个调用是否是叶调用,因为我们不考虑性能。调用call的指令形式和JUMP类似,我在设计的时候就决定了这是一个全局的跳转,所以我们不再关心局部跳转和远程跳转,那是intel的事。
我们当然知道中断的形式和异常是怎么回事,在考虑中断和异常的时候,我们要考虑两件事,一是提供哪些中断和异常,二是中断调用的形式。中断向量号支持0-255,其中0-15留着系统使用,但是并不是每个都被使用了。
0 |
除以0故障 |
默认终止执行。 |
1 |
单步调试 |
断点 |
2 |
不可屏蔽中断 |
|
3 |
设置断点 |
|
4 |
缺页异常 |
|
中断向量表的基地址放到一个寄存器里。中断向量放在标志位中,now。
进程切换使用专用的指令,TSS表基址存放在一个寄存器中,每个表项参照80386的结构。
综上所述,我们需要一个PC寄存器,一个中断表基址寄存器,一个任务表基址寄存器,一个程序状态寄存器。
第一我不想设计什么专用的指令用来加载中断表基址或者任务表基址。
第二我完全想不通模拟的CPU要什么状态寄存器,所以设计如下。
【0】zero寄存器,任何时候读取都是0,任何时候写它都无效,任何时候用它间接寻址产生错误。 |
【1】中断表基址寄存器 |
【2】任务表基址寄存器 |
【3】程序状态寄存器,只放置三个状态,TF,中断许可,和中断发生,高八位存放中断发生时中断向量,再八位存放优先级数。 |
【4】bp 帧寄存器 |
【5】sp 堆栈指针寄存器 |
【6】PC |
【7】页目录基址寄存器 |
【8】缺页地址寄存器 |
【9-31】剩下的寄存器在仅仅设计汇编程序的时候,应该是不需要约定的,也就是从$0-$22全部是通用寄存器。 |
另有4个double型浮点数寄存器。
在我们刚刚设计一个新的CPU的时候,毫无疑问,什么都要从新的开始。我们要给它一些东西。
l 一个屏幕
l 一块内存
l 一个键盘
l 一个交叉编译器
l 一个程序下载器
屏幕的缓冲区映射到内存的0xb8000处(为什么?)采用80X25模型,每行80个,共25行。所以需要80字节X25=2000字节。最终为b8d70-1。
键盘的缓冲区映射到内存的0xb8d70处的16字节。接外部中断16。