home brew computer system

io和中断的设计定型,成了目前我的cpu设计过程中最亟待解决的问题。若不定型,可能影响接下来其他组件的设计和测试工作。

中断控制基本没啥可修改的,cpu端只有一个中断脚,cp0(协处理器0,即cu)中有四个寄存器,cause用于记录外部中断、内部中断和异常的种类;status中控制这一个中断的开关;base记录中断处理程序的地址,在中断处理程序中使用mfc0指令查询cause寄存器跳转到相应中断的处理程序地址;EPC用来保存中断前的指令地址。这一部分,我只需要额外实现三条指令mfc0,mtc0和eret。

io模块的输入输出部分应该是一个寄存器组,包括状态寄存器、指令寄存器、数据寄存器。状态寄存器标志忙闲,指令寄存器为从cpu获取指令,数据寄存器作为io控制器输入输出的通道。此外,io模块还要与cause寄存器相连,辅助一套中断屏蔽与优先级逻辑,完成中断向量。IO模块中的中断控制可以级联。

几乎所有人都不建议,在FPGA中使用多于一个时钟。在FPGA中使用不同的时钟,涉及到在不同的时钟域(clock domain)进行同步,数据的同步通常使用两个串联的触发器,flag(一个周期的信号)通常转换成电平转换,然后再到另一个域进行同步,详细的设计可以看这里

因此,我也决定在我的设计中撤销异步的清零、下边沿的清零。在需要使用不同速度的组件,如设计目标中的可以暂停的,可以调速的cpu,采用计数器激活的clock enabler,详细设计可以看这里。这样对我现有的设计的修改产生了一定的工作量。

现在lcc的代码已读完,下一步要做的是soc的设计和验证,以及lcc的移植、汇编器的移植。
对于汇编器,我准备使用customasm.
如果使用这个汇编器,可能在移植lcc时需要修改lcc模板中的一些伪指令如ld,lcomm等。

对于soc的设计,这些天一直在思考的问题是异步通讯的问题。我所希望的系统,cpu运行的速度应该是可变的,甚至可以是单步的。而我又希望能使用de0 nao板载的SDRAM,因此内存控制系统与cpu的通讯一定是异步的。异步通讯要有ready信号,由于我希望cpu运行的速度可变,因此我需要在cache控制器中设置一个双触发的触发器,在ready信号变化时记录并保持ready信号,直至cpu的下一个周期到来,用这个信号控制cpu中cache控制器和ram控制器的异步通讯。

系统地址空间的划分,也是一个需要确定的设计。目前我计划将地址空间按照| ROM | IO | RAM |划分,具体各区域尺寸尚未确定。系统cache只对RAM区域有效。目前我的想法是,根据指令和数据需求的不同,在cache控制器上增加片选、控制信号mux,以实现不同区域对cache的不同操作。对于指令,只需要读取ROM和RAM,所以I cache只需要在原有I cache上增加对ROM的多路复用(直通),由于这个ROM是用作bootloader的,因此不需要很大体积,可以使用同步的方式与I cache链接,因此I cache的改动很小。对于数据,则三个区域都需要读取。
显然,io与cpu之间的通讯也是异步的,但我尚未构思完毕整个io控制器的结构,因此io控制器的设计应该是接下来的工作之一。

IO控制器应该包括一些总线(I2C,SPI,etc),一些通讯端口(GPIO,Uart,etc),以及向量中断和异步通讯的支持。需要仔细考虑。

我目前已有cpu部分的Verilog代码,但是这部分代码缺乏测试,没有精确中断支持,没有与cache集成,而且一些指令还需要修改,例如:我打算删除乘法和除法的硬件支持,原因是原有的代码中对乘法和除法的硬件支持来自Altera的IP,这与本项目的设计目标冲突,而且在sparc平台的lcc后端中,有使用软件支持乘法除法的参考样例;我还需要考虑是否增加不同长度的取数操作,考虑如果在lcc移植过程中将所有整数类型的尺寸或者对齐都设置为4字节会怎样。

但是这些工作的顺序很难确定,这是我首先要考虑的问题。

目前我想到的合理的顺序是:

  • 考虑是否增加不同长度的取数操作
  • 回顾已有的cpu代码,分析那些代码可以与cache、IO、中断等独立,并对这些代码进行测试。
  • 定义存储控制和io控制的接口,与cpu对接,测试cpu的中断
  • 设计存储控制
  • 设计io控制
  • 工具链的移植

经过了几个月,终于完整的读完了lcc 3.6 的代码,这一阶段工作基本告一段落。对于我来说,lcc是一个十分复杂的软件,虽然我能感受到作者在解释其实现所用的极大努力,但理解起来仍然十分困难。但是当我完整的看完lcc的代码包括其后端后,得到的收获确实很大。通过阅读,基本可以确定对,于我来说,lcc的移植工作技术上是可行的。

LCC的结构

先谈一下lcc的结构。原书中的叙述方法,更多的是从功能模块的角度进行叙述的,详细的描述了每一个模块的实现,但是从纵深的角度来看,缺乏对模块之间相互调用相互配合的描述,因此需要阅读时特别留意整个编译程序运行过程中函数之间的调用,才能把握这个复杂的编译程序的整体脉络。

如果从传统的前端-后端角度去看这个编译器,很难去理解。我认为前端-后端这样的分法,实际上是从功能的逻辑层面去考虑的。虽然整个lcc确实在实现上区分了前端和后端,但是在运行过程中,前端和后端的代码始终在相互配合,难以在运行过程明确分离。在我尚未意识到这一点时,阅读代码基本是在关注某个模块的实现,比如语法分析。而这些模块如语法语法分析,实际上是运行过程中比较深得一层,但从这个角度,很难看到阅读编译器的代码如何带给人与仅上编译原理课程不同的好处,填补理论和实践之间的鸿沟。
因为这些一个一个的模块,尤其是语法分析,实际上与编译原理的课程内容很相似,事实上,编译原理课程与语法分析这部分的实现重叠很大。

从语法分析到语义分析,这部分内容就开始显现与计算机课程之间的区别了,编译器实现了ANSI C完整的语义,阅读这部分,更像是阅读C语言标准,这里面可以提炼出一些话题进行深入讨论,如C语言中的空指针定义,强制类型转换,函数调用,可变参数函数,参数的估值顺序,结构体中数据的对齐,C语言中复杂的声明等等。从编译器角度看这些问题,的确给人耳目一新的感觉,但是仅仅读到这里,仍然会有许多疑惑尚未被解释。例如函数调用、参数传递,这些都需要和计算平台配合,这些功能的具体实现,还要继续深入了解。我读到这里的时候,最大的疑惑就是程序中的数据是如何和各个标识符相关联的,内存的地址是如何和变量名相关联的。

继续深入下去,lcc的结构就渐渐浮出水面。按照c语言的文法,一个translation unit是c语言程序的基本处理单元,translation unit 包括声明,可以是变量或者是函数的声明,一个c语言文件可以包括若干translation unit。变量的声明,只是处理了标识符、类型、值在编译器内的内部表示。对于变量的初始化,全局变量不产生代码,而是直接将符号和值相互对应起来。只有进行函数定义的时候,才会生成代码。所以分析lcc的整体脉络,需要知道书中讨论的各种功能模块,实际上是以函数定义为单元,往复进行调用。所以分析应从函数定义开始,逐步深入。便可理解一个简单的c语言程序是如何被lcc处理的。

处理函数定义的函数funcdefn第一部分处理要定义的函数的类型,参数列表,处理局部变量和参数(主要是通过后端指定名字,将名字指定为相对于栈的offset),然后对函数定义的compound statement进行语法语义分析,一边分析,一边生成中间语言(森林形式的中间语言),生成中间语言的过程加以优化(如消除公共子式,常量折叠)。然后调用后端function函数,对中间语言进行处理。

function函数,首先根据funcdefn传递过来的参数和局部变量offset计算栈偏移,如果需要,则为生成复制参数的代码(如原来参数由寄存器传入,需要用另一个寄存器或放入栈中),为生成保存函数调用约定中要保存的寄存器的代码做准备,然后调用gencode,对中间语言进行处理。

gencode首先生成对参数进行复制的中间语言,然后根据中间代码森林中树的类型,调用相应的后端函数。Blockbeg对于compound statement中的局部变量以for循环的形式逐个调用local,分配寄存器变量或在栈中划分空间;Local,address节点专门处理临时生成的local变量。gen节点是重中之重,负责生成主要的代码。

gencode—>gen,处理树状的中间语言。这部分书中表13.1描述的较为清晰,prelabel处理已经确定了寄存器的节点的target,_label在树上用树文法与后端模板进行匹配,reduce选择最好的指令输出。prune删去一些不生成指令的子节点,linerize对指令进行排序,最后ralloc对需要寄存器的节点分配寄存器。

然后function 函数开始真正生成代码,首先先输出函数名作为label,作为函数的入口,然后计算framesize和sp,并将sp移动的指令输出(划分栈空间),然后输出保存寄存器的代码,接下来是移动参数的代码。这些工作做完,function调用emitcode函数,生成函数中的语句生成的代码(从已经被标记修减的中间语言树中,按模板生成汇编指令)。最后生成函数的出口,包括恢复保存的寄存器,栈弹出,跳转返回指令。

变量的标识符和值

我在阅读代码的时候,十分关注标识符和值是如何关联的。实际上lcc将于C语言的变量名,处理为在数据段中的一个标签。对于未初始化的全局变量,lcc将变量名标签放入bss段;对于初始化的全局变量,lcc根据情况将【变量名标签:值】对放入LIT(read only)或data段,实际的地址生成是汇编器的工作。对于局部变量,则全部表示为栈偏移的形式。这样做的原因是显而易见的:程序运行时的函数调用是动态的,很难确定局部变量的绝对地址,所以使用相对于栈的偏移来解决问题。不同于全局变量,局部变量的初始化是用生成的代码实现的。

对于一些特殊的变量,如数组、字符串的初始化,首先将常量的内容放入LIT段,然后生成一个临时变量保存这个常量内容的标签。

lcc中mips后端的帧结构

高地址
    --------------------------------
    | 调用者的帧                    |
    --------------------------------
    |调用者传递的参数               |
    --------------------------------        ——
    |局部变量                       |        |
    --------------------------------
    |被调用者保存的现场             |       Framesize
    -------------------------------
    |被调用者调用其他函数时传递的参数|         |
    -------------------------------         ——----->sp
    |另一个帧                       |
低地址        

这里值得一提的是,参数的偏移是相对于sp+framesize的正偏移,因此是从调用者的帧中获取参数的值(参数传递),而局部变量的偏移是相对于sp+framesize的负偏移。

另一处值得说明的是,被调用者如果调用了其他的函数,那么就需要在当前栈中开辟存放outgoing argument的空间,一个函数可以调用很多个其他的函数,这个空间如何确定?在生成函数的代码的过程中,对所有的子函数调用的参数进行统计,得到最大的outgoing argument数量,这里这个空间是这样确定的。

小结

了解了lcc的结构,理解了一个简单的c语言程序是如何由lcc一步一步生成汇编语言的,就可以进行移植工作了。

移植工作主要包括:修改指令模板,修改寄存器分配规则,修改函数调用规则,修改各个基本类型的长度和对齐,修改各种名称约定(如各数据段的名字)等。

对于我即将进行的工作,主要的修改应该包括修改类型的长度和对齐(由于cpu取数操作与mips不同),对于乘法除法指令模板应该为函数调用(cpu无乘法器、除法器,这里还需修改clobber函数),对于浮点操作,需要先用编译器编译生成计算函数,然后将指令模板修改为函数调用。修改个数据段名字与汇编器配合。

本项目所设计的CPU,与李亚民书中所描述的CPU主要区别是在MIPS CPU中引入了微代码,方便指令集的扩展、编译器的移植和系统编程。

带有微指令的IF级

原理图

由原理图可知,主要的修改是加入了:

  • 微指令存储器 uprog
  • 微指令寄存器 uPC
  • 下一个微指令选择器
  • IF控制器 IF Control
  • 状态寄存器 State

微指令存储器的结构

本平台使用的微指令有如下结构:

classDiagram
class MicroCode{
    head 1
    head 2
    ......
    head n
    code1 ()
    code2 ()
    coden ()
}

class Head{
    a JUMP instruction
    first op in the code section-delay slot
}

微指令存储器包括两个部分,头部按照指令号寻址,按顺序存放在存储器中。每个头部域有两行,第一行是一个跳转指令,跳转到对应的代码域,第二行是为了满足延时槽的特点,将代码域的第一行代码放置在延时槽中。

由于微指令的头部有两行,所以在使用指令中的指令号作为地址对微代码存储器寻址时,要先向左位移一位。

如此设计微指令存储器,可以方便寻址,并且最大限度地利用微指令存储器的空间,头部和代码之间没有分割。

微指令存储器的代码域由一个终结指令分割。这个终结指令供IF控制器判断一个微指令是否结束。代码域中可以使用任何一条用硬线逻辑实现的指令,一旦进入微指令状态,微指令中的跳转指令将只修改微指令计数器uPC。每个微指令代码域的最后一条指令(终止符前)不能是跳转指令。

微指令工作过程

在不考虑中断或异常的情况下,整个IF阶段实际上分为两个个状态:

  1. 正常状态
    • 该状态是指现在没有微指令在执行
  2. 伪指令状态
    • 该状态指现在正在执行微指令

与此同时,还要判断两个动作:

  1. 正常状态–>微指令状态(enter)
  2. 微指令状态–>正常状态(exit)

IF的状态由State Register记录,动作由state和正在输入的指令(INS_in, uINS_in)判断。

状态转换 State nState INS_in uINS_in wpc wupc enter out_sl
n->n 0 0 0 x 1 0 0 0
n->u 0 1 1 x 1 1 1 1
u->u 1 1 x 0 0 1 0 1
u->n 1 0 0 1 1 1 0 0
u->u 1 1 1 1 1 1 1 1

上表中,表头对应原理图中的信号。state=0表示正常状态,state=1表示进入微指令状态。nState表示下一个状态。INS_in=0 表示当前指令以硬件方法实现,INS_in=1 表示当前指令是以微指令方式实现。uINS_in=0表示当前微指令不是结束符,uINS_in=1表示当前微指令是结束符。wpc和wupc分别控制pc和upc的写入使能。

enter为进入微指令状态时的指示信号,为了提高ipc,一旦发现一条指令时使用微指令实现的,就应当直接输出对应的微指令,所以进入微指令状态需要额外处理:微指令存储器的地址直接由enter信号选择为指令中的地址位,微指令存储器输出head中对应的跳转指令,下个时钟周期到来时,该地址+4放入upc,微指令状态延时槽仍然保留。之后的upc由ID阶段的upcSource更新。

out_sl标志输出是微指令存储器内的内容还是指令cache中的内容。当状态发生转换时,n->u时,若将指令cache中的指令直接传入ID,ID是无法识别的,这个时候因为enter信号的作用,使得该指令对应的微指令已准备好,所以out_sl为1,即当微指令实现的指令出现时,直接传入其对应的微指令。同理,当u->n时,若将微指令结束符传入ID,ID也是无法识别的,这个时候急需要使out_sl=0选择输出下一条指令。这里还有一个特殊情况,就是当下一个输入的指令也是由微指令实现的,那么实际上IF阶段再一次进入了微指令状态(reenter),这时enter信号选择了新的微指令地址,向ID级输入正确的指令。

关于pc和upc的控制有些复杂,简单来说,就是消耗一条指令,就要写一次pc,消耗一条微指令就要写一次upc。所以可以看到当enter和reenter的情况发生时,等于说既消耗了一条指令,又消耗了一条微指令,所以在这种情况下pc和upc均被写入。

由于延时槽和流水线的存在,使得下一个pc和upc的值的判断变得复杂。而我的设计又希望尽可能地将修改停留在IF级,所以才把pc和upc地写入限制加在了IF级。由于ID级要计算跳转指令的目标地址,所以当前PC或者uPC要传入ID级。改进的IF级中,nPC和nuPC的两个多路复用器,除了+4这一项输入分别对应pc和upc自身外,其余三个输入是相同的,均由ID级直接给出,选择信号也是相同的,也由ID级直接给出。这样做的好处是对于ID级不需要做任何修改,就能实现在微指令状态中的跳转修改upc,而在正常状态下的跳转指令修改pc。考虑下面几种情况中pc和upc的变化。

存储器位置 情况一 情况二 情况三
01 硬件实现指令(非跳转) 跳转指令 硬件实现指令(非跳转)
02 微指令实现指令 微指令实现指令 微指令实现指令
03 硬件实现治指令 跳转指令的目标 微指令实现指令

情况一是比较正常的情况,pc=01,该指令是一个硬件实现指令,所以直接输出,这时候向下一级输出PC。下一个时钟到来,若当前指令是延时槽中的指令,pc等于其目标;若当前指令不处于延时槽中,则pc=pc+4。若跳转的目标是一条普通指令,这种情况比较正常,不涉及到状态切换,在这里不考虑。

当pc=pc+4 或者当前指令是延时槽中的指令,并且跳转的目标刚好是02,这时候要发生状态切换。此时01指令已在ID级,它不是跳转指令,所以pc_source被ID级设置成pc+4。而此时是enter的情况,所以upc被设置成指令中所知的位置,而upc_source和pc_source公用一个信号,所以upc_source也被设置成upc+4.当 下一个时钟周期到来时,由微指令存储器指出的微指令被送入ID,而pc和upc分别被写入为03和微指令头部延时槽的地址。并且IF级向ID级送入upc,供微指令中的跳转指令参考。

当微指令执行结束,也就是upc指向了一个终止符。这时上一条指令(ID级中的指令),由于一定不是跳转指令,所以会把pc_source设置成pc+4,准备向ID级输出pc所指的指令03。下一个时钟到来,upc+4,pc+4=04,而upc指向结束符的下一个指令,脱离结束符的状态。

情况二比较特殊,02位置的指令是一个延时槽。我们考虑这种情况下upc和pc如何变化。pc=01,该指令是一个硬件指令,所以直接输出,这时候向下一级输出pc,此时时钟到来,pc+4=02指向微指令实现的指令。由于ID级是跳转指令,所以ID级将pc_source设置成响应的跳转目标,这里是03。所以时钟到来后,pc=03,upc为指令中所指示的地址+4。可以看出,这种情况实际上和上一种情况是等价的,只是看起来比较特殊。在微指令执行结束后,exit情况也同上一种情况等价。

情况三考虑重进入这种情况。pc=01该指令是一个正常的指令,与情况一相同,在这就不详细分析了。考虑重进入的时刻,将02所代表的微指令送入ID时,pc已被设置为03,此时02中的微指令执行完毕,upc指向终止符,由于此时ID级一定不是跳转指令,因此pc_source被设置成pc+4,由于是重进入,当时钟到来时,IF会选出03所指的微指令中的跳转指令,并将upc设置为03中指令所指的位置+4(微指令头部的延时槽),同时会将pc设置为04.

由对上述三种情况的分析可以知道,我所设计的微指令系统时可以正常工作的。对这些情况的分析是多余的,是因为npc和nupc的值是由ID级的指令决定的。ID级是硬件指令,IF正在处理微指令,这种情况(enter)upc是特殊处理的,并不会影响pc的更新。同理ID级时微指令,IF级正在处理硬件指令,这种情况下upc和pc都直接加4,upc+4后跨越了终结符,其值在下一次进入时在被更新。其余的情况ID级时硬件指令,IF级也是硬件指令或者ID级是微指令,IF级也是微指令,这两种情况与未加入微指令系统的情况相同,只是选择性的对pc和upc的写入进行使能控制。

微指令系统对精确中断的影响

在流水线中实现精确中断并不是一件容易的事,尤其是在有分支、有延时槽的情况下。所以笔者不打算采用列举各种情况来分析引入微指令对精确中断的影响,而是试图从更高的层面去思考这个问题,以对终端系统进行针对性的修改。

中断和异常会在ID级或者EXE级到来。ID级的异常,可以是硬件中断,未实现的指令或者是系统调用systemcall。EXE级的异常可以是算术指令的溢出。

该微指令系统对原有精确中断的兼容性分析

分析引入微指令对精确中断的影响,要分析IF阶段的取指结果,和那些因素有关。如果中断到来,所保存的现场能够在恢复时确保取出的指令是正确的,那么引入微指令对中断就是兼容的。

时钟到来时,存入IR的指令与下面因素有关:

  1. pc的值
  2. upc的值
  3. IF的状态state

初看上一张的真值表,可以发现存入IR的指令还和当前指令是否是硬件实现的,和当前微指令是否是结束符有关。但如果考虑存储器中的内容在是固定的(对于指令和微指令而言,存储器的内容都是固定的),那么当前指令和当前微指令均是由pc和upc决定的。

也就是说,只要保存pc、upc和state,在恢复时就可以恢复这一个时钟到来时所取的指令。

而精确中断的机制,就是根据发生异常的位置,保存中断返回地址取指时的IF状态,因此,对原有终端系统的修改,只需要将过去只保存PC,改为同时保存PC、uPC和state即可。

中断到来对取指的影响

如果没有中断机制,IF级的状态转移如上一节的表格所示。但是中断机制的引入使IF级变得更加复杂。

中断相当于强制跳转并保存现场,也就是说中断到来时应当强制更新pc的值。但是IF级所取的下一条指令是由上文所说的pc,upc以及state决定的,而ID给出的中断信号,只能改变PC_INT多路器的选择信号。由此可知,上文中的状态转移过程,若wpc是1,则中断来临时,pc会在时钟到来时更新到中断向量所指的位置。

但有一种特殊情况,若此时IF级处于微指令状态,state=1,并且下一条微指令不是终结符,那么wpc=0,则pc在时钟到来时不会更新为中断向量所指的地址。这会造成错误。

再考虑IF级的输出。IF级输出也由pc,upc以及state决定,如果只对pc进行修改,使得pc在中断来临时强制修改,仍然不能使得中断向量正确向ID级传送。

所以作者在状态寄存器向IF控制器的输入处增加了一个多路器,在中断到来时强制设置state=0,但又不修改state寄存器的内容,让原有的state寄存器内容可以向下传递,使中断控制电路可以正确的保存中断前的状态,与此同时制造enter或者normal的假象,正确处理中断时的跳转问题。

小结

这篇文章描述了该自制计算机系统中处理器使如何处理微指令的。本文所描述的方法,可以充分利用硬线逻辑控制器的速度优势,又可以提供一个完整的微指令环境对指令集进行扩充。该方法区别于在很多书中记载的微指令实现方法,不使用微指令解码来控制各个功能部件实现不同的功能,而是直接以硬件实现的指令作为微指令。一条由微指令实现的指令,像一个汇编语言中的宏,如果没有这个微指令模块,这些功能自然可以通过汇编语言的宏来实现。但是,微指令模块的意义仍然存在,经过精心调试的微指令,可以充分利用延时槽、流水线冲突避免等特性来获得比用户自定义的宏更好的性能,也可以减少系统编程时出错的概率,还可以减少可执行代码的体积。在某些必须使用多周期才能实现的硬件指令,把他转换成这样的微指令,只需要付出极少的硬件代价,并且其执行可以高效的利用流水线。

该系统实现的难点有二:

  1. IF级的指令选择设计
  2. IF级的指令选择与中断系统的结合和兼容设计

关于该系统的进一步验证,等到具体实现,会用EDA工具仿真实验,届时笔者会把硬件逻辑描述和测试用例同步更新在文章中。

该项目是一个自制计算机系统的项目,其目标包括cpu设计、外围的控制器如内存、cache、IO、中断等设计、外设如打印机、存储器等的设计,编译器和操作系统的移植。项目本身并不追求极限性能,和完美的安全性,只是在力所能及的条件下做一些优化。功能完善、易于测试和扩展是本项目的首要目标。

硬件设计目标

本项目所使用的CPU为类MIPS架构,硬线逻辑仅支持整数指令,没有硬件乘法器、除法器。在FPGA中综合,不使用任何基于厂商的IP。采用5级流水线,精确中断,集成SDRAM双端口控制器,包含一级指令和数据cache。

CPU的整体结构与李亚民所著的《计算机原理与设计》中的相似,但调整了一些指令的实现,并期望引入微代码来增加CPU的可扩展性,降低编译器移植和系统编程的难度。

编译器移植目标

本项目计划移植lcc编译器到该平台,以实现工具链软件。由于本项目所使用的CPU与MIPS结构相似,所以移植基于lcc编译器的MIPS后端md文件进行。由于本项目所使用的CPU与MIPS结构不完全相同,修改lcc的后端的工作可能包括:

  • 熟悉lcc的整体结构,包括其前端和中间代码生成
  • 基于MIPS后端针对该平台进行移植
  • 进行一些力所能及的优化和测试工作

编译器是一个十分复杂的程序,编译器移植工作可能是整个项目中难度最大的部分。

其他工具链软件的设计

  • 对于连接器,本项目认为其在项目初期是可选的
  • 对于汇编器,本项目拟选择一个维护良好的开源汇编器为基础,进行修改
  • 对于预处理器,本项目拟直接使用gcc工具链的C语言预处理器

操作系统的移植目标

鉴于MINIX操作系统有详细的资料,并且代码量很少只有几万行,本项目打算在工具链软件调试适当时,进行操作系统的移植工作,目标是将MINIX操作系统移植到该自制平台。这些工作可能包含:

  • Minix 源码的阅读
  • X86 保护模式的考察
  • 对于该自制平台的保护模式的设计取舍的思考
  • 在不影响兼容性的情况下加入虚拟内存(段式或页式)
  • Minix 的移植工作

从目前来看,minix中平台相关的代码,大量是处理保护模式、中断向量等内容,在硬件相关的设计上可以进行取舍,以简化移植工作。

对于该自制平台的保护模式的设计目标,主要是为了保护操作系统内核的安全,保障操作系统的稳定运行。并不追求完美的进程间的数据隔离。因为Intel的处理器,花费了巨大的精力,实现了其精妙的保护模式,仍然逃不过幽灵、熔毁等旁路攻击。所以在一个科研性质的自制平台上追求完美的数据隔离是不自量力的行为。

Minix3 并不支持分页,使用的是段式存储,这需要对cpu的内存控制进行一些修改。

希望向CPU中加入微代码,也有方便系统编程的目的。可将常用的系统编程指令序列设计成若干由微指令解释的指令,如内存块的复制,中断、异常发生时现场的保存、从中断、异常返回时的现场恢复等。

因为系统编程这一部分,大部分是用汇编语言直接编写,所以编译器并不能帮上忙,若直接定义新指令,可以简化这部分编程的难度,也利于调试。而且这些系统编程指令,往往不是用户程序所使用的,因此编译器可能永远都不知道这些指令的存在,所以引入这些指令可能不需要对编译器的修改。

参考资料

该项目是一个长期项目,笔者已经为实施该项目所需要的知识储备准备了多年,并搜集了一些参考资料。

硬件系统的设计

  1. Verilog HDL 数字设计与综合, Samir Lalnitkar, 夏文宇等译。
  2. See MIPS Run, Dominic Sweetman. 这本书很好看,解释了许多重要的概念,例如MIPS中的指令延时槽,内存的保护等。关于这本书还有一些小故事,之前我一直在读这本书,直到最近我才发现这本书有一本中文译本,而且翻译质量非常好,打开一看竟然是我自己的老师屈建勤翻译的,相见恨晚呀!感谢屈老师能为大家提供这么好的学习资料的翻译
  3. Computer System Architecture, M.Morris Mano. 这本书是很老的书,但是笔者是从这本书里入门的。这本书是笔者刚开始学习计算机系统设计时,经过了很多次试错,最终选定的一本书,写的非常详细。书中从最基本的数字逻辑开始,一直讲到外围电路的设计,没有使用任何基于商业软件的IP,而且最终书里实现了一个很简单的CPU。
  4. 计算机原理与设计,李亚民。本项目设计的CPU基于这本书所记载的类MIPS CPU进行修改。笔者非常感谢这本书的作者,这是一本伟大的著作。这本书是我看到过的对处理器设计描述最详尽的著作。在工程上,理论和实践的差距之巨大,无法用语言描述。阅读这本书,从作者精妙的代码中,就可以填补理论和实践之间巨大的鸿沟,这对一个工程师来说会产生一种巨大的满足感。
  5. Fpga4Fun: https://www.fpga4fun.com/. 笔者通过这个网站入门FPGA。
  6. Computer Organization And Design The Hardware/Software Interface. 这是一本经典教材。

软件系统的设计

  1. Operating System Design and Implementation (the Minix Book), Andrew S. Tanenbaum. 这本书简直是学习操作系统的圣经,就是如果认真读代码的话,需要大量的精力。
  2. Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 3A: System Programming Guide, Part 1. Minix书中的代码是基于X86的,要想读明白其平台相关的部分,X86的系统编程手册是不可缺少的材料。关于X86的保护模式,手册里有介绍,但是他的介绍非常繁琐,笔者也是花了很长时间才弄明白,有机会笔者会写一篇博文,详细介绍minix如何使用X86的保护模式。
  3. MOP: Minix Overdocumentation Project,http://www.os-forum.com/minix/boot/index.php. 这个网站的作者,花费了大量的时间,对minix的启动代码进行了逐行分析。只要完整的阅读完这个网站,你就可以知道一台X86 PC在按下电源按钮后,BIOS运行结束后到操作系统加载之间的所有运行情况。但是要做好心理准备,这个过程很复杂,并且理解他需要了解X86的保护模式。Minix书中并没有对其boot monitor的代码进行注释,只是在书中提了一句,说boot monitor十分复杂,不亚于一个小型的操作系统。事实上也确实如此,笔者不才,完整阅读这个网站花费了两个多月的时间。笔者发现这个网站,是在读minix书的过程中,由于书中对其启动器描述的缺乏,使得我对操作系统初始的运行环境有疑惑,进而找到了这个网站,完美地解答了疑惑。这个网站的作者,对笔者也有很深的影响,有时候做一件事不需要考虑太多结果。学习本身就是一件有意义的事情,对于个人如此,对于社会更是如此。这个网站也是我准备这个博客的原因之一,要把自己的学习过程社会化,只要能有一个人从我的经历中得到帮助,本身就是一件很有意义的事情。
  4. Lcc-A Retargetable C Compiler: Design and Implementation. lcc是一个很有名的编译器,它本身是一个文本程序,这本书详细的介绍了其实现的每一个细节。只是笔者水平有限,读起来十分吃力,花了将近四个月时间才读完,掌握了对其修改的能力,也深入了解了ANSI C标准的实现。值得一提的是,这本书有一中文译本,但是很遗憾,不同章节翻译的质量有很大的波动,而且也有一些翻译上的错误,在这里笔者就不推荐了。
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×