CPU是如何执行代码的?

2021年12月6日汽车技术评论291阅读模式

从半导体开始讲起。

 

半导体

 

CPU是如何执行代码的?

半导体,就是导电性能介于导体与绝缘体之间的材料,比如二极管。

CPU是如何执行代码的?

电流可以从正极流向负极,但反过来则不行。所以,可以它可以防止电流逆流。

当负极12V,正极0V,二极管断开;

当负极0V,正极12V,二极管导通。

正极的电流源源不断的流向负极,导最终达到正负极都是12V,稳定后,可将其视为导线。

 

有了半导体这个性能,我们可以尝试做一个【与

 

CPU是如何执行代码的?

 

当A端或B端为0V时,Y端和0V导通,使得Y端也是0V。
当AB两端均为12V时,Y和AB之间没有电流流动,Y端也是12V。
我们把这个装置成为【与,把高电平计为1,低电平计为0。

即AB同时输入1,输出端Y才是1;AB有一个为0,输出端Y则为0。

 
按照此逻辑,我们同样可以设计出
【或门】【非门】【异或门】【或非门】【异或非门】

CPU是如何执行代码的?

当然,这些【门】都可以用二极管或三极管实现,但实际应用过程中,考虑到电路的功耗等因素,更多的使用场效应管(也叫MOS管)来实现。这里对硬件的内容不多赘述,有机会再展开分享。

有了这些基础的门电路,我们就可以做基础逻辑了。从最基础的加法器开始说。

 

加法器

 

CPU是如何执行代码的?
加法器,就是计算“加法运算”的电路。
生活中,我们采用十进制的计数体系,用0-9十个数字运算;
机器码的世界里,采用二进制的计数体系,用0和1运算。
通过一个【异或门】和一个【与门】就可以实现简单的加法器:
AB只能输入0或1,即下面的加法器,可以计算0+0,0+1或1+1:

CPU是如何执行代码的?

S表示计算的结果,C表示是否要进位。

1 + 1 = 10,那么S就是0,C就是1。

由逻辑状态表可写出逻辑式:CPU是如何执行代码的?

并由此画出图1(a)的逻辑图。图1(b)是半加器的逻辑符号。

CPU是如何执行代码的?

   (a)逻辑图

CPU是如何执行代码的?

     (b)逻辑符号

到这里,我们就能计算“1+1”了,有成就感吗?

那如果1+2,咋办呢?用上面的加法器好像搞不定了。
也就是要解决:第二位需要处理第一位有可能进位的问题。
所以,要设计一个“全加器”。相对应地,上面的加法器称为“半加器”。

CPU是如何执行代码的?

上面的图使用太复杂,简化一下:

CPU是如何执行代码的?
(a)逻辑图  

 CPU是如何执行代码的?
 (b)逻辑符号

全加器有2个输出和3个输入组成,分别输入要相加的两个数和上一位的进位,然后输入结果和是否进位。
 
再进一步,如果我们将4个加法器串联在一起,咦,OMG,竟然可以计算4位数的加法了,15+15以内,都不在话下。Amazing!
 

CPU是如何执行代码的?

实现减法

 

CPU是如何执行代码的?
虽然已经能够实现4位数的加法了,但是能进行的计算操作还是非常有限,比如我想知道CPU里的7-3是怎么实现的呢?
值得一提的是,减法也是通过加法器原理实现的

在4位数中,一共可以表达16个数,需要引入‘补数'的概念,例如3的补数是13,4的补数是12,5的补数是11,当你计算7减去4的时候,可以变成7加上3的补数,即7+13。

 

可是7+13是20,但是7-3等于4啊?

 

20已经超出4位能表达的16个数了,已经溢出,所以20还得减去16,就是4了。用二进制算一下:
 
7-3 = 0111 - 0011 = 0111 + 1101(二进制13) = 10100
10101已经溢出了,去掉最高位是0100,就是十进制4了。
 
其实,补数的概念源于钟表
 
比如,现在是7点,我想让它回到4点。有两种办法, 一种方法是让时针后退3格,另外一种方法是让时针前进9格,前进到12点的时候,其实就相当于溢出了,需要舍弃。
 
等等,这不就是我们的求模(MOD)运算吗?钟表是12进制的,那么
 

向后退3格:7 - 3 = 4

向前进9格 :(7 + 9) mod 12 = 4

向前进21格:(7+9+12) mod 12 = 4

向前进33格:(7+9+12+12) mod 12 = 4

.....

但是,我们怎么得到所谓的补数呢?从3怎么得到13呢?
 
其实,这些问题前辈们都已经考虑过,并有非常简单的操作实现了,即代码世界里的“补码”的概念。
 

CPU是如何执行代码的?

补码:所有位取反,再加1。
 
负数的表示
 

 7-3可以换算成7+13了,如果是3-7呢?

负数的引入,使得系统变得更复杂了。首先我们得用一个标志位来表示正数还是负数。

CPU是如何执行代码的?


最高位的0表示正数,1表示负数,真正有效的数字只剩下3位了,正数的范围是从1到7,负数的范围从-1到-7,不过这里出现了两个零!一个正0,一个负0,这不妥吧。

我们又想到了前面提到的“补码”。

例如8-3相当于8+(-3)的补码,那我们完全可以把表格中的负数用“补码”表示,然后把那个负0 特别当做-8来处理:
CPU是如何执行代码的?

我们来算一下7-4。7是0111,-4是1100。注意我们把符号位也算进去了,两者相加:

CPU是如何执行代码的?

再我试试4-7,4是0100,-7是1001,两者相加:

CPU是如何执行代码的?

把负数用补码表示,不但减法变加法,  连符号位都可以参与运算了!
 
 在CPU内使用补码来表示二进制数,如果是一个正数, 补码就是它本身,如果是负数,需要把除了符号位之外的二进制数进行取反加一的操作。
 
到这里,是不是有些熟悉的画面出现了,比如,
 
UINT8的范围是[0,255];
SINT8的范围是[-128,127];

所以,补码让二进制的表示更加易于理解。

加法,减法都有了,那乘法和除法呢?

 

乘法和除法

CPU是如何执行代码的?
有了加减的基础后,乘除就显得很容易理解了。
是不是感觉和小学一年级学习加减乘除的过程一样。
我们从最简单的乘以2开始,有些同学说,为什么不乘以1?我不想解释。
乘2简单,对于一个2进制数,在后面加个0就是乘2:
 
比如
5=101(2)
10=1010(2)
我们只要把输入往左移动一位,再在最低位上补个零就是乘2。所以在C代码的逻辑中,乘法除了用*乘外,还可以用左移一位实现。当然,*乘最终也是通过移位实现。
看完简单的乘2了,那乘3呢?很简单,先移一位(乘2)再加一。乘5呢?先左移两位(乘4)再加一。
是不是相比于加减而言,乘除操作更加易于理解。

四则运算

CPU是如何执行代码的?

有了前面的基础,我们就可以计算基础的四则运算了吧。

试试看。比如计算A+B*2,用前面的基础来计算,不难吧。

那如果,我再计算(A+B)*2呢?也不难,把前面加法器模块和位移模块的接线调整一下,就能实现。

没错啊,编程的过程就是把线拔过来,插过去啊。

这肯定不行吧。需要引入两个有用的模块。

 

两个模块

CPU是如何执行代码的?
触发器和选择器。
 
触发器flip-flop,简称FF,长这样:

CPU是如何执行代码的?

触发器的作用是存储1个bit数据。

比如RS型的FF,R是Reset,输入1则清零。S是Set,输入1则保存1。RS都输入0的时候,会一直输出刚才保存的内容。

用FF来保存计算的中间数据(也可以是中间状态),1 bit肯定是不够的。但是通过并联后,用4个或8个来保存4位或者8位数据。这种保存数据的东西,我们称之为寄存器(Register)。

选择器:MUX

CPU是如何执行代码的?

选择器,即开关,当sel输入0则输出I0的数据,是啥输出啥,0、1皆可。同样地,sel输入1则输出I1的数据。当然选择器可以做的很长,比如四进一出的:

CPU是如何执行代码的?

有了这选择器模块以后,我们就可以给加法器和移位模块设计一个激活针脚。

这个激活针脚输入1则激活这个模块,输入0则不激活。如此,就可以控制数据是走加法器模块还是移位模块了。

 

指令和数据

CPU是如何执行代码的?
我们给CPU先设计8个输入针脚,4位指令,4位数据。
再加3个指令:
0100,数据读入寄存器
0001,数据与寄存器相加,结果保存到寄存器
0010,寄存器数据左移一位
 

其实,数据与寄存器相加,结果保存到寄存器,这一条执行起来,也至少需要三步,读取指令,执行指令,写寄存器。

 

经典的RISC设计则是分5步:读取指令(IF),解码指令(ID),执行指令(EX),内存操作(MEM),写寄存器(WB)。

 

下面我们用前面定义的3个指令测试下吧。以(1+4)*2+3为例。

 

0100 0001 ;寄存器存入1
0001 0100 ;寄存器的数字加4
0010 0000 ;乘2
0001 0011 ;再加三
把程序整理一下:
 
01000001000101000010000000010011
顺便一提,其实,用程序控制CPU是个很高级的想法,很早的计算机(器)CPU都是单独设计的。
 
1969年,有一家日本公司BUSICOM,它想搞个程控的计算器,而负责设计CPU的美国公司也觉得每次都重新设计CPU是个很low的事。于是,双方一拍即合,于1970年推出一种划时代的产品:世界上第一款微处理器4004。
 
这个架构改变了世界,那家负责设计CPU的美国公司也一步一步成为了业界巨头。它就是Intel,可别说你没听说过。
回到我们的程序,一个简单的运算,如果按照上面的程序执行,是不是效率有点低?确实。小朋友估计掰着手指头都已经计算出来了,这里还没有结果呢。
 
后来,计算机行业的前辈们进行了优化。
把我们机器语言写成的程序
 
0100 0001 ;寄存器存入1
0001 0100 ;寄存器的数字加4
0010 0000 ;乘2
0001 0011 ;再加三
改成如下:
MOV   1 ;寄存器存入1
ADD   4 ;寄存器的数字加4
SHL   0 ;乘2(介于我们设计的乘法器暂时只能乘2,这个0是占位的)
ADD   3 ;再加三
 

熟悉吗?没错,这就是汇编语言。汇编语言和机器语言相互紧靠的交互层。

 

我们写的汇编可以完美的改写成机器语言,直接指挥CPU,进行底层开发。同时,我们也可以把内存中的数据dump出来,以汇编语言的形式展示出来,方便调试和debug。
 

汇编语言极大的增强了机器语言的可读性和开发效率,但对于人类来说也依然是太晦涩了,于是程序猿前辈们又发明了高级语言,以近似于人类的语法来表现数据结构和算法。
 
从高级编程语言到机器语言处理,需要编译器的介入,编译过程比较复杂,三言两语也难以说明,大家知道有这么回事儿就行了。如果想深入研究,可以整本《编译原理》看看。

目前比较流行的高级语言有VB、Java、C、C++、Python、PHP等。

下面是2019年最新的世界编程语言排行榜:

CPU是如何执行代码的?

随着人工智能和大数据等发展,Python的应用也如日中天,有时间精力的同学可以花些精力,多涉猎一下编程语言,不一定十分精通,能“不求甚解”也很难能可贵。
 
到这里,C文件中一行代码的执行过程基本算是说明白了。

 

写在后面

CPU是如何执行代码的?
上文写的内容,主要是依托于加法器来解释高级编程语言中逻辑的实现过程。但是,实际在CPU中的实现是比这要复杂得多的,仅从高级语言到机器语言的解析过程就有很多层,仔细解释起来可能要把编程的几本书搬过来都说不明白。其中还涉及一些数据结构,像堆栈;涉及基础软件的调度执行,如操作系统等。嵌入式的系统远比我们想象得还要复杂。这里也是想尝试跟大家解释得简单些。
我们了解的越多,圈子越大,不知道得也就越多。但是,我们依旧要保持这种探索的精神,“路漫漫其修远兮,吾将上下而求索”,共勉。

weinxin
扫码关注公众号
关注公众号领精彩彩蛋!

发表评论