《操作系统真象还原》——0.25 指令集、体系结构、微架构、编程语言

  1. 云栖社区>
  2. 博客>
  3. 正文

《操作系统真象还原》——0.25 指令集、体系结构、微架构、编程语言

异步社区 2017-05-02 16:11:00 浏览1555
展开阅读全文

本节书摘来自异步社区《操作系统真象还原》一书中的第0章,第0.25节,作者:郑钢著,更多章节内容可以访问云栖社区“异步社区”公众号查看

0.25 指令集、体系结构、微架构、编程语言

指令集是什么?表面上看它是一套指令的集合。集合的意思显而易见,那咱们说说什么是指令。

在计算机中,CPU只能识别0、1这两个数,甚至它都不知道数是什么,它只知道要么“是”,要么“不是”,恰好用0、1来表示这两种状态而已。

人发明的东西逃不出人的思维,所以,先看看我们人类的语言是怎么回事。

不同的语言对同一种事物有不同的名字,这个名字其实就是代码。比如说人类的好朋友:狗,咱们在中文里称之为狗,但在英文中它被称为dog,虽然用了两种语言,但其描述的都是这种会汪汪叫、对人类无比忠诚的动物。人是怎样识别小狗的呢?识别信息来自听觉、视觉等,这是因为人天生具备处理声音和图像的能力,能够识别出各种不同的声音和颜色不同的图像。可是计算机只能处理0、1这两个数,所以让计算机识别某个事物,只有用01这两个数来定义。也就是说,要用0、1来为各种事物编码。

为了更好地说明指令集,咱们这里不再用现有的语言举例子,当然也不是要自创指令集。下面举个简单的例子来演示指令集的模型。

咱们拿表达式A=B+C为例。假设A、B、C都是内存变量的值,它们的地址分别是0x3000、0x3004、0x3008。在此用Ra表示寄存器A,Rb表示寄存器B,Rc表示寄存器C。

完成这个加法的步骤是先将B和C载入到Ra和Rb寄存器中,再将两个寄存器的值相加后送入寄存器Ra,之后再将寄存器Ra的值写入到地址为0x3000的内存中。

步骤有了,咱们再设计完成这些步骤的指令。

步骤1:将内存中的数据载入到寄存器,咱们假设它的指令名为load。

screenshot

步骤2:两个寄存器的加法指令,假设指令名为add。

步骤3:将寄存器中的内容存储到内存,假设指令名为store。

以上指令名都是假设的,名字可以任意取,因为CPU不识别指令名。指令名是编译器用来给人看的,为的是方便人来编程,CPU它只认编码。目前CPU中的指令,无论是哪种指令集,都由操作码和操作数两部分组成(有些指令即使指令格式中没有列出操作数,也会有隐含的操作数)。咱们也采用这种操作码+操作数的思路,分别为这两部分编码。

咱们先为操作码设计编码。

screenshot

接下来为操作数编码,操作数一般是立即数、寄存器、内存等,咱们这里主要是为寄存器编码。
screenshot

好啦,操作码和操作数都有了,其实指令集已经完成了。不过在一长串的二进制01中,哪些是操作码,哪些是操作数呢?这就是指令格式的由来啦。我们人为规定个格式,规定操作码和操作数的大小及位置,然后在CPU硬件电路中写死这些规则,让CPU在硬件一级上识别这些格式,从而能识别出操作码和操作数。

假设我们的指令格式最大支持三个寄存器参数和一个立即数参数。其中操作码和各寄存器操作数各占1字节,立即数部分占4字节。各条指令并不是完全按照此格式填充,不同的指令有不同的参数,只有操作码部分是固定的,其他操作数部分是可选的。当CPU在译码阶段识别出操作码后,CPU自然知道该指令需要什么样的操作数,这是写死在硬件电路中的,所以不同的指令其机器码长度很可能不一致。

为了演示指令集模型,我们在上面假设了寄存器名、指令名、格式。按理说这对于指令集来说已经全了,不过,为方便咱们了解编译器,不如咱们再假设个指令的语法吧,咱们这里学习Intel的语法格式:“指令目的操作数,源操作数”。目的操作数在左,源操作数在右,此赋值顺序比较直观。Intel想表达的是 a=b这种语序,如a=b,便是mov a,b。

以上三个步骤的机器码按照十六进制表示为:
screenshot

以上自定义的指令便是按照咱们假设的语法来生成的。对于机器码的大小,由于指令不同,需要的操作数也不同,所以机器码大小也不同。另外,机器码中的立即数是按照x86架构的小端字节序写的,这一点大家要注意。小端字节序是数值中的低位在低地址,高位在高地址,数位以字节为单位。前面有一小节说明大小端字节序问题。

步骤2的机器码为01 00 01 10。操作码占1字节,CPU识别出第1字节的二进制01是add指令,知道此指令的操作数是3个寄存器,并且第1个寄存器操作数是目的寄存器,另外两个寄存器是源操作数(这都是我们假定的,并且是写死在硬件中的规则,不同的指令有不同的规则,您也可以创造出内存和寄存器混合作为操作数的加法指令)。于是到第2字节去读取寄存器编码,发现其值为二进制00,就是寄存器Ra对应的编码。接着到下一个字节处继续读出寄存器编码,发现是二进制01,也就是寄存器Rb,Rc同理。于是将寄存器Rb和Rc的值相加后存入到寄存器Ra。

步骤3中,机器码为10 00 0c300000,CPU读取机器码的第1 字节发现其为二进制10,知道其为指令store,于是便确定了,目的操作数是个立即数形式的内存地址,源操作数是个寄存器。接着到指令格式中的寄存器操作数1的位置去读取寄存器编码,发现其值为00,这就是寄存器Ra的编码。机器码中剩下的部分便作为立即数,这样便将寄存器Ra的值写入到内存0x0000300c中了。

以上指令集的模型,确实太过于简单了,也许称之为模型都非常勉强。现实中的指令格式要远远复杂得多。下面我们看看目前世面上的指令集有哪些。

最早的指令集是CISC(Complex Instruction Set Computer),意为复杂指令集计算机。从名字上看,这套指令集相当复杂,当初这套指令集问世的时候,它的研发者们都没想过要给它起名,只是因为后来出现了相对精简高效的指令集,所以人们为了加以区分,才将最初的这套相对复杂的指令集命名为CISC,而后来精简高效的指令集称为RISC(Reduced Instruction Set Computer)。

CISC和RISC并不是具体的指令集,而是两种不同的指令体系,相当于指令集中的门派,是指令的设计思想。举个例子,就像中医与西医,中医讲究从整体上调理身体,西医则更多的是偏向局部。这就是两种不同的医疗思路,类似于CISC和RISC这两种指令体系。那什么是指令集呢?拿中医举例,像华佗、张仲景这两位医圣,他们虽然都是基于中医的思想治病,但医术各有特色,水平也不尽相同,这就相当于不同的指令集。一会儿咱们会介绍具体的指令集。

为什么说CISC复杂呢?

首先,因为它是最早的指令集,当初都是摸着石头过河,肯定有一些瑕疵在里面。其次,当初的程序员都是用汇编语言开发程序,他们当然希望汇编语言强大啦,尽量多一些指令,尽量一个指令能多干几件事,所以指令集中的指令越来越多,越来越复杂。不过这样的好处是程序员同学很爽。最后,CISC是Intel使用的指令集,Intel公司在兼容性方面做得最好,指令集在发展的过程中,还要兼容过去有瑕疵的古董,以至于最后的指令集变得有点“奇形怪状”了。

作为后起之秀的RISC,借鉴了前辈CISC的经验,取其精华,弃其糟粕,当然要更好更轻量啦。它是怎么来的呢?

CISC不是做得很全很强吗,可是很多时候,程序员并不会用到那些复杂的指令和寻址方式,即使用到了,编译器有时候为了优化,未必“全”将其编译为复杂的形式。这就导致了CPU中的复杂的指令和寻址方式无用武之地。根据二八定律,指令集中20%的简单指令占了程序的80%,而指令集中80%的复杂指令占了程序的20%。根据这个特性,处理器及指令集被重新设计,保留了那些基本常用的指令,减少了硬件电路的复杂性。这样,大部分指令都能在一个时钟周期内完成,更有利于提升流水线的效率。而且,指令采用了定长编码,这样译码工作更容易了。由于其太优秀了,后来的处理器,如MIPS,ARM,Power都采用RISC指令体系,做得最好的就是MIPS处理器,它严格遵守RISC思想,业界公认其优雅。

我们常用的CPU是Intel和AMD公司的产品,它们用的指令集便是基于CISC思想的x86。AMD的x86指令架构是Intel授权给他们的,为区别于此,Intel在官方手册上称自己的指令集为IA32。

虽然AMD采用的也是x86指令集,但Intel可没把硬件实现方法也告诉AMD,否则AMD的CPU和Intel的CPU不就完全一样了吗,人家Intel也不肯呢。指令集是一套约定,里面规定的是有哪些指令、指令的二进制编码、指令格式等,如何实现这套约定,这是硬件自己的事。打个比方,这就像和朋友约好了在某餐厅吃饭,咱是坐车去,还是走着去,这是咱们的事,与吃饭是无关的。说白了,在Intel的CPU上运行的软件也能够在AMD的CPU上运行,原因就是它们共用了同用一套指令集,也就是对二进制编码达成了共识。它们面对相同的需求,可能采取了不同的行动,但都完成了任务。比如机器码是b80000,Intel的CPU经过译码,知道这是将0赋值给寄存器ax,相当于汇编语言mov ax,0。AMD的CPU在译码时,也得将此机器码认为是将0赋值给寄存器ax。至于它们在物理上是怎么将0传入寄存器ax中的,这是它们各自实现的方式,与指令集无关。它们各自实现的方式,就叫微架构。

总结一下,指令集是具体的一套指令编码,微架构是指令集的物理实现方式。

发展到后来,x86指令集越来越复杂。它本属于CISC体系,但由于效率低下,最终在其内部实现上采取了RISC内核,即一条CISC指令在译码时,分解成多条RISC指令,这样其执行效率便可与RISC媲美啦。

目前市面上常见的指令集有五种,除x86是CISC指令体系外,ARM、MIPS、Power、C6000都是RISC指令体系的指令集。

CPU与指令集是对应的,一种CPU只能识别一种指令集,所以很多CPU都以其支持的指令集来称呼。比如ARM、MIPS,它们本身是CPU名称,又是指令集名称。

ARM主要用在手机中,作为手机的处理器。Power是IBM用于服务器上的处理器。C6000是数字信号处理器,广泛用于视频处理。而MIPS虽然本身很优秀,但其在各领域起步都较晚,并没有广泛应用的领域。

由于MIPS本身的优越性,龙芯用的就是mips指令集,有没有人问,为什么咱们自主研发的CPU还要用人家国外的指令集?就不能也研发出一套指令集吗?能倒是能,不过语言不通用。就像我自己可以发明一门语言,语言本身没什么问题,问题是我用自己发明的语言和别人交流,谁听得懂呢,谁又愿意去学这门语言呢?大家都很忙,不通用的东西没人愿意花精力去学。如果龙芯也自立门户创造新的指令集,那有谁愿意给它写编译器呢?即使有了编译器,操作系统也要重新编译发布,应用程序也要重新编译发布,指令集背后不仅是个计算机生态链,更重要的是全球经济链。

平时所说的编程语言,虽然其上层表现各异,归根结底是要在具体的CPU上运行的,所以必须由编译器按照该CPU的指令集,翻译成符合该CPU的指令。说到这,不得不说一下交叉编译,本质上交叉编译就是用在A平台上运行的编译器,编译出符合B平台CPU指令集的程序,编译出的程序直接能在B平台上运行啦。这里的平台指的就是CPU指令体系结构。

网友评论

登录后评论
0/500
评论
异步社区
+ 关注