为什么CPU能看懂,因为CPU里面的线就是这么接的呗。你输入一个二进制数,就像开关一样激活CPU里面若干个指定的模块以及改变这些模块的连同方式,最终得出结果。
几个可能会被问道的问题
Q:CPU里面可能有成千上万个小模块,一个32位/64位的指令能控制那么多吗?
A:我们举例子的CPU里面只有3个模块,就直接接了。真正的CPU里会有一个解码器(decoder),把指令翻译成需要的形式。
Q:你举例子的简单CPU,如果我输入指令0011会怎么样?
A:当然是同时激活了加法器和位移器从而产生不可预料的后果,简单的说因为你使用了没有设计的指令,所以后果自负呗。(在真正的CPU上这么干大概率就是崩溃呗,当然肯定会有各种保护性的设计,死也就死当前进程)
简单来说,就是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),并且一直保持这个动作。
作者:Sadudu
链接:https://www.zhihu.com/question/348237008/answer/847081068
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
等待士兵完成后,司令员(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,就是他了,马上就给司令员拿过去。。。
持续循环。。。
本文系作者在时代Java发表,未经许可,不得转载。
如有侵权,请联系nowjava@qq.com删除。