迅维网

查看: 2886|回复: 20
打印 上一主题 下一主题

CPU 是怎么认识代码的?

[复制链接]
跳转到指定楼层
1#
发表于 2020-2-23 20:46:26 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式 来自: 北京 来自 北京

马上注册,获取阅读精华内容及下载权限

您需要 登录 才可以下载或查看,没有帐号?注册

x
就是为什么计算机能运行我们编写的代码(比如c语言,计算机为什么会运行这个东西,原理是什么)? 就目前我的理解,我们编辑的c语言最终加载到计算机的是二进制的数据,然后cpu 根据这些数据去进行相关的运算,那cpu 是为什么能看懂这些二级制的数呢? 还有就是我们编写c语言的时候,不是有全局变量,局部变量之类的吗? 那这些到最后也是二进制的数字,这写变量的入栈出栈,对于cpu而言,是讲这些 变量放入 硬件的堆栈吗?

2#
发表于 2020-2-23 20:46:53 | 只看该作者 来自: 北京 来自 北京
又是读个大学就能懂系列的。
行吧,老规矩,尽量简单的语言来解释一下。
先说一下半导体,啥叫半导体?就是介于导体和绝缘体中间的一种东西,比如二极管。
电流可以从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)。
这几种门都可以用二极管或者三极管做出来,具体怎么做就不演示了,有兴趣的童鞋可以自己试试。当然实际并不是用二极管三极管做的,因为它们太费电了。实际是用场效应管(也叫MOS管)做的。
然后我们就可以用门电路来做CPU了。当然做CPU还是挺难的,我们先从简单的开始:加法器。
加法器顾名思义,就是一种用来算加法的电路,最简单的就是下面这种。
AB只能输入0或者1,也就是这个加法器能算0+0,1+0或者1+1。
输出端S是结果,而C则代表是不是发生进位了,二进制1+1=10嘛。这个时候C=1,S=0
费了大半天的力气,算个1+1是不是特别有成就感?
那再进一步算个1+2吧(二进制01+10),然后我们就发现了一个新的问题:第二位需要处理第一位有可能进位的问题,所以我们还得设计一个全加法器。
每次都这么画实在太麻烦了,我们简化一下
也就是有3个输入2个输出,分别输入要相加的两个数和上一位的进位,然后输入结果和是否进位。
然后我们把这个全加法器串起来
我们就有了一个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先过位移模块,再进加法器就可以了。
啥????你说啥???你的意思是我改个程序还得重新接线?
所以你以为呢?编程就是把线来回插啊。
惊喜不惊喜?意外不意外?
早期的计算机就是这样编程的,几分钟就算完了但插线好几天。而且插线是个细致且需要耐心的工作,所以那个时候的程序员都是清一色的漂亮女孩子,穿制服的那种,就像照片上这样。是不是有种生不逢时的感觉?
虽然和美女作伴是个快乐的事,但插线也是个累死人的工作。所以我们需要改进一下,让CPU可以根据指令来相加或者乘2。
这里再引入两个模块,一个叫flip-flop,简称FF,中文好像叫触发器。
这个模块的作用是存储1bit数据。比如上面这个RS型的FF,R是Reset,输入1则清零。S是Set,输入1则保存1。RS都输入0的时候,会一直输出刚才保存的内容。
我们用FF来保存计算的中间数据(也可以是中间状态或者别的什么),1bit肯定是不够的,不过我们可以并联嘛,用4个或者8个来保存4位或者8位数据。这种我们称之为寄存器(Register)。
另外一个叫MUX,中文叫选择器。
这个就简单了,sel输入0则输出i0的数据,i0是什么就输出什么,01皆可。同理sel如果输入1则输出i1的数据。当然选择器可以做的很长,比如这种四进一出的
具体原理不细说了,其实看看逻辑图琢磨一下就懂了,知道有这个东西就行了。
有这个东西我们就可以给加法器和乘2模块(位移)设计一个激活针脚。
这个激活针脚输入1则激活这个模块,输入0则不激活。这样我们就可以控制数据是流入加法器还是位移模块了。


于是我们给CPU先设计8个输入针脚,4位指令,4位数据。
我们再设计3个指令:
0100,数据读入寄存器
0001,数据与寄存器相加,结果保存到寄存器
0010,寄存器数据向左位移一位(乘2)
为什么这么设计呢,刚才也说了,我们可以为每个模块设计一个激活针脚。然后我们可以分别用指令输入的第二第三第四个针脚连接寄存器,加法器和位移器的激活针脚。
这样我们输入0100这个指令的时候,寄存器输入被激活,其他模块都是0没有激活,数据就存入寄存器了。同理,如果我们输入0001这个指令,则加法器开始工作,我们就可以执行相加这个操作了。
这里就可以简单回答这个问题的第一个小问题了:
那cpu 是为什么能看懂这些二级制的数呢?
为什么CPU能看懂,因为CPU里面的线就是这么接的呗。你输入一个二进制数,就像开关一样激活CPU里面若干个指定的模块以及改变这些模块的连同方式,最终得出结果。
几个可能会被问道的问题
Q:CPU里面可能有成千上万个小模块,一个32位/64位的指令能控制那么多吗?
A:我们举例子的CPU里面只有3个模块,就直接接了。真正的CPU里会有一个解码器(decoder),把指令翻译成需要的形式。
Q:你举例子的简单CPU,如果我输入指令0011会怎么样?
A:当然是同时激活了加法器和位移器从而产生不可预料的后果,简单的说因为你使用了没有设计的指令,所以后果自负呗。(在真正的CPU上这么干大概率就是崩溃呗,当然肯定会有各种保护性的设计,死也就死当前进程)
细心的小伙伴可能发现一个问题:你设计的指令
【0001,数据与寄存器相加,结果保存到寄存器】
这个一步做不出来吧?毕竟还有一个回写的过程,实际上确实是这样。我们设计的简易CPU执行一个指令差不多得三步,读取指令,执行指令,写寄存器。
经典的RISC设计则是分5步:读取指令(IF),解码指令(ID),执行指令(EX),内存操作(MEM),写寄存器(WB)。我们平常用的x86的CPU有的指令可能要分将近20个步骤。
你可以理解有这么一个开关,我们啪的按一下,CPU就走一步,你按的越快CPU就走的越快。咦?听说你有个想法?少年,你这个想法很危险啊,姑且不说你有没有麒麟臂,能不能按那么快(现代的CPU也就2GHz多,大概也就一秒按个20亿下左右吧)
就算你能按那么快,虽然速度是上去了,但功耗会大大增加,发热上升稳定性下降。江湖上确实有这种玩法,名曰超频,不过新手不推荐你尝试哈。
那CPU怎么知道自己走到哪一步了呢?前面不是介绍了FF么,这个不光可以用来存中间数据,也可以用来存中间状态,也就是走到哪了。
具体的设计涉及到FSM(finite-state machine),也就是有限状态机理论,以及怎么用FF实装。这个也是很重要的一块,考试必考哈,只不过跟题目关系不大,这里就不展开讲了。
我们再继续刚才的讲,现在我们有3个指令了。我们来试试算个(1+4)X2+3吧。
0100 0001 ;寄存器存入1
0001 0100 ;寄存器的数字加4
0010 0000 ;乘2
0001 0011 ;再加三太棒了,靠这台计算机我们应该可以打败所有的幼儿园小朋友,称霸大班了。而且现在我们用的是4位的,如果换成8位的CPU完全可以吊打低年级小学生了!
实际上用程序控制CPU是个挺高级的想法,再此之前计算机(器)的CPU都是单独设计的。
1969年一家日本公司BUSICOM想搞程控的计算器,而负责设计CPU的美国公司也觉得每次都重新设计CPU是个挺傻X的事,于是双方一拍即合,于1970年推出一种划时代的产品,世界上第一款微处理器4004。
这个架构改变了世界,那家负责设计CPU的美国公司也一步一步成为了业界巨头。哦对了,它叫Intel,对,就是噔噔噔噔的那个。
我们把刚才的程序整理一下,
01000001000101000010000000010011你来把它输入CPU,我去准备一下去幼儿园大班踢馆的工作。
神马?等我们输完了人家小朋友掰手指都能算出来了??
没办法机器语言就是这么反人类。哦,忘记说了,这种只有01组成的语言被称之为机器语言(机器码),是CPU唯一可以理解的语言。不过你把机器语言让人读,绝对一秒变典韦,这谁也受不了。
所以我们还是改进一下吧。不过话虽这么讲,也就往前个30年,直接输入01也是个挺普遍的事情。
于是我们把我们机器语言写成的程序
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。
汇编语言极大的增强了机器语言的可读性和开发效率,但对于人类来说也依然是太晦涩了,于是我们又发明了高级语言,以近似于人类的语法来表现数据结构和算法。
比如很多语言都可以这么写:
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  1
ADD  4
SHL  0
ADD  3再简单的转换成机器语言,就可以拿到我们设计的简单CPU上运行了
其实到了这一步,应该把这个问题都讲清楚了:C语言写出来的东西是怎么翻译成二进制的,电脑又是怎么运行这个二进制的。
只不过题主最后还提到栈和硬件的关系,这里就再多说几句。
其实栈是一种数据结构,跟CPU无关。只不过栈这个数据结构实在太常用了,以至于CPU会针对性的进行优化。为了能让我们的CPU也能用栈,我们给它增加几个组件。
第一,增加一组寄存器。现在有两组寄存器了,我们分别成为A和B。
第二,增加两个指令,RDA/RDB和WRA/WRB,分别为把指定内存地址的数据读到寄存器A/B,和把寄存器A/B的内容写到指定地址。
顺便再说下内存,内存有个地址总线,有个数据总线。比如你要把1100这个数字存到0011这个地址,就把1100接到数据总线,0011接到地址总线,都准备好了啪嚓一按开关(对,就是我们前面提到的那个开关),就算是存进去了。
什么叫DDR内存呢,就是你按这个开关的时候存进去一个数字,抬起来之前你把地址和数据都更新一下,然后一松手,啪!又进去一个。也就是正常的内存你按一下进去1个数据,现在你按一下进去俩数据,这就叫双倍速率(Double Data Rate,简称DDR)
加了这几个命令之后我们发现按原来的设计,CPU每个指令针脚控制一个模块的方式的话针脚不够用了。所以我们就需要加一个解码器了(decoder)。
于是我们选择用第二个位作为是否选择寄存器的针脚。如果为0,则第三第四位可以正常激活位移器和加法器;如果为1则只激活寄存器而不激活位移和加法器,然后用第四位来决定是寄存器A还是B
这样变成了
0100,数据读入寄存器A
0101,数据读入寄存器B (我们把汇编指令定义为MOVB)
0001,数据与寄存器A相加,结果保存到寄存器A
0011,数据与寄存器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背了个多大的历史包袱。
这个时候我们用栈的话,先栈地址初始化
0101 1000 ; MOVB 16; 把栈底地址定义为1000之后入栈的话,比如把数字3,4入栈
1111 0011 ; WR   03; 把3写到内存,地址为1000
0011 0001 ; ADDB 01; 栈地址+1
1111 0100 ; WR   04; 把3写到内存,地址为1001
0011 0001 ; ADDB 01; 栈地址+1 这样就把3,4都保存到栈里了。
出栈的话反过来
0011 1111 ; ADDB -1; 栈地址-1
1101 0000 ; RD   00; 把内容读入寄存器A,00是占位
0011 1111 ; ADDB -1; 栈地址-1
1101 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,给力不给力?特别给力,但你平常用的程序支不支持是另一码事,要支持怎么办?重新编译呗。
这个时候开源的优势就显示出来了,重新编译很方便。闭源的话你就要指望作者开恩啦。
结语:
多谢大家捧场,断断续续更了一周,终于算是更完了。
对于大多数人来说,电脑就是个黑箱,我们很难理解它到底是怎用工作的。这个问题又很难一句两句解释清楚,因为它是一环扣一环的,每一环都很抽象,每一环都是基础值俩个学分,展开了讲没上限的那种。
这就导致了即使是系统学过计算机的人也不见得就有一个明确而清晰的思路。
所以借着这个机会,想用尽量短的篇幅和尽量简单的语言把这个事从头到位解释了一下,希望能给大家解答一些疑惑。结果写完之后发现也还是写了长长的一篇。能读到底的都是猛士,再次谢谢大家捧场。
最后,行文匆忙,接下来会把之前的各种小错改一改,有什么问题也可以在评论里提出来哈。

回复 支持 反对

使用道具 举报

3#
发表于 2020-2-23 20:47:21 | 只看该作者 来自: 北京 来自 北京
谢邀。现代CPU是如何认识代码的?这个基本的知识实际上知道的人却十分少。其他答案都举了加法器等等逻辑运算单元做例子,但实际上它只是CPU decode的后端的末尾部分。真正复杂的逻辑在前端和后端发射,pipeline等部分。今天我们就从最新的Icelake的微结构Sunny Cove
讲起,看看指令怎么被分解到运算单元去的。
准备工作

现代高级语言要被编译器编译成CPU认识的机器代码,才能够被CPU所识别并执行;而汇编语言只是被发明出来方便人类解读和产生机器代码的,这些基本知识我就不介绍了。一组机器代码和相应汇编语言的例子:
这些指令和数据流在被识别(decode)之前会被分别放入一级cache(L1 Cache)中,指令放在指令L1 Cache中,数据放在数据L1 Cache中。注意L1 Cache是唯一指令和数据单独分割的Cache。然后,指令Cache中的指令会进入解码器decode,那里才是神奇的开始:
微码/微指令

现代CPU的指令解码器(Instruction Decode Unit ,IDU)大致分成两种:硬件指令解码器和微码指令解码器。
硬件指令解码器是完全由硬件连线(hardwired)完成的机器代码解码。它是最原始的解码器,由有限状态机驱动,解码速度十分快。它现在还在很多精简指令CPU(RISC)中发挥作用。
我们现在普遍使用的电脑X86 CPU,采用的是复杂指令集(CISC),指令很多,而且长短不一。如果所有的指令全部采用硬件解码,那将是一个不可能完成的任务。所以一条机器指令,将被拆解成数个类似RISC的精简微操作:微码,Micro-Ops。而这些Micro-Ops,则可以完全被硬件执行。如下面这个例子:
那么每个X86指令会被分解成多少个micro-ops呢?这和该指令的复杂程度相关,很简单的指令甚至只有一个micro-ops,一般3个左右,复杂的可以4个以上。
这么做除了能化繁为简外,它的输出Micro-Ops作为可以执行的最小单位,可以被调度入Pipeline中来提高指令的并行性
然后才会进入ALU,IMUL等等逻辑运算单元。它们基本是由逻辑门搭出来的。
其他

这么神奇的Micro-Ops,我们是不是不会碰到它呢?实际上,很多人都听过它并还在每次重启都更新过它!!
是的,你没有看错,它的另一个名字就是Microcode,Kabylake等等CPU的Meltdown问题就是用它来修正的。微指令的解码是从CPU中的一个ROM中得到翻译过的微指令的:
而CPU中出了Bug等等问题,它还有一块RAM来patch这个Rom:
既然是RAM,这就是为什么Microcode的Patch每次重启都要重新打上去的原因。这个Patch在指令decode这么底层来进行干预,所以才能修正很多有时候需要重新修改硬件才能解决的问题
后记

一条机器指令,经过重重解码,才会流到逻辑运算单元。而这个decode的过程,让曾经泾渭分明的RISC和CISC两种CPU架构的界限变得模糊了起来。RISC CPU加入了越来越多的指令,很多CPU也不再仅仅是硬件指令解码,而对部分指令采取了微码解码方式。而CISC CPU因为加入了Micro-Ops,而在decode后端显示出了RISC的特性。


回复 支持 反对

使用道具 举报

4#
发表于 2020-2-23 20:47:43 | 只看该作者 来自: 北京朝阳 来自 北京朝阳
这个问题好复杂的,,,大学中需要学好几门课才能完整回答这个问题。我在这大致提几个相关的知识点,零零碎碎地也算是回答以下这个问题。(我也没有这个能力把这个问题完整而又体系化地解释清楚)
    CPU不能直接识别并运行高级语言(比如C语言)写成的代码。这里的“直接”就是不依靠专门的软件就能完成的意思。任何高级语言代码必须经过某个复杂的软件(编译器、解释器)的处理才能运行。CPU能够直接识别的程序是由一条一条指令组成的。每一条指令都对应一个CPU的一个基本操作,这些操作实际上都很基础,比如更改某个寄存器的值、让两个寄存器中的数字相加。指令在不同CPU上的结构不一样,有时候不同CPU之间没办法互相识别对方的指令。但高级语言不存在这个问题,高级语言的代码并不直接操纵CPU。对于指令,通过纯粹的电路是可以完成识别的,毕竟它在结构上比较简单,而且也不太需要考虑上下文。不过似乎也可以将指令进一步翻译成微指令、微程序,这一部分我还没学完,不敢乱说,不过微指令的识别肯定是靠纯电路完成的。机器语言(CPU可以直接识别的语言)不存在“变量”这个概念,它只能操作内存和寄存器(CPU内部的几个暂存电路,数量很有限)。变量这个概念在实现的时候通常是将之对应于某个寄存器或者某一片内存。CPU提供了一系列指令来方便程序员维护一个叫“栈”的数据结构。栈位于内存当中,栈顶和栈底都保存在特殊的寄存器当中,CPU可以随时将数据压栈或者出栈。这个“数据”的含义实际上比较宽泛,它可以是一个数字、字符,也可以是CPU的运行状态。CPU可以将某一刻的运行状态压栈,然后跳转到其他地方执行一段程序,然后出栈恢复之前的执行状态,这就实现了函数的调用。没错,CPU也不太认识什么叫做“函数”,不过将运行状态压栈以及恢复运行状态都有专门的指令,一般就把它们成为”子程序调用指令“和“返回指令”。对于“局部变量”,CPU连“变量”都不认识,所以局部变量什么的也不存在了。不过这玩意可以依靠栈来实现,通过压栈就可以随时分配一个内存,出栈就可以认为这个内存区域不再被使用。配合刚才说过的“函数”调用的实现方法,这种依靠压栈分配出的内存一定只能被当前正在执行的函数使用。因为函数调用前,这片内存还没有被分配,返回后这片内存也不再使用了。

回复 支持 反对

使用道具 举报

5#
发表于 2020-2-23 20:47:50 | 只看该作者 来自: 四川 来自 四川
简单来说,就是C语言先转换成汇编语言(机器码),然后再把机器码加载到FLASH中,接着CPU按照FLASH的指令完成相应动作。
篇幅可能比较长,涉及到从C语言到汇编,从汇编到十六进制,从十六进制到机器码,从头到尾来举例说明,我们写的程序,如C语言,是如何一步步到达CPU那里进行执行。
这里有几个前提:
    下面做的测试,都是基于C语言。为什么要用C语言,因为C语言是高级语言里面最接近底层的语言,如果一上来就汇编的话可能就比较难理解了。讲解用的CPU,是由比较古老的8086系列发展来的51单片机,不涉及操作系统层,方便直观的讲解从代码到CPU的过程。51单片机就是这个东西,简称MCU,里面集成了RAM,ROM,CPU等,是一个小型的计算单元。我们日常所用的很多功能性电子设备里面的处理器都是MCU,小到一个电动牙刷,大到无人机,里面的控制单元基本上都是MCU来完成的。MCU不同于现代的CPU如Intel 9700K , AMD3600X等,MCU通常只执行单一的程序,用来完成特定的任务。比如电动牙刷上面的MCU,可以通过按按键开控制电机开关,控制电机的频率等等。
89C52单片机
OK,知识铺垫完,接下来我们就来看,C语言是如何一步步到达CPU里面执行的。其中汇编部分可能比较晦涩难懂,但是我尽量用浅显的语言来表述。
1、C语言
C语言是个伟大的发明,让人们脱离了晦涩难懂的汇编时代。。。
我们先来看一段大家所熟知的C语言程序。
这里的代码非常简单,就是让变量a一直增加,一直做这样的循环。
写完这段代码之后,这段代码是不能直接执行的。那么如何才能让CPU读懂并执行这些代码呢?这里就需要一个编译器来进行一个转换,把C语言转换成机器码。像gcc编译器,工作就是把语言转换成可执行的指令码。
这里我们用Keil IDE来对这段代码进行编译。编译后会生成一个hex文件,MCU可以读取HEX文件执行指令。为了更好的了解CPU读取hex的过程,首先我们来了解一下汇编语言。
2、汇编语言
汇编语言是个伟大的发明,让编程脱离了最底层的机器码编程。。。
这里我们来看C语言是如何转换成汇编的。
通过编译器,我们可以直观的看到C语言的汇编翻译。注意到图片中圈出来的一行代码:
0x000C:        02000F   LJMP  main(C:000F)
这行代码,对应的就是C语言里面的main函数。LJMP是51单片机的汇编指令,意思是 Long jump,我们暂且不管CPU是怎么认识LJMP的,下面会详细讲。我们现在只需要知道,这句话就是告诉CPU,你该去往main里面干活了。
那么main在哪里呢?注意看代码的最后,后面有个括号,里面是(C:000F)这个000F是什么意思?会不会跟这个代码有关?
注意看这行代码的开头,是0x000C,0x代表是十六进制,000C是什么呢?跟000F似乎很像,会不会有什么联系?为什么main后面会有会有000F呢?
我们接着往下看,他又出现了!
事情似乎简单了起来,原来如此,这里的000F和下文是相呼应的。是的,在汇编里面,LJMP代表的是长转移,后面紧跟一个16bit的地址,CPU读取到这条指令后,就会控制PC(program counter)程序指针,寻找这个16bit的地址。然后从这个地址开始执行代码。
???PC指针是什么?这个暂且先不管,你可以理解PC是CPU的情报员,而CPU是司令官,PC从情报局去取情报给司令员看,然后司令员根据情报指令来执行任务。而这里的情报局,就是ROM啦。0x000F是ROM的地址,你可以理解为情报局的不同取货号。这里看不懂也没关系下面还会讲,我们继续往下看。
现在我们似乎知道了C语言和汇编是有某种对应关系的,我们继续往下看汇编代码。
在C语言里面,我们初始化了一个int类型的变量a,并且把a给赋值位为0了。我们来看对应的汇编代码。
C:0x000F    E4       CLR      A
C:0x0010    FF       MOV      R7,A
C:0x0011    FE       MOV      R6,A暂且不管前面的地址0x000F 和后面的 F4是什么,我们只看汇编指令。
首先是CLR A,CLR(clear)和LJMP一样,也是汇编指令,当CPU读取到这条指令之后,就知道要把A给清零0了。A是51单片机里面的累加器,是一个特殊的寄存器,通常作为一个中介来传递变量。
接下来是MOV   R7,A,MOV(move)是汇编指令里面的转移指令,这句话等价与C语言中的R7=A,就是把A里面的值赋值给了R7寄存器。
这个变量是个16bit的变量,而51单片机是8bit的单片机,每个寄存器只能储存8bit的变量,所以这里我们需要用两个寄存器来储存这个16bit的变量,所以可以看到后面的R6也是用来进行储存的,这两个寄存器一个储存高八位,一个储存低八位,加起来就是一个完整的16bit变量。
那么,我们来尝试把a的值改变一下?
这里可以看出来,R6是用来储存16进制的高8bit,R7用来储存16进制的低8bit,两个寄存器同时占用来表示一个16进制的变量。
接下来我们来看加法操作,在C语言里面,我们简单的用一个a++就可以完成对a的自加,那么在汇编里面是怎么实现的呢?
来看INC R7,INC(increase)是一条加法指令,可以让R7寄存器的值+1。
CJNE(Compare Jump Not Equal)是一个比较+转移指令。
CJNE R7,#0x00,C:0018 , 首先把R7和0x00做比较,当不相等的时候,就转移到0x0018这个地址,于是开始运行这个指令。
而0x0018的指令,就是SJMP C:0013,SJMP(short jump)是一条跳转指令,与LJMP相似,区别就是跳转的地址范围不同。当指令到这条指令的时候呢,程序就跳转到0x0013这里。
于是再次增加R7,重复此循环。
那么什么时候停止此循环呢?答案就是当R7溢出的时候,也就是R7加到了255,对应的十六进制是0xFF的时候,再加一次R7溢出,于是R7变成了0x00,CJNE指令不成立,程序继续执行INC R6,就完成了十六进制的进位。
那么怎么用R6和R7表示C语言里面的a呢?很简单,通俗一点的话,就是
a = R6*256+R7,或者a = R6<<8|R7
那么问题来了,a一直加下去,总有加到65535(0xFFFF)的时候,那怎么办呢?没办法,你C语言里面就没做处理,汇编自然也不管了啊,加到头就溢出,从0开始呗。
OK,到此为止,我们举了个简单的例子,分析了整个从C语言到汇编指令的转换过程,并且这里以一个简单的例子来说明了一下C语言与汇编的对应关系,实际的操作过程中,这一步我们一般是看不到的,是编译器自动为我们进行了转换的。
看到这,能明白汇编语言是什么,其实你已经明白了C语言是如何到达CPU的大致步骤,接下来就是捅破那层窗户纸的时候了。
3:机器码
机器码真是个伟大的发明,能够让人们对CPU进行动态的规划编程,而不用为每一个任务单独设计一个电路。
机器码是什么呢?机器码就是用来直接喂给CPU的一种指令集合。是人们约定好的一种协议,在设计CPU的时候就已经规定好了。
我们玩游戏的时候,通常会用WASD来控制任务的前左后右四个方向的行动,OK,想想一下在你手里右一台电报机,并且你知道对应的WASD指令如下:
· - -
· -
· · ·
- · ·
那么当你收到一个  · - -  指令的时候,你就知道这代表的是W,那么就应该往前走了。
其实CPU也是一样,当他接到的不同的机器码的时候,他知道自己要干嘛。如果你要问他为什么知道,那是设计的时候就这样设计的,比如我用0000 0000代表A寄存器+1,我用0000 0001代表R0寄存器+1,我用0000 0010指令代表LJMP等等,这都是规定好的指令,然后通过设计这样的硬件,就是CPU了。CPU的工作就是机械化翻译这些指令,然后根据指令控制相关的寄存器动作。
在51单片机里面,不同汇编代码是有不同的对应的机器码的,他们之间有一个对应表,如下图。
再来看,我们的汇编程序,是不是之前有个东西一直被忽视了呢?
没错,这里的十六进制码就是机器码!比如,我们用0x0F来代表 R7寄存器+1(INC R7),用0x0E来代表R6寄存器+1,用BF来代表(CJNE),后面跟的00代表要比较的数值,再后面的01代表条件程里要跳转到那里去。。。那你再来看下面这个码表,有没有什么新发现?
似乎是一种柳暗花明又一村的感觉。。。。。。
我们回到刚刚的例子,我们把CPU比喻成司令员,把寄存器(一部分是RAM的寄存器)比喻成士兵,把PC指针比喻成情报员,把程序储存器ROM比喻成有顺序的情报。
再来看这个程序
那么整个工作流程就是,情报员(PC)去从上往下依次按照号码取情报(ROM里面存放的机器码),然后递交给司令官(CPU),司令官(CPU)一次处理一条情报0x0F(INC R7),司令员一看就明白怎么回事,立即让代号R7士兵(R7寄存器)的8个手指(8bit的寄存器,只能处理8bit数据)代表的数+1,然后R7士兵绝对服从,从 00010011(19)(假设的)变成了00010100(20),并且一直保持这个动作。

                               
登录/注册后看高清大图

二进制寄存器 +1
https://www.zhihu.com/video/1164215686535782400
等待士兵完成后,司令员(CPU)继续来处理PC指针取到的下一条情报(CJNE R7,#0x00,C:0018),司令员(CPU)看了下情报BF,代号CJNE,哟,这条指令不简单,这是要让R7士兵的值(R7寄存器)与某个数进行比较啊,于是又看到了下面的00,哟,这原来要比较的是00啊,再继续让下看01,是要往下跳01个地址啊。OK,这次你们俩不相等,就跳过后面的1条指令(INC R6被调过去了),告诉情报员(PC指针),跳过下一条情报,直接看后面一条。于是情报员乖乖的跳过了下一条指令,取到了0x0018地址的指令给司令员,司令员(CPU)看到80,哦,这是要让情报员(PC指针)再跳到别的地方取指令。跳到哪里呢?继续看到F9,司令员眉头一皱,这是个补码(最高位是1),稍微思考,OK,这个数的-5,原来是要往前跳转5个地址,随即命令情报员(PC指针)往前跳5个地址。情报员回到0x0018,开始往回找,数了五次之后,数到了0x0013这里(INC R7),OK,就是他了,马上就给司令员拿过去。。。
持续循环。。。
一直加到某一次,R7士兵的8个手指不够用了,怎么办,只能溢出,于是从头开始,变成了0x00。司令员(CPU)某头一皱,终于相等了,马上通知情报员,这次不用跳转了,直接拿下一条指令就行。于是情报员拿到了0x0017地址的情报,司令员一看是0E,马上命令R6士兵(R6寄存器)加1。。。。。。
这就是整个CPU和周围的寄存器协调工作的流程,看懂可能需要一些基础知识,看不懂可能就要多看几遍了。。。
看到这,似乎已经明白了C语言到CPU的过程了。但是对于MCU来说,中间还缺少了一部分。
4.机器码->FLASH
我们从把C语言->汇编语言->机器码这个过程弄明白后,还有一个过程,就是把机器码加载到MCU的ROM中,这个ROM如果是桌面上的CPU如intel 或者AMD的cpu的话,这个ROM可能就是CPU的缓存了。
在得到机器码之后,我们要把机器码转换成一个hex文件,才能够下载到MCU的ROM中,而这个HEX文件记录的就是整个代码的机器码,被顺序的储存在单片机的ROM中如本例,我们来看一下对应的HEX文件。
汇编语言
通过编译,我们可以得到以上的hex文件。
HEX文件,是Intel公司提出的按地址排列的数据信息,从上面的内容可以看出来,hex文件是十六进制的字符串,大家有兴趣的可以看一下这篇博客,详细讲解了hex文件的构造。
单片机HEX文件完全解读 - Craftor - 与非博客 - 与非网可以看到第三行,从第9个字符开始,就是机器码了。
通过烧录器,可以把这些机器码烧写在单片机的FLASH里面,然后CPU就可以愉快的指挥各个寄存器干活啦!
5. 机器码->数字电路
有空的话就补充一下把,这部分太难懂了,都是数字电路里面的基础电路,讲起来也没什么意思。。。
关于这一部分,还可以参考另外一位高赞答主。


本文是用了一个比较古老的CPU处理器来为例讲解的,并且以一个简单的C语言例子分析了整个过程,至于题主提问到的出栈,入栈等等,需要深入的了解到CPU外围的寄存器分布,汇编层面的PUSH,POP等,较为复杂。
本回答为抛砖引玉,更深的知识需要深入的研究汇编语言以及CPU和外部的设备通信。

回复 支持 反对

使用道具 举报

6#
发表于 2020-2-23 20:48:02 | 只看该作者 来自: 北京 来自 北京
CPU是不认识程序员写的代码的,这需要编译器当中间的翻译官,在说方舟编译器的时候说过这些,这里更系统的说一下,会涉及到的计算机课程包括《计算机组成原理》、《电子电路》、《汇编语言》、《编译原理》、《C/C++/JAVA 程序设计》等。
某种意义上来说算盘就是一种早期的计算机,算盘计算需要的是什么?数据和操作,比如“1+1”这个运算“1”就是数据,“+”就是对数据进行的操作。经过这么多年的发展,我们的电脑可以干很多很多的事情了,但是本质上还是在做类似“1+1”的操作。
目前所接触到的计算机都是以冯·诺依曼体系结构为主,结构如下:
简单说就是CPU从外面取数据,然后在内部通过运算器做运算,最终将计算结果输出,控制器则是对程序规定的控制信息进行分析,控制并协调输入,输出操作或内存访问。
CPU是一颗数字电路芯片(还有模拟电路芯片),他是怎么制造出来的呢?可以参考Intel制作的从沙子到芯片的短片,如下:


https://www.zhihu.com/video/1163536515015139328
CPU上面主要是晶体管电路,晶体管只有两个状体:高电平和低电平,在计算机中用0(false)和1(true)表示,通过0和1能表示多少东西呢?前面我们说CPU的本质就是计算,只有两个状态如何计算“1+1”,“2+2”呢?有一个数学分支——布尔代数。布尔代数只有两个数据表示:true(1)和false(0),运算符也没有加减乘除,只有:NOT(非)、AND(与)、OR(或)(读起来好像跟闹太套很像)。
以AND(为例),只有两个都为true,结果才是true,有一个为false结果就为false,这种看似神奇的逻辑本来是证明哲学的,所以。。。
在布尔代数中,只需要通过,NOT(非)、AND(与)、OR(或)来实现运算,其实还有个常用的XOR(异或)。运算解决了,数字怎么解决,只有0和1怎么表示那么多的数字?这个大家都能想到——二进制就行了,可是程序中不仅有数字还有字母呢,怎么搞?提前编码,就是ascii表,实际上我们输入一个大写字面“A”,在里面对应的就是二进制0100 0001(十进制65);


有了布尔代数以及ascii表,计算机就可以表示所有的数字和字母(初期不支持汉字)以及符号了,下面就可以设计门电路了,CPU就是由各种门电路组成的:
到了这里可以很容易得出一个结论,CPU用0和1就可以搞定数据和运算,CPU是搞定了,对CPU来说各种0和1的组合就是它能读懂的“语言”,这就是我们所说的机器语言。0和1这种语言也是人来规定的,人当然也能读懂,但是当面对非常庞大的一堆的0和1时,任谁都会崩溃,这太反人类了。
于是汇编语言诞生了,汇编语言与机器指令是一一对应的,比如我们要计算1+1,如果直接输入机器码会是很长的一串0和1的组合,如果是汇编语言(8086)呢?大概就是下面的样子:
mov ax, 1    //寄存器ax送入值1,
mov bx, 1    //寄存器bx送入值1,
add ax, bx   // ax和 bx 寄存器值相加是不是很容易看懂了?寄存器又是什么呢?是CPU内部暂时存储指令、地址、数据的元器件,intel的64位处理器中有16个寄存器。
又说起硬件了,我们继续说软件,汇编语言有很多指令和助记符帮助程序员,所以这些程序编写起来会方便很多,那CPU是如何理解汇编器的呢?其实机器也不懂汇编语言,把汇编语言转换成都是01的机器语言需要汇编器(Assembler),其实就是汇编语言编译器,但是这个很底层,用二进制写的,机器可以读懂这个翻译。
对程序员来说,汇编语言确实比机器码方便了,但是依然会设计寄存器,地址等,想要做一个大型软件还是很麻烦,这就有了C语言等,后来又有了JAVA、python等高级需要,比如要用C语言写一个"1+1",就是下面的形式:
int x;
x = 1 + 1; 是不是更简单了,C语言一样效率不高,因为想要更好的描述世界,进行大型程序开发面向过程的语言是力不从心的,于是就有了面向对象,有了反射等机制。不管是C语言还是JAVA、python等,都是为了方便程序员更好的抽象和描述世界,描述需求。机器依然不懂,编译器因此诞生了,编译器就是把程序员用的各种高级语言翻译成处理器 才懂的机器语言。
上面我一直说的CPU都是基于Intel 的8086处理器,课本上也一直是这个,现在应该也还是,但是事实上现在已经有很多的处理器已经不是X86处理器了,ARM和X86的区别是什么呢?ISA(An instruction set architecture , 指令集架构)不同,我们还说上面最简单的A+B的描述,在不同的指令集上,他们的实现是不一样的,也就是我们平时说的架构不同,所以同一个程序最后编译到不同架构的处理器上得到的机器码也是不同的,无法移植。
那能不能跨平台呢?在IT界,加个中间层没有什么不可以的,比如JAVA就是增加了一个虚拟机,JAVA的虚拟机就是JAVA跨平台的基础。


先写这么多吧,看看有什么疑问再补充,每一部分都是一本书,如果哪里描述的有问题,欢迎讨论指正。

回复 支持 反对

使用道具 举报

7#
发表于 2020-2-23 20:48:46 | 只看该作者 来自: 广东广州 来自 广东广州
二进制数就是高低电平。其他的几位答主大都讲的很清楚了,我讲一下我的理解。从偏向于电路的角度,来讲讲CPU怎么认识二进制的数
首先,二进制数据储存在存储中,主存储器是DRAM,它由电容和一个门控开关组成,电容充满电荷时,代表存储的是1,因为如果你这时候打开门控开关,你用一个万用电表去读数,读到的就是电容板的电势,你自然读到了高电平。电容器放完电,不带电荷了,你用万用表读电压,读到的自然就是接地的电压,也就是低电平。
每个内存单元只能存1或0,换句话说就是1bit的二进制数据,内存动辄几个G,也就是有数十亿个这样的单元储存着0和1,把这些单元按矩阵排列起来。我们就可以通过特定的行数和列数,找到我们所需要的单元。打开它的门控开关,它就会向数据通路上持续的显示出高/低电平(1和0),直到处理器接受到它们。这就是内存寻址。
CPU这边就接受到了许多1和0,就是从内存传出的高低电平的信号。如何识别这些信号呢?人们就约定了一种规范,这种规范就叫做指令编码。人们在规范中,约定了指令的长度。指令的格式,这样,在设计CPU的时候,人们就会决定要有多少根信号线用来传输数据。一般情况下,人们会把指令的低位当作操作码,高位当做操作数,于是,低位的信号线就连接到了解码器。解码器一般的数字电路基础书都有,最简单的解码器是一种最小项输出电路。操作码是5位的。可以转化成32位的输出。这种输出有高有低,可以作为CPU内部各个工作电路的使能端,控制它们是否工作。高位的信号线就连接到了多路选择器,可以通过多路选择器在寄存器组中选择寄存器用来保存操作数(就是一堆1和0)。或者读取操作数。寄存器是一种能够保存高低电平的电路。它可以是锁存器,也可以是D触发器。具体原理你可以去查相关数字电路的书籍
至于CPU要怎么去执行二进制数据的运算。许多数字电路的书籍里都会介绍半加器,全加器,超乘法器,除法器等。它们能够接受高低电平的输入,并相加相减形成输出。再把他们保存到寄存器里。
下面我们举个例子。一个最简单的二进制加法指令是这样执行的,首先,CPU要取指,指令的地址保存在程序计数器PC里,这是一个寄存器,保存着指定长度的高低电平。取指时。CPU通过信号线向内存控制器发送一个高电平,表示我要访问你了,内存控制器回应一个高电平,我已经做好准备了,于是,寄存器与地址信号线相连的门控开关被打开,寄存器里的高低电平信号就通过电线抵达内存,内存控制器把它分成两部分,一部分是行地址,一部分是列地址,各自通过两个多路选择器,得到了行和列,打开找到的门控开关,于是内存里保存的高低电平被读出,通过数据信号线送到了CPU。其中低位送去解码器,高位送去多路选择器,解码器解码,输出控制信号,让加法器开始工作。多路选择器找到了寄存器,打开门控开关,里面存储的高低电平输入进入加法器,输出被写入寄存器,加法运算就完毕了。
以上只是非常简单的例子,实际的计算机系统非常复杂。指令编码也复杂得多。可以说要好几本书都讲不完的。
全局变量,局部变量这些东西,可以看看汇编语言中的调用约定,所谓栈,其实就是一段内存空间,内存是怎么保存数据和读取数据的前面已经说过。出栈,入栈,其实就是指令的一种,即push指令和pop指令,变量,也就是一串高低电平,保存在寄存器中,CPU从内存中取到push指令的时候,就会把寄存器中保存的变量写入内存中的指定地址,这个地址由栈基址寄存器和偏移量指定。

回复 支持 反对

使用道具 举报

8#
发表于 2020-2-23 20:49:30 | 只看该作者 来自: 北京 来自 北京
cpu根本不认识代码,他就是无脑执行指令,他每次从他的好兄弟内存那里拿一些数据,一部分执行,一部分就放到他的“仓库”缓冲里,执行完了接着找内存要,有时候内存也没有,他们在想办法找硬盘要,硬盘个大却动作缓慢,所以cpu和内存兄弟俩就得先干别的事等他,等准备好了给了内存在回来继续他们未完成的任务。
当然cpu也不是一味的索取,时不时的就会还给内存和硬盘一些东西,有好的有坏的,有值钱的也有垃圾,有时候搞的内存和硬盘焦头烂额,不堪负重。
cpu能力虽然强,但是却“深居内殿”,根本不知道外边在干嘛,唯一能沟通的就是内存好兄弟,他也只能根据内存提供的信息做判断。所以有时候内存也会坑cpu,让他一直原地打转,坑的惨的能把cpu坑烧了。
cpu只负责执行一条条指令,具体这些指令组合起来是什么结果、有什么作用它不关心。
真的很无脑!

回复 支持 反对

使用道具 举报

9#
发表于 2020-2-23 20:50:30 | 只看该作者 来自: 北京 来自 北京
过百更新流水线操作
在计算机中,一个很迷人的地方就是分层
越顶层,人越看得懂,越容易写,机器执行效率越低
越底层,人变得看不懂,写起来很吃力,机器执行效率越高
每一行代码都需要有这三个信息,输入从哪里来,要对输入做什么处理,输出送到哪里去
从最简单的指令,最简单的三层结构举例
顶层代码:c=a+b
先搞清楚上述的三件事,输入一是a,输入二是b,要把他们进行加法,结果放到c
那么要把他转换成底层的代码,需要知道a是存在哪里的,b是存在哪里的,c是具体哪个位置。这里假设a是第2个存储单元r2,b是第7个存储单元r7,c是第11个存储单元r11
那么我们可以将这行代码翻译成
底层代码:加法 r11 r2 r7
接下来我们要继续处理这三个问题,变成机器码,我们假设加法对应100101,r11对应1011, r2对应0010,r7对应0111
机器码:100101101100100111
那么在cpu执行到这样一条指令的时候,会把前6位100101送到执行单元的控制信号中,让执行单元执行加法,然后取出11到14位0010对应的r2存储单元的值,然后取出15到17位0111对应的r7存储单元的值,计算出来之后,送到7到10位1011对应的存储单元里面。
这样就执行完了一条指令。
总结一下在我们提出的三层结构里,我们写c=a+b ,编译器变成100101101100100111,送给cpu执行。

回复 支持 反对

使用道具 举报

10#
发表于 2020-2-23 20:50:37 | 只看该作者 来自: 北京 来自 北京
你的c语言被编译器编译成汇编语言,最后又被变成01指令。这些指令一条一条放在内存里。可以小端放可以大端放,可以高位交叉放,也可以低位交叉放,总之被安排在内存里。
然后需要告诉cpu开始的位置。由CPU里面一个叫PC的寄存器负责。PC会自动增一,保存了下一条指令的地址。每次运行完,把PC地址指的指令做下一条运行。
你的cpu出厂被设计了可以允许一些指令。比如什么RISC和CISC,每个指令有操作码和操作数。知道操作码和操作数就可以执行这个指令。比如操作码是对应加减之类,操作数可以是数本身,也可以是存数的内存地址,也可以存内存地址的内存地址,也可以是存内存地址的寄存器的地址。
CPU从开始位置一条一条指令运行假如没有跳转。控制具体运算的数据通路是由控制器CU。控制器根据时序和IR里的指令来控制每个机器周期做什么。
一般每个指令周期有被分成取指令,间址、执行、和处理中断。
下面以add (r0) + r1 -> r0为例子 具体图不画了,寄存器和内存直接有总线相连接。
一、取指令
(PC) -> MAR  送下一条指令的地址
M(MAR) -> MDR 把内存对应地址内容送到CPU里的寄存器
MDR -> IR 送到指令寄存器
IR 译码 指令寄存器送给CU译码
二、间址取r0
(r0) -> MAR 送r0中的地址给寄存器
M(MAR) -> MDR -> ACC 取到累加器
三、执行
ACC + r0 -> ALU -> ACC 通过ALU运算
ACC -> r0 保存结果到寄存器
译码部分可以通过逻辑电路,也可以通过微指令,微指令可以理解为把译码部分提前存起来用的时候直接从ROM里面读出来就知道具体逻辑。
你学玩计算机组成原理后保证就懂了机组挺有意思的。计算机的专业课会安排。我学化工的,我在车间三班倒开阀门空隙里自己看书学的。

回复 支持 反对

使用道具 举报

11#
发表于 2020-2-23 20:51:23 | 只看该作者 来自: 北京 来自 北京
如果不想看晦涩的计算机组成原理,汇编语言之类的,可以看看这本科普性质的书。
https://book.douban.com/subject/26365491/

回复 支持 反对

使用道具 举报

12#
发表于 2020-2-23 20:52:20 | 只看该作者 来自: 北京 来自 北京
(应该比较适合0基础科普的小更新)来来来,我用尽量简单的语言来解释这其中的原理(′~`;)
首先,咱们知道,二进制的的0和1,如果不限位数的话,可以无数种不同的组合对吧?
CPU实际上是各种晶体管,经过不同的设计排列组合成指令后的集合体;
咱们现在用的机器属于冯诺依曼结构计算机,这个结构的计算机,特点就是把程序和要用的信息预存储到硬盘里,而硬盘里存储的东西,其实简单来说就是以磁的形式存放了操作CPU所执行的指令顺序
注意,二进制对于机器而言可不是数字,那是信号子的排列组合
你先要操作加法啊,还是先操作减法啊等等,不同的指令根据排列组合的顺序不同依次执行,就会得出不一样的结果,但不论怎么操作,命令均为处理器里设计好的命令;
当然啦,后来的攻城狮们,觉得用二进制操作计算机好麻烦哦,就打包归类,把常用的二进制整合了一下,形成了汇编语言;
再后来,攻城狮们觉得汇编对于顶层应用的开发不够友好啊,就再一次整理归类,搞出了高级语言,比如c啊,java啊,c嘎嘎啊什么的;
还有啊,CPU里加载的可不是单纯的数据,而是你想要调用的硬件指令+需要运算的数据;(或者说是算法+数据结构也OK)
大概就是这个亚子
觉得好就赞一个呗
补充个小观点供大家想象:
代码是为了表达用户利用计算机解决事情的逻辑而存在的工具
当封装到一定高度,满足大多数用户表达需求时,开发工具图形化程度越高,开发流程才能越精简,技术门槛降低,能够参与进开发创造的人才能越多
那么咱们也可以预见,未来有一天,当不需要写代码也能够表达逻辑时,像scratch这类的工具会越来越多,关注业务逻辑的开发者们也就可以不用去在意源码,而把剩余的精力放到业务创新上了,因为逻辑清晰,不需要源码也能开发不错的应用产品了
是不是感觉未来会很好玩呢hhh?
PS:设计计算机程序算法通俗点讲,就是用数学的方法,设计出先做什么后做什么的逻辑顺序,为计算机各元件的有序工作提供思想指导。
编程实际上编写的是逻辑,而数据大多数时候一般不是人编写的,而是由自然世界或者程序规则提供的内容,譬如录音就是采集自然世界中声音的数据,录像就是用像素记录光影数据,至于鬼畜嘛,那是在用程序批量调节音视频数据咯hhh
评论区好奇宝宝问题整理分割线
1、cpu是如何从硬盘里面提取数据的呢?
答:CPU的读取就好比你摁下电灯开关,灯就会亮一样,你的手摁与不摁就是指令。
我们假设,你摁下时就是1,不摁时就是0,“亮灭”的组合规定代表意思是“你”,“亮灭亮”的意思是“好”,那么连续工作就可以表示“你好”的指令。
同理,电脑在通电后,外存储器就好比你的手一样,持续的给CPU这盏“电灯”发送由开发人员设计好的命令,如果这些命令要求CPU读取数据,CPU才会从硬盘再读取数据,就是这个亚子
2、不同的格式,比如mp3,dwg会有什么不一样呢?
答:不同的格式其实就是不同的数据结构形式呀,
这就好比外面的大厦,有的18层,有的26层,有的有车库,有的没有,但这些都叫大厦。
设计建造方式不同,自然结构不同,但是其本质是一样的,都是由二进制的排列组合构成,但是不同种格式里面的二进制数据存储的结构不同罢了
3、那么,存在电脑的上的文件,是如何存在的呢?
答:以很多个0、1构成的排列组合,存储在硬盘这种外存储器呀,不同的排列组合,就会构成不同的文件形式,最终在外存储器上以磁的排列组合形式存在

回复 支持 反对

使用道具 举报

13#
发表于 2020-2-23 20:53:13 | 只看该作者 来自: 北京大学医学部 来自 北京大学医学部
首先CPU是一种开关电路,开关电路一般有两种状态开或者关,所以二进制1和0正好可以表示两种状态控制电路,而所有的代码都可以被编译成二进制格式。
开关电路就是逻辑门电路组成,与门电路,或门电路,非门电路构成了最基本的开关电路。



逻辑电路
https://www.zhihu.com/video/1166870829694005248


计算机基本原理

回复 支持 反对

使用道具 举报

14#
发表于 2020-2-23 20:53:55 | 只看该作者 来自: 中国 来自 中国
先转为汇编代码 其实就是助记符 每段汇编对应一段二进制代码 汇编器把这段二进制代码写入到磁盘中 程序运行时由另一段程序(操作系统)把这个二进制加载到内存中 内存与cpu通过地址线和控制线 数据线(就是几十条很细的导线)与内存连接
cpu通过一个叫做ip寄存器(寄存器有很多种 你可以把寄存器看做一个速度很快的内存)的硬件读取到下一条指令在内存中的地址然后把这个地址信号通过控制线发送给内存控制器 内存控制器取出相应地址的数据 通过数据线发送给cpu
然后呢cpu就取到了一条机器指令 cpu将这个机器指令分成两段 一段是操作数 一段是操作码(当然操作码还可以分成ifun icode这两段)  cpu通过操作数操作码来控制寄存器访问内存、访问寄存器、启动cpu内置的计算相关的电路(alu)最后更新ip寄存器的内容 再通过ip寄存器发出的信号找到下一条指令并且执行 周而复始 直到程序遇到特殊的指令来结束这个程序
再来讲一点细节问题: 我们可以把内存和寄存器看做有两个个口的一个蓄水池 一个口来输入水(就是修改内存寄存器数据) 一个口来放水(输出数据)一个口 但是我们有很多的蓄水池 所以我们要加一个输入要操作的蓄水池的信号线(控制线) 另外为了同步 我们还需要加入一个时钟线 时钟就是一个按照时间间隔发出脉冲信号的硬件(好像是请通过晶振实现)比如我第一毫秒时 时钟线是高电平 下一毫秒就变成了低电平(当然 实际的时钟频率可比这个高多了)    这些蓄水池的蓄水就是通过时钟线控制的 时钟线要高电平(时钟上沿) 蓄水池的入水阀门才可以打开 也就是说 时钟下沿的时候 我的修改内存或者寄存器的这些信息就会被通通堵在输入线上
这样做有什么用?当然是同步啦 比如说我没有设置这个时钟阀门 那么我第一条指令都还没有执行完成 ip寄存器就被改变了 马上就进入下一条指令的运行 这样会造成很大的混乱 所以我设置一个时钟 让一条指令在绰绰有余的时间里执行完毕 再把阀门打开 把ip寄存器更新 并且进入下一条指令
当然这些所讲的只是cpu的很小一部分(勉强叫SEQ电路)真真的现代CPU还有很多的黑科技的 比如分之预测、流水线之类的

回复 支持 反对

使用道具 举报

15#
发表于 2020-2-23 20:54:31 | 只看该作者 来自: 北京 来自 北京
本人曾玩过一段时间的单片机(Arduino),随着对底层学习(AVR编程、汇编)的深入,我能感觉到整个机器的运行原理在我面前变得越来越清晰。事实上单片机就是一种简化的计算机,我相当明白为什么初学者不能理解计算机乃至CPU的原理,因为脱离了电路(不单是逻辑电路)空谈理论,只会让人觉得无中生有、空穴来风
其实它们是差了一个特别重要的知识点,它们没有从最基本的规则说起,那就是:
时钟频率



CPU等等电子部件是自己自由地活动着的吗?不是,它们其实都只是一种电子元件而已,受着最简单的规则支配,做着最简单、机械的事。实际上,所有部件的运行都需要一种信号,每收到一次这种信号就进行一步操作。对CPU而言,输入一次这样的信号就会让它执行一条来自内存的机器指令。
网上找的有关整个stm32单片机时钟频率的概述图
其中HSI、HSE、LSE、LSI都是振荡器,这些振荡器能产生一个固定频率的方波信号(电子脉冲),每种振荡器产生的频率都不一样。随后再经过倍频器PLLMUL将这个频率翻上2倍、3倍或者直至16倍等等的整数倍后通过选择器SW,从中选取一种需要的频率传输给CPU。最终频率达到多少,CPU就以多快的频率运行。比如输入4GHz的方波信号,CPU每秒就会执行

                               
登录/注册后看高清大图
条机器指令。这样的机制其实就相当于一个时钟一样,钟每走多少步CPU就走多少步。了解过超频的人一定知道,超频时一般都是提高倍频,这里的倍频就是倍频器对振荡信号的翻倍量。
一张华硕P8P67 Deluxe主板的图片。红圈圈出来的就是晶振——一种最主要的振荡器部件
在这样的时钟频率的支配下,CPU就得以运行起来。具体怎么运行,那就跟其他各位大佬说的一致了。如果真想具体弄清机器指令和内存等等这些底层的原理,推荐学习一下单片机和少量汇编。单片机其实就是一种小型的计算机,有助于你理解这些原理。如果一开始就接触那些晦涩难懂的整套的原理,那就跟啃干面包似的,学来不知道有什么用的感觉会让你放弃。说实话别把这些东西想得太难,从单片机起,你逐渐就能够理解整个体系。况且,我还只是一个小小的高中生_(xз」∠)_,难道这还不能说明这些东西所有人都可以弄懂吗
如果各位看官觉得这篇回答有帮助的话,就点个赞同吧!码字不易啊。。

回复 支持 反对

使用道具 举报

16#
发表于 2020-2-23 20:55:18 | 只看该作者 来自: 北京 来自 北京
计算机可以进行的操作一般由指令序列组成。指令序列就是一条又一条的指令语句。一条指令语句就是一串二进制数,可分为操作码Opcode和操作数Oprand。操作码通过处理器的控制器解码,指定算术逻辑运算器执行何种运算,操作数提供运算的参数本身或寄存器编号或内存地址指针。一条指令语句就被执行了。
指令序列中的指令一条又一条的被处理器这样执行,程序也就被处理器执行了。
用命名的名字(标识符)表示操作数字面量和寄存器编号以及内存地址指针,用关键字和运算符表示操作码表达的运算,就构成了汇编程序。全局变量和局部变量只是名字翻译成具体操作数时,考虑不同(作用域的不同)。汇编器翻译文本写成的汇编程序到二进制机器码。再按前述由处理器执行。
更进一步的,形成函数调用约定,形成一定语法的高级语言编写高级语言程序。编译器经由词法分析、语法分析、语义分析、代码生成、代码优化,将高级语言写成的文本,即高级语言程序翻译为汇编程序。再由汇编器翻译成机器码,由处理器执行。
栈一般是栈寄存器指向的,约定为后进先出的,用于临时保存数据、保存环境、函数传递参数的一段内存区域。用栈的地方很多。学了操作系统原理,你会看到有很多不同权限级的栈。
Nand2tetric讲得很清楚,建议去读。

回复 支持 反对

使用道具 举报

17#
发表于 2020-2-23 20:55:50 | 只看该作者 来自: 北京 来自 北京
cpu什么都不懂,它就是一堆元器件,有电流在里面走来走去,人懂,人把走来走去的电流抽象了,抽象成机器码,让你知道电流这么跑是干嘛,那么跑又是干嘛,然后觉得这么还是看着费劲,又抽象成汇编语言,扫一眼就知道在干嘛,不用一个个数0和1,然后再抽象成高级语言,常见的机器码序列让你一行代码就能表示了,然后再抽象成用户程序,点几下鼠标就让电脑把一堆高级语言写的代码跑了。
你要是真想了解了解,可以看看计算机组成原理和计算机体系结构的书,概念上就能理解了,要是觉得概念上理解还是虚,可以用verilog之类的语言自己编个cpu,网上应该有不少代码能参考,因为编这个是一些计算机专业的实验课。
不过现在好像不兴搞这个了,可能太硬核了,怎么造计算机没人用得上,都是研究怎么用计算机,用计算机跑个神经网络,或者争论用什么高级语言,甚至用高级语言上的什么框架

回复 支持 反对

使用道具 举报

18#
发表于 2020-2-23 20:56:33 | 只看该作者 来自: 北京 来自 北京
大家都讲的很好,不过我发现很多人都讲的很复杂,我就补充一些细节好了。
其实基础非常简单。CPU里的各种部件都有个“开关”线,比如说高电平工作,低电平就失效。然后CPU读进一个机器码以后,比如这个机器码有5位,就会被一个特殊的电路翻译到32根线去。这是因为5位有32种情况,正好一根线一种情况。然后,这32根线就被接上了CPU内部的32个设备(比如半加器全加器移位存值取值等等)的开关上去。于是,任意一个机器码,就启用了CPU的某个设备,就能进行某种运算了。之后的当代CPU的设计比这种复杂太多了,但是基本原理就是这样。
这里有个专栏文章:https://zhuanlan.zhihu.com/p/66680640
讲的比较清楚了,里面有个图你可以看一看。
要明白CPU包括计算机组成原理的最基本的逻辑是,计算机是分层的,每一层都有自己那层的逻辑。因此在对计算机的基本原理好奇之前,一定要先问自己到底想知道哪一层的知识。上面提到的是最基础的电气这一层,在电气层之上还会有其他的(规律完全不同的)抽象层,大体上有这些:
    物理电路层机器码编码层(汇编层)硬件互动协议驱动程序操作系统内核操作系统外壳应用层
所以评论里说十分之一都没说完是正确的,毕竟内容的确会很多。不过只要记住每层的逻辑都不一样,但是又有相通之处,就不会迷路了。

回复 支持 反对

使用道具 举报

19#
发表于 2020-2-23 20:57:11 | 只看该作者 来自: 云南 来自 云南
点赞最多的那个评论已经说得非常清楚
但是,可能过程有点复杂,有些人还是不明白
我们看看CPU的结构
代码最终形成的东西就是 010101010
对应就是  开关 和一些对应的数字,我们理解是数字,但是CPU理解就是一堆 0 和1 的组合。
通过这些 0 和1 的组合,就可以做很多事情
比如 加减乘除等等
这样就可以做,定时器~
中断~
等等~
操作系统就出来了~
有了操作系统,文件系统,各种外设和通讯协议,一个完整的PC就出来了。
总之是,由简单到复杂~

回复 支持 反对

使用道具 举报

20#
发表于 2020-2-23 20:57:33 | 只看该作者 来自: 北京 来自 北京
一开始的电脑是二进制打孔电脑。
在纸上打洞,通过检测透不透光,有洞代表1,无洞代表0(也可能相反定义)。现在的光盘用的是就是类似原理,存储密度更高。
2进制输入电脑后,电脑开始计算,输出到打孔机或灯泡上,亮代表1,不亮代表0,多个灯泡可以组合成LED显示屏类似的效果。现在的51单片机,显示器数据管和这个差不多,很多玩具,电子产品上有使用。
功能比现在计算器差,体积巨大。
后来发明了批处理,寄存器,内存,汇编语言,可以编译成2进制运行,再后来发明了操作系统,图形窗口。操作系统用bios引导加载程序,常驻内存和cpu,改变了人机输入输出及平台环境。
cpu有指今集,代表那些二进制cpu执行不同的命令,内部实现是靠电路焊死的,如同你的电视摇控器,你按下1就切换到1频道。
不管什么程序最终都是二进制运行,有部分是动态的,只有运行时才知道,但最终会确定下来指令。
cpu针腿输入二进制如同弹钢琴,你按不同的键输出不同的声音,连续状态下,有无限排列组合输出,实现各种复杂的输出。
你可以使用8086cpu+8259a中断控制器+8255输入输出接口芯片,写段汇编(c语言也是编译成汇编,你可以反汇编查看编译后的c语言的汇编代码),实现可以行人手动控制过马路的红绿灯,来更加清楚了解整个过程。
8086+8259+8255交通灯控制系统《微型计算机原理与接口技术》课程设计 - MCU综合技术区 单片机论坛

回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

快速回复 返回顶部 返回列表
附近
店铺
微信扫码查看附近店铺
维修
报价
扫码查看手机版报价
信号元
件查询
点位图 AI维修
助手



芯片搜索

快速回复