今天菜鸟看了一下入门篇第四讲,发现还是和提升性能有关(主要就是讲:性能提升和功耗、散热的冲突、功耗的公式和定义、一些没听说的定理),菜鸟就在想,这还是计算机组成原理吗?所以菜鸟并不打算写,直接跳过 [ 今后会写,写完这篇博客之后发现还是要了解的,并且彻底对极客时间丧失了信心,原来后面的也只是标题党,看着像,其实内容华而不实,咳咳咳,还是老话,马扁子的东西请大家谨慎购买!!! ] 。然后,菜鸟看了一下后面的,就发现了新的大陆,没错就是菜鸟所熟悉的。菜鸟只想吐槽,马扁子能不能长点心?就这么安排课,谁顶得住(lll¬ω¬) !!!
话不多说,就让我们开始今天的课程吧!
菜鸟写完这篇,感觉自己烦死了,受不了马扁子,所以质量有点差,建议读者放弃阅读,因为我只想给读者最好的!
原理篇一知识点:
在早期,写程序可不是直接拿出键盘敲着高级编程语言就可以实现的,人们是将程序写在纸上,然后根据程序代码,在一个古老的物理设备——打孔卡(Punched Card) [也叫纸带] 上不打孔或打孔来代表0或1。这样,要写的程序、要处理的数据,就变成一条条纸带或者一张张卡片,之后再交给当时的计算机去处理。
那么为什么以前无法和现在一样使用高级程序语言直接编程呢?
说简单一点就是,CPU无法解析(理解)这些高级编程语言,CPU只能理解机器语言,即一连串的0和1。即使是到了现在,计算机仍然只能理解机器语言。
那么现代计算机如何执行高级编程语言的呢?
我们常说,CPU 就是计算机的大脑。CPU 的全称是 Central Processing Unit,中文是中央处理器。
从硬件的角度来看,CPU 就是一个超大规模集成电路,通过电路实现了加法、乘法乃至各种各样的处理逻辑。
从软件工程师的角度来讲,CPU 就是一个执行各种计算机指令(Instruction Code)的逻辑机器。这里的计算机指令,就好比一门 CPU 能够听得懂的语言,我们也可以把它叫作机器语言(Machine Language)。
不同的 CPU 能够听懂的语言不太一样。比如,我们的个人电脑用的是 Intel 的 CPU,苹果手机用的是 ARM 的 CPU。这两者能听懂的语言就不太一样,这样两种 CPU 各自支持的语言,就是两组不同的计算机指令集,英文叫 Instruction Set。这里面的“Set”,其实就是数学上的集合,代表不同的单词、语法。
上面只是极客时间的一面说辞而已,这里菜鸟自己搜索了一下三种语言的区别:
一、指代不同
二、编码方式不同
扩展:
一个计算机程序,不可能只有一条指令,而是由成千上万条指令组成的。但是 CPU 里不能一直放着所有指令,所以计算机程序平时是存储在存储器中的。这种程序指令存储在存储器里面的计算机,我们就叫作存储程序型计算机(Stored-program Computer)。
(当然还有其它类型,读者感兴趣可以自行搜索一下)
我们拿一小段真实的 C 语言程序来看看(这里菜鸟学过了单片机,知道一些汇编语言,所以这里就直接拿极客时间的了)
// test.c
int main()
{
int a = 1;
int b = 2;
a = a + b;
}
这里是很简单的c语言代码,我就不解释了。
要让这段程序在一个 Linux 操作系统上跑起来,我们需要把整个程序翻译成一个汇编语言(ASM,Assembly Language)的程序,这个过程我们一般叫编译(Compile)成汇编代码。
针对汇编代码,我们可以再用汇编器(Assembler)翻译成机器码(Machine Code)。
在一个 Linux 操作系统上,我们可以简单地使用 gcc 和 objdump 这样两条命令,把对应的汇编代码和机器码都打印出来。(这里菜鸟还是先自己试试)
gcc -g -c test.c
objdump -d -M intel -S test.o
结果:
可以看到,左侧有一堆数字,这些就是一条条机器码;右边有一系列的 push、mov、add、pop 等,这些就是对应的汇编代码。一行 C 语言代码,有时候只对应一条机器码和汇编代码,有时候则是对应两条机器码和汇编代码。同一台机器,汇编代码和机器码之间是一一对应的。
这个时候你可能又要问了,我们实际在用 GCC(GUC 编译器套装,GUI Compiler Collectipon)编译器的时候,可以直接把代码编译成机器码呀,为什么还需要汇编代码呢?
原因就是:如果直接将高级编程语言翻译成机器码,那么你在两者相互对照时,是完全无法理解的,但是有了汇编语言,以及里面的简单单词add、mov等,即使没学过汇编语言,对于简单的汇编语言你大致可以猜出是什么意思,再配上数字,就更加容易理解机器码了,所以汇编语言其实就是一个桥梁,连接着机器码和高级语言,不至于变换太过突兀,汇编语言就是给程序员看的机器语言。
从高级语言到汇编代码,再到机器码,就是一个日常开发程序,最终变成了 CPU 可以执行的计算机指令的过程。
其实菜鸟看到这,完全不知道和标题(从编译到汇编,代码怎么变成机器码?)有什么关系,只想骂一顿马扁子,菜鸟除了自己总结了一下原因,运行了一下代码,其它全是复制过来的,菜鸟感觉这里就是讲了一个过程,并没有将具体的原因,一脸懵逼(lll¬ω¬)
了解了这个过程,下面我们放大局部,来看看这一行行的汇编代码和机器指令,到底是什么意思。
一般来说,常见的指令可以分成五大类:
第一类是算术类指令。我们的加减乘除,在 CPU 层面,都会变成一条条算术类指令
第二类是数据传输类指令。给变量赋值、在内存里读写数据,用的都是数据传输类指令
第三类是逻辑类指令。逻辑上的与或非,都是这一类指令
第四类是条件分支类指令。日常我们写的“if/else”,其实都是条件分支类指令
最后一类是无条件跳转指令。写一些大一点的程序,我们常常需要写一些函数或者方法。在调用函数的时候,其实就是发起了一个无条件跳转指令。
这里一类举一个例子:(感觉没有必要,又不是来学单片机的,醉了,而且读者看了也不一定清楚,马扁子太会折磨人了,这里读者随便看看就行,懂的可以追究,不懂的可以直接不看,不过这五个类确实应该看看,并理解记忆下来!!)
下面我们来看看,汇编器是怎么把对应的汇编代码,翻译成为机器码的。
我们说过,不同的 CPU 有不同的指令集,也就对应着不同的汇编语言和不同的机器码。为了方便你快速理解这个机器码的计算方式,我们选用最简单的 MIPS 指令集,来看看机器码是如何生成的。
MIPS:
MIPS 的指令是一个 32 位的整数,高 6 位叫操作码(Opcode),操作码就是代表这条指令具体是一条什么样的指令,剩下的 26 位有三种格式,分别是 R、I 和 J。
R 指令是一般用来做算术和逻辑操作,里面有读取和写入数据的寄存器的地址。如果是逻辑位移操作,后面还有位移操作的位移量,而最后的功能码,则是在前面的操作码不够的时候,扩展操作码表示对应的具体指令的。
I 指令,则通常是用在数据传输、条件分支,以及在运算的时候使用的并非变量还是常数的时候。这个时候,没有了位移量和操作码,也没有了第三个寄存器,而是把这三部分直接合并成了一个地址值或者一个常数。
J 指令就是一个跳转指令,高 6 位之外的 26 位都是一个跳转后的地址。
(读者理解一下就行,菜鸟心里恨死马扁子了,大家千万不要买呀!!!这里的看看就好,不要死磕,不要深究,因为马扁子根本就没有任何解释,你想深究都没有方向,马扁子直接给的结果,反正这里只是想让大家知道汇编语言是怎么通过机器语言翻译成机器码的)
以一个简单的加法算术指令 add s1, $s2, 为例,给你解释。为了方便,我们下面都用十进制来表示对应的代码。
对应的 MIPS 指令里 opcode 是 0,rs 代表第一个寄存器 s1 的地址是 17,rt 代表第二个寄存器 s2 的地址是 18,rd 代表目标的临时寄存器 t0 的地址,是 8。因为不是位移操作,所以位移量是 0。把这些数字拼在一起,就变成了一个 MIPS 的加法指令。
(时间距离写完该博客已经过去超过12小时,但是菜鸟心里一直放不下,因为感觉没有写好,所以这里加一点:
其实这里可以这样理解,假设前面的操作码只有2位,那么其对应的只有00,01,10,11,而这四个你可以理解为+,-,*,/,而后面的则是进行运算的数,以及运算完了该放入哪里,即地址。这就是汇编语言最简的对应过程,其实汇编语言就是将操作码换成了人们便于理解的单词,而后面的其实没怎么变,只是用字母,代替了应该填写的相应运算器的地址或者跳转的地址。)
为了读起来方便,我们一般把对应的二进制数,用 16 进制表示出来。在这里,也就是0X02324020。这个数字也就是这条指令对应的机器码。
回到开头我们说的打孔带。如果我们用打孔代表 1,没有打孔代表 0,用 4 行 8 列代表一条指令来打一个穿孔纸带,那么这条命令大概就长这样:
到这里,想必你也应该明白了,我们在这一讲的开头介绍的打孔卡,其实就是一种存储程序型计算机。
只是这整个程序的机器码,不是通过计算机编译出来的,而是由程序员,用人脑“编译”成一张张卡片的。对应的程序,也不是存储在设备里,而是存储成一张打好孔的卡片。但是整个程序运行的逻辑和其他 CPU 的机器语言没有什么分别,也是处理一串“0”和“1”组成的机器码而已。
这一讲里,我们看到了一个 C 语言程序,是怎么被编译成为汇编语言,乃至通过汇编器再翻译成机器码的。除了 C 这样的编译型的语言之外,不管是 Python 这样的解释型语言,还是 Java 这样使用虚拟机的语言,其实最终都是由不同形式的程序,把我们写好的代码,转换成 CPU 能够理解的机器码来执行的。
只是解释型语言,是通过解释器在程序运行的时候逐句翻译,而 Java 这样使用虚拟机的语言,则是由虚拟机对编译出来的中间代码进行解释,或者即时编译成为机器码来最终执行。
然而,单单理解一条指令是怎么变成机器码的肯定是不够的。接下来的几节,我会深入讲解,包含条件、循环、函数、递归这些语句的完整程序,是怎么在 CPU 里面执行的。(这里菜鸟想吐槽,鬼信,垃圾jike!!!)