首先要开始这个话题要先说一下半导体。啥叫半导体???
半导体其实就是介于导体和绝缘体中间的一种东西,比如二极管。
电流可以从A端流向C端,但反过来则不行。你可以把它理解成一种防止电流逆流的东西。 当C端10V,A端0V,二极管可以视为断开。 当C端0V,A端10V,二极管可以视为导线,结果就是A端的电流源源不断的流向C端,导致最后的结果就是A端=C端=10V等等,不是说好的C端0V,A端10V么?咋就变成结果是A端=C端=10V了?
你可以把这个理解成初始状态,当最后稳定下来之后就会变成A端=C端=10V。 文科的童鞋们对不住了,实在不懂问高中物理老师吧。反正你不能理解的话就记住这种情况下它相当于导线就行了。利用半导体,我们可以制作一些有趣的电路,比如【与门】
此时A端B端只要有一个是0V,那Y端就会和0V地方直接导通,导致Y端也变成0V。只有AB两端都是10V,Y和AB之间才没有电流流动,Y端也才是10V。我们把这个装置成为【与门】,把有电压的地方计为1,0电压的地方计为0。至于具体几V电压,那不重要。也就是AB必须同时输入1,输出端Y才是1;AB有一个是0,输出端Y就是0。 其他还有【或门】【非门】和【异或门】,跟这个都差不多,或门就是输入有一个是1输出就是1,输入00则输入0。 非门也好理解,就是输入1输出0,输入0输出1。 异或门难理解一些,不过也就那么回事,输入01或者10则输出1,输入00或者11则输出0。 (即输入两个一样的值则输出0,输入两个不一样的值则输出1)。 这几种门都可以用二极管做出来,具体怎么做就不演示了,有兴趣的童鞋可以自己试试。每次都画二极管也是个麻烦,我们就把门电路简化成下面几个符号。
![](https://static.mianbaoban-assets.eet-china.com/2020/11/q6bmqi.jpeg)
![](https://static.mianbaoban-assets.eet-china.com/2020/11/j673m2.jpeg)
每次都这么画实在太麻烦了,我们简化一下
![](https://static.mianbaoban-assets.eet-china.com/2020/11/zUNrEj.jpeg)
![](https://static.mianbaoban-assets.eet-china.com/2020/11/UJJbui.jpeg)
我们就有了一个4位加法器,可以计算4位数的加法也就是15+15,已经达到了幼儿园中班水平,是不是特别给力?做完加法器我们再做个乘法器吧,当然乘任意10进制数是有点麻烦的,我们先做个乘2的吧。乘2就很简单了,对于一个2进制数数我们在后面加个0就算是乘2了比如:
5=101(2)10=1010(2)
所以我们只要把输入都往前移动一位,再在最低位上补个零就算是乘2了。具体逻辑电路图我就不画,你们知道咋回事就行了。 那乘3呢?简单,先位移一次(乘2)再加一次。 乘5呢?先位移两次(乘4)再加一次。 所以一般简单的CPU是没有乘法的,而乘法则是通过位移和加算的组合来通过软件来实现的。这说的有点远了,我们还是继续做CPU吧。现在假设你有8位加法器了,也有一个位移1位的模块了。串起来你就能算了!
(A+B)X2
激动人心,已经差不多到了准小学生水平。那我要是想算 呢?
AX2+B
简单,你把加法器模块和位移模块的接线改一下就行了,改成输入A先过位移模块,再进加法器就可以了。 ![](https://static.mianbaoban-assets.eet-china.com/2020/11/vAJjMz.png)
![](https://static.mianbaoban-assets.eet-china.com/2020/11/jUZjUv.jpeg)
是不是有种生不逢时的感觉?
虽然和美女作伴是个快乐的事,但插线也是个累死人的工作。所以我们需要改进一下,让CPU可以根据指令来相加或者乘2。
![](https://static.mianbaoban-assets.eet-china.com/2020/11/2eERbm.jpeg)
![](https://static.mianbaoban-assets.eet-china.com/2020/11/NbInie.jpeg)
![](https://static.mianbaoban-assets.eet-china.com/2020/11/Zrqm6b.jpeg)
0100,数据读入寄存器0001,数据与寄存器相加,结果保存到寄存器 0010,寄存器数据向左位移一位(乘2)
为什么这么设计呢,刚才也说了,我们可以为每个模块设计一个激活针脚。然后我们可以分别用指令输入的第二第三第四个针脚连接寄存器,加法器和位移器的激活针脚。 这样我们输入0100这个指令的时候,寄存器输入被激活,其他模块都是0没有激活,数据就存入寄存器了。 同理,如果我们输入0001这个指令,则加法器开始工作,我们就可以执行相加这个操作了。
这里就可以简单回答这个问题的第一个小问题了: 那CPU是为什么能看懂这些二级制的数呢? 为什么CPU能看懂,因为CPU里面的线就是这么接的呗。你输入一个二进制数,就像开关一样激活CPU里面若干个指定的模块以及改变这些模块的连通方式,最终得出结果。
![](https://static.mianbaoban-assets.eet-china.com/2020/11/vAJjMz.png)
0100 0001 ;寄存器存入1 0001 0100 ;寄存器的数字加4 0010 0000 ;乘2 0001 0011 ;再加3
太棒了,靠这台计算机我们应该可以打败所有的幼儿园小朋友,称霸大班了。而且现在我们用的是4位的,如果换成8位的CPU完全可以吊打低年级小学生了! 实际上用程序控制CPU是个挺高级的想法,在此之前,计算机(器)的CPU都是单独设计的。 1969年一家日本公司BUSICOM想搞程控的计算器,而负责设计CPU的美国公司也觉得每次都重新设计CPU是个挺傻X的事,于是双方一拍即合,于1970年推出一种划时代的产品,世界上第一款微处理器4004。 这个架构改变了世界,那家负责设计CPU的美国公司也一步一步成为了业界巨头。哦对了,它叫Intel,对,就是噔噔噔噔的那个。
我们把刚才的程序整理一下,
01000001000101000010000000010011
你来把它输入CPU,我去准备一下去幼儿园大班踢馆的工作。 神马?等我们输完了人家小朋友掰手指都能算出来了?? 没办法机器语言就是这么反人类。哦,忘记说了,这种只有01组成的语言被称之为机器语言(机器码),是CPU唯一可以理解的语言。不过你把机器语言让人读,绝对一秒变典韦,这谁也受不了。 所以我们还是改进一下吧。不过话虽这么讲,也就往前个30年,直接输入01也是个挺普遍的事情。 于是我们把我们机器语言写成的程序
0100 0001 ;寄存器存入10001 0100 ;寄存器的数字加40010 0000 ;乘20001 0011 ;再加三
改写成
MOV 1 ;寄存器存入1ADD 4 ;寄存器的数字加4SHL 0 ;乘2(介于我们设计的乘法器暂时只能乘2,这个0是占位的)ADD 3 ;再加三
是不是容易读多了?这就叫汇编语言。 汇编语言的好处在于它和机器语言一一对应。 也就是我们写的汇编可以完美的改写成机器语言,直接指挥cpu,进行底层开发;我们也可以把内存中的数据dump出来,以汇编语言的形式展示出来,方便调试和debug。
![](https://static.mianbaoban-assets.eet-china.com/2020/11/fMB3Ij.png)
a=(1+4)*2+3;
当然这样计算机是不认识的,我们要把它翻译成计算机认识的形式,这个过程叫编译,用来做这个事的东西叫编译器。 具体怎么把高级语言弄成汇编语言/机器语言的,一本书都写不完,我们就举个简单的例子。 我们把
(1+4)*2+3
转换成
1,4,+,2,*,3,+
这种写法叫后缀表示法,也成为逆波兰表示法。相对的,我们平常用的表示法叫中缀表示法,也就是符号方中间,比如1+4。而后缀表示法则写成1,4,+。 转换成这种写法的好处是没有先乘除后加减的影响,也没有括号了,直接算就行了。 具体怎么转换的可以找本讲编译原理的书看看,这里不展开讲了。 转换成这种形式之后我们就可以把它改成成汇编语言了。 从头开始处理,最开始是1,一个数字,那就存入寄存器。
MOV 1
之后是4,+,那就加一下
ADD 4
然后是2,*,那就乘一下(介于我们设计的乘法器暂时只能乘2,这个0是占位的)
SHL 0
最后是3,+,那再加一下
ADD 3
最后我们把翻译好的汇编整理一下
MOV 1ADD 4SHL 0ADD 3
再简单的转换成机器语言,就可以拿到我们设计的简单CPU上运行了。
![](https://static.mianbaoban-assets.eet-china.com/2020/11/vAJjMz.png)
0100,数据读入寄存器A0101,数据读入寄存器B (我们把汇编指令定义为MOVB)0001,数据与寄存器A相加,结果保存到寄存器A0011,数据与寄存器B相加,结果保存到寄存器B(我们把汇编指令定义为ADDB)0010,寄存器A数据向左位移一位(乘2)
最后我们可以用第一位来控制是不是进行内存操作。如果第一位为1则也不激活位移和加法器模块,然后用第三个针脚来控制是读还是写。这样就有了
1100,把寄存器B的地址数据读入寄存器A(我们把汇编指令定义为RD)1110,寄存器A的数据写到寄存器B指定的地址(我们把汇编指令定义为WR)我们加了个解码器之后,加法器的激活条件从 p4 变成了 (NOT (p1 OR p2)) AND p4 加法器的输入则由第三个针脚判断,0则为寄存器A,1为寄存器B 这就是简单的指令解码啦。 当然我们也可以选择不向下兼容,另外设计一套指令。不过放到现实世界恐怕就要出大乱子了,所以你也可以想象我们平常用的x86背了个多大的历史包袱。
![](https://static.mianbaoban-assets.eet-china.com/2020/11/fMB3Ij.png)
![](https://static.mianbaoban-assets.eet-china.com/2020/11/vAJjMz.png)
0101 1000 ; MOVB 16; 把栈底地址定义为1000
之后入栈的话,比如把数字3,4入栈 1111 0011 ; WR 03; 把3写到内存,地址为10000011 0001 ; ADDB 01; 栈地址+11111 0100 ; WR 04; 把3写到内存,地址为10010011 0001 ; ADDB 01; 栈地址+1
这样就把3,4都保存到栈里了。 出栈的话反过来
0011 1111 ; ADDB -1; 栈地址-11101 0000 ; RD 00; 把内容读入寄存器A,00是占位0011 1111 ; ADDB -1; 栈地址-11101 0000 ; RD 00; 把内容读入寄存器A,00是占位
这样就依次得到4,3两个值。 所以,入栈出栈其实就是把数据写到指定的内存位置,CPU其实不知道你是在干啥。 当然我们也可以让CPU知道。 接下来我们再改进一下,给CPU再加一个寄存器SP,并定义两个指令:一个PUSH,一个POP。动作分别是把数据写入SP的地址,然后SP=SP+1,POP的话反过来。 这样有什么好处呢?好处在于PUSH/POP这样的指令消耗特别少,速度特别快。而栈这种数据结构在各种程序里用的又特别频繁,设计成专用的指令则可以很大程度上提升效率。 当然前提是编译器知道这个指令,并且做了优化,所以同样的程序(c语言写的),编译参数不一样(打开/关闭某些特性),编译出来的东西也就不一样,在不同硬件上的运行的效率也就会不一样。 比如上古时代的mmx,今天的SSE4.2,AVX-512,给力不给力?特别给力,但你平常用的程序支不支持是另一码事,要支持怎么办?重新编译呗。 这个时候开源的优势就显示出来了,重新编译很方便。闭源的话你就要指望作者开恩啦。 本文转自知乎,作者Zign 链接:https://www.zhihu.com/question/348237008/answer/843382847