本篇博客试图对CPU运行的细节进行一次全景式的描述,以弥补写Version1.0时的缺憾。此文以《深入理解计算机系统》的第四章“处理器体系结构”为蓝本,结合从《编码》中得到的知识,力图把执行一条指令时,每一步中CPU到底都干了些什么描述清楚。
由于这个执行过程与所使用的指令集密不可分,所以首先摆上《系统》中用到的ISA:
这是一套不定长的指令集,指令之间的长度并不一致,直觉上这比定长指令要麻烦些(麻烦的是硬件的设计)。大体上,这些指令分为三部分:第一部分是指令指示符,占一个字节;第二部分是寄存器指示符,也就是寄存器地址,也占一个字节;第三部分是立即数,表示一个地址或者操作数,四个字节。其中第一部分是必不可少的,剩余的两部分根据指令类型不同“酌情删减”。
可以看到,第一个字节和第二个字节还可以继续细分:一个字节有8位,它们被平均分成高四位与低四位,里面存放着不同的代码。
寄存器指示符字节的两个部分意义很简单,它们分别代表两个寄存器地址,一般来讲(好吧是我瞎猜)rA代表源寄存器,而rB代表目的寄存器。如果一个寄存器地址被设为0xF,则意味着不需要访问寄存器。原来寄存器也是有地址的,表面上我们看到的是%eax,%ebx这个字符,但这只是为了方便人类的阅读,计算机内部还是要把字符翻译成数字的。请看下图:
指令指示符字节的两个部分分别称为code和function,code表示一个大类,如果该类指令下有许多指令,则需要用function来标号不同的指令,没有的话就设为0(注:此句说法有误。应该是,此类指令只有一条的话,function值是0,若有其他指令,则在0的后面递增)。可以看到,code的值是从0到0xB,代表12类不同的指令,而2、6、7号指令下面有再细分,function用fn代表,不再详细论述。
那么问题来了:如何识别以及利用这些代码呢?比如0号指令代表halt指令,电脑怎么知道?毕竟目前这只是停留在纸面上。我的意思是,必须有个翻译电路,把这些代号翻译成相应的控制信号(以后会在我的文章经常看到“翻译”这个词,我觉得计算机中经常会用到这个“方法”,比如IP系统)。《系统》中有这么一句话:每条指令的第一个字节有唯一的code和function组合,给定这个字节我们就可以决定所有其他附加字节的长度和含义。下面我们就来具体解释一下这句话的含义:
由图可以看到,每条指令被执行之前,其起始地址就已经被加载到PC中,新的时钟周期一旦开始,在控制信号的作用下,各个相关的”门“就会被打开,于是内存中相应地址中的指令就会被瞬间传输到指令寄存器中(好吧,这里其实有个问题我也没搞清楚:就是如何确定指令的长度?因为这里的说法似乎暗示指令的所有字节是一次性取出的,但是那句话的暗示感觉相反:即先取出第一个字节才能确定总指令长度),然后指令寄存器中的指令被分拆:
第一个字节被送到split单元,并在那里被拆分成code和function,然后分别送往icode和ifun两个部件(目前我们只可以把它们视为黑盒子,不需要了解内部电路的实现),icode根据code值计算出三个信号instr_valid、need_regids、need_valC,其含义是指令合法性、是否需要访问寄存器、有无立即数。
剩余的字节被送往align单元,align会根据need_regids信号和寄存器指示符字节产生寄存器的地址信号(这个信号应该被送往寄存器的地址选择器),具体来说,如果need_regids=0,那表示不需要访问寄存器,那么地址信号就会被Align设为0xF;如果need_regids=1,那表示要访问寄存器,那么产生的地址信号其实直接等于从内存中读取的代码,align不会做什么改变,于是这个时候如果只需要一个寄存器的话,另一个寄存器地址必须要携程0xF,而且是由指令本身提供的,而不是由align设置。
另外align还要产生立即数,它需要两个信息,即need_regids、need_valC:如果need_valC=0,那就不需要了;如果need_valC=1,那还要看need_regids的值,以便确定第二个字节是不是ValC的起始字节。
写到这里,我越来越觉得,应该不会一次性(即在一个时钟周期内)把指令的全部字节读入指令存储器,而是先读入第一个,然后根据需要再读入后续字节。比如,有的指令本身只有一个字节,第二个字节根本不存在,那还读入干嘛?甚至第二个字节本身可能是下一条指令的首字节,这样的话,CPU处理下一条指令的时候会把其他字节当成指令指示符,然后可以预见整个程序就会崩溃。
不过接着我读到这句话,又彻底推翻了刚才的结论:PC增加器单元根据当前的PC以及need_regids、need_valC产生valP,即下一条指令的地址。
计算方式为:p+1+r+4i,其中p为当前指令的其起始地址,1代表指令指示符,r是need_regids的值,而i是need_valC的值。这个公式很简单对不?但是我从中看到了刚才的逻辑里面致命的一个缺点,即只要有valP,不管现在这条指令有没有读入下一条指令的字节,总能确定下一条指令的起始地址。还有,指令从内存到指令寄存器的过程是一个复制的过程,而不是剪切。。。所以,CPU的逻辑就是:不管你这条指令有多长,我先一次性读入六个字节再说,后面的字节用到了最好,用不到也不妨碍下一条指令的执行。(不得不说,这个发现是在我写的过程中发现的,前几天想了好久也没想出来,看来“写是为了更好的思考”这句话真的很对)
我想,写到这里基本上就可以结束了,最关键的那部分就只有这么多了。剩下的部分无非就是ALU、寄存器、内存等部件在由icode和ifun产生的控制信号的控制下,打开或关闭各种“门”,让数据按照指定的管道流动,非常琐碎。可能ALU的实现稍微复杂一些,但今天就先写这么多吧。有时间再补充。