学习编程 2018-05-27
什么是指令系统体系结构呢?要回答这个问题,其实非常的简单。但是想解释清楚,也没有那么容易。我们还是从一个小故事开始吧。
有一天两个小伙伴碰了面,发现对方都很愁苦,一个就问另一个"嘿你在愁苦什么呀?" 这个说"唉呀,最近遇到了很多运算上的问题。"
"运算量好大呀。"
对应说,"可不是吗,我也遇到了类似的问题。"
"咱们怎么解决呢?"
"不如咱们一起设计一个计算机吧!"
就说好啊,两人就一拍即和。
"我们设计计算机怎么分工呢?"一个说,"我显然是软件 程序员,我来编写如何运算的软件指令。"
另一个说"那正好,我是硬件工程师,我来设计计算机的硬件,主要是CPU,那咱们就分头工作吧!"
"这事估计得花一年时间才能把CPU设计出来,也才能把软件写好。真的就可以这么开始了吗?一年之后我们再碰面怎么保证你写的软件就能在我做的CPU上运行起来呢?"
"所以还不能着急,咱们得商量商量,得把咱们的共同规则订好,然后呢才能分头去设计软件和硬件,这样保证之后我们在碰面的时候软件和硬件能顺利的结合在一起。"
那好,这两个小伙伴就开始商量了,他们要商量的是什么呢?就是我们要谈的指令系统体系结构。
他们要面临的计算任务并不复杂,所以只要一个很简单的计算机指令系统就可以了。我们要设计多少指令?要设计哪些指令?首先要根据需求来确定。看上去我们只需要一些简单的加法,所以首先,我们需要设计一个加法指令。那这些指令和要运算的数据,都是要放在存储器当中的,如果把两个存储器当中的数字相加,可能会比较复杂。所以我们指令系统设计的时候呢,这个加法指令是这么做的:将一个寄存器当中的数,和一个存储器当中的数相加,然后存到这个寄存器当中。寄存器我们用R来表示,存储器当中的地址用M来表示。所以我们这个指令系统当中,包含了一条运算类的指令。
但是寄存器当中的数从哪里来呢?自然是从存储器当中来。把存储器当中的内容,装到寄存器当中来,这条指令就是LOAD,就是将后面一个操作数M,所指向的内存单元中的内容放到前一个操作数R所指向的寄存器当中。有了这两条指令之后,我们可以对内存当中的数进行加法运算了。但是运算的结果还在寄存器当中,所以我们还需要一条指令,把寄存器当中的数,再放回到存储器当中。这条指令,我们记为STORE,它的作用是将寄存器R当中的数,存入到M所指定的存储单元中。这两条指令用于在存储器和寄存器中间传送数据,所以我们称之为传送类的指令。
我们已经知道CPU是从内存当中,按照地址,依次取出指令开始执行的,那有时候我们想改变取指令的位置,这时候就需要用到这条指令,记作JMP L,当CPU执行这条指令之后,就会转移到L所指向的存储器单元中去取出下一条指令来执行,这样的指令我们称为转移类指令。
然后我们就设计好了我们想要的计算机的指令系统,虽然它很简单,但足够完成我们想要的运算任务了。但这还是用英文单字和字母进行的描述,并不是计算机所能识别的二进式代码。因此我们还要做进一步的规定,这就是指令的具体格式。
首先我们约定,每条指令都是等长的,都是两个字节。其中第一个字节,我们取了高四位作为操作码,操作码就是指明了这是一条什么类型的指令。我们现在有四条指令,LOAD、ADD、STORE和JMP,我们分别给它分配了四个不同的操作码。用十进制来表示就是0、1、2、3,因为我们预留了四个二进制位,所以以后还可以扩展,最多可以扩展到16条指令,但是现在我们只定义了四条指令。
接着看第一个字节的低四位,这个四位我们约定了作为寄存器号。我们现在提供四个寄存器,编号从0000到0011,分别指代CPU当中的R0到R3这四个寄存器。由于我们预留了四个二进制位,所以以后还可以继续扩展,最多到十六个寄存器。这就为我们发展第二代、第三代以及之后的设计时,提供了扩展的空间。
这个指令的第二个字节,我们约定作为存储单元的地址,这样有八个二进制位,所以我们一共可以使用2的八次方,也就是256个字节的存储器。
我们看一个例子,如果软件设计人员写了这么一条指令:0001 0010 0000 1001,那从第一字节高四位的操作码,0001这四位我们就可以看出来它是一个加法指令。从第一字节低四位0010这四位指定的寄存器号,就可以看出来他想访问的是R2这个寄存器。然后第二字节存储单元的地址,翻译成十进制的话就是9。所以这条指令的编码想完成的操作,就是将R2的内容和存储单元9的内容相加,存到R2寄存器当中。
但如果软件程序员写了这样的指令,0101 1010 0000 1001,如果CPU看到了这条指令,那它是无法识别的,它不知道该做什么样的操作。因为0101这个操作码并没有定义。1010这个寄存器号也没有定义。当然也许在第二代或第三代的设计中,我们可以再定义了0101是减法,1010可能是十号寄存器,那么在那个时候的CPU上,就可以执行这条指令了。但在我们约定的第一代CPU上显然是无法执行的。那约定好了指令的具体格式,我们再来看一看我们可以做的运算的任务。
假设我们要完成这样一个任务,将存储器地址M1中的内容,与存储器地址M2中的内容相加,最后存到M3这个存储单元当中,在完成运算之后,程序转向存储器当中L所指向的位置继续执行。但这样的任务,我们如何用我们现在有的这个指令系统来实现呢?我们能用的只有这四条指令,我们没有提供直接将两个存储单元的数进行相加的指令。
这个程序应该这么写,第一步将M1中的内容送到了一个寄存器,暂且记为RX,然后将RX的内容和M2的内容相加,运算结果存入RX,再将RX的内容送到存储器M3当中,这就完成了运算。再转移到L,取出下一条指令继续执行。
我们假设现实在第一个任务中,M1指向存储单元地址5,M2指向6,M3指向7,最后要转移的目标地址L是18。这几个都是十进制的描述。那基于这样的任务,我们的软件程序员就可以编写出中间这列这样的机器语言的程序,当然初看上去全是01的代码,很难分辨,为了便于学习,我将它按照不同的含义用不同的颜色表示出来。相同的颜色代表着它们对应的关系,这样我们就可以看出来第一条指令,0000的操作码,应该是一个LOAD指令, 接下来0011指定了寄存器的编号,所以它指向那个R3,第三部分是存储器的地址,这个二进制数实际上就是十进制的5,这就可以看出来我们是用LOAD这条指令,将存储单元当中地址5所对应的内容,传送到了R3这个寄存器当中。这样我们就可以依次分析出每条指令的功能,当然我还是要先强调一下,编写程序的实际的顺序。
最早的软件程序员,是需要直接编写机器语言指令的,那么它要编写的就是中间这列的01编码,并把这样的01编码在穿孔纸带上,钻上对应的小孔,送到计算机当中去,这样的工作显然是非常的繁琐,容易出错,效率也很低。那后来随着技术的进步,程序员就可以编写类似左边这一类的汇编语言程序。汇编语言程序基本上和机器语言程序可以作到一一对应,那我们通过一些工具,就可以先将汇编语言程序转换成对应的机器语言程序,然后再将机器语言程序输送到计算机当中。再进一步,人们又可以编写各种高级语言的程序。那当然还是需要更为复杂的工具,将高级语言经过若干个步骤,最后还是要转换成我们这个表格中间这一列所展示的机器语言,也就是一系列二进制代码。
现在我们已经写好了这样的程序,我们需要把这样的程序放到存储器当中去。我们要注意的是,这只是存储器的一个片段,最左边这一列只是存储器的地址。我们展示了存储单元从地址5一直到地址18,所保存的内容。
我们从上往下依次看,在地址5和6这两个存储单元中,存放了两个我们需要进行运算的源操作数,现在存放的是12和34。
地址单元7准备用来存放运算结果的,现在被初始化为0。
地址单元8和9这两个字节,存放了LOAD这条指令。为了便于观看,我还是用不同的颜色标出了不同含义的二进制位。
再往下的两个字节,是加法这条指令。
然后是STORE这条指令。
然后是转移JMP这条指令。我们知道JMP这条指令会让CPU转向地址单元18,去取出指令来。所以这里所描述的第五条指令是不会被执行的。在执行完JMP这条指令以后,CPU会取出第六条指令进行执行。
我们还是用大家熟悉的模型机的图示进行说明。假想我们已经把刚才的程序和数据输送到了这个模型机的存储器当中,与此同时,硬件工程师也设计完成了CPU,并将CPU和存储器进行连接,构建成了完整的计算机的系统。
为了便于阅读,我将存储器当中二进制码进行了转换。我们可以看到,在存储单元中,依次存放着我们要进行运算的源操作数,准备存放结果的空间,还有包含了四条指令的这段程序。然后看右边CPU当中,如果PC寄存器已经装入了0000 1000这样一个地址,接下来的过程大家应该很熟悉了。PC中的这个地址会通过MAR寄存器,再通过地址总线送到存储器,存储器就会找到这个单元所对应的那条指令,也就是LOAD这条指令,把这条指令的编码送回CPU。接下来就会依次完成我们所编写的这个程序。
当然最后还有一个问题,为什么PC寄存器当中的地址是这个呢?其实这是不一定的,这也是我们进行指令系统体系结构设计时,必须要约定的一个内容,就是CPU在启动时,或者说在复位完成之后,第一条指令从哪里开始取出,这也是最开始,软硬件双方必须商量好的事情。至于这个地址到底应该是什么,并没有明确的规则。但通常情况下,我们会约定为这个体系结构所能访问的存储单元的最小地址,也就是0,或者是接近最高地址的地方。
那大家也可以思考一个很简单的问题,如果我们这个体系结构约定的CPU的起始地址是全0的话,存储器当中的程序应该做怎么样的改动呢?如果要求现在左边这个图中,已经存放的程序和数据的位置都不允许变动呢?其实解决方法很简单,留给大家思考。
现在我们已经对什么是指令系统体系结构有了初步的了解,也知道如何着手开始设计一个属于自己的计算机。从下一节开始,我们将一起分析几个真实的体系结构。