一语中的
- CPU = 运算单元 数据单元 控制单元
- CPU 只能识别「二进制」的指令
- 总线像连接 CPU 和内存这两个设备的高速公路
- 执行高级语言的两种方式:1. 解释执行 2. 编译执行
- 汇编语言和机器语言是一一对应的
- 内存中的每个存储空间都有其对应的独一无二的地址
- CPU时钟周期:取出指令、分析指令、执行指令这三个过程
文章概要
- 计算机工作模式
- CPU和内存如何配合工作的
- 机器语言/汇编语言/高级语言
- CPU如何执行程序
计算机工作模式
对于计算机来讲,最核心的就是「CPU」(Central Processing Unit,中央处理器)
CPU 和其他设备连接,要靠一种叫作「总线」(Bus)的东西,其实就是主板上密密麻麻的集成电路,这些东西组成了 CPU 和其他设备的高速通道。
在这些设备中,最重要的是「内存」(Memory)。因为单靠 CPU 是没办法完成计算任务的,很多复杂的计算任务都需要将中间结果保存下来,然后基于中间结果进行进一步的计算。CPU 本身没办法保存这么多中间结果,这就要依赖内存了。
总线上还有一些其他设备,例如显卡会连接显示器、磁盘控制器会连接硬盘、USB 控制器会连接键盘和鼠标等等。
CPU和内存如何配合工作的
CPU:包括三个部分,运算单元、数据单元和控制单元。
- 运算单元只管算,例如做加法、做位移等等。
- 数据单元包括 CPU 内部的缓存和通用寄存器组,「空间很小」,但是速度飞快,可以「暂时」存放数据和运算结果
- 控制单元是一个统一的「指挥中心」,它可以获得下一条指令,然后执行这条指令。这个指令会指导运算单元取出数据单元中的某几个数据,计算出个结果,然后放在数据单元的某个地方。
每个程序都有一个「项目执行计划书」,里面是一行行程序执行的指令。每个进程都有一个程序放在硬盘上,是「二进制」的,再里面就是一行行的指令,会操作一些数据。
进程一旦运行,比如图中两个进程 A 和 B,会有「独立」的内存空间,互相隔离,程序会分别加载到进程 A 和进程 B 的内存空间里面,形成各自的代码段。
程序运行的过程中要操作的数据和产生的计算结果,都会放在数据段里面。
CPU 的控制单元里面,有一个「指令指针寄存器」(IP 寄存器),它里面存放的是下一条指令在内存中的地址。控制单元会「不停」地将代码段的指令拿进来,先放入指令寄存器。
机器语言/汇编语言/高级语言
机器语言
把 CPU 看成是一个非常小的运算机器,为了能够完成复杂的任务,为 CPU 提供了一大堆指令来实现各种功能,这一大堆指令称为指令集(Instructions)。也就是「机器语言」。
❝CPU 只能识别二进制的指令 ❞
汇编语言
但是,二进制代码难以阅读和记忆,又将二进制指令集转换为人类可以识别和记忆的符号 -- 「汇编指令集」
代码语言:javascript复制1000100111011000 机器指令
mov ax,bx 汇编指令
CPU 不能直接识别汇编语言,还需要一个「汇编编译器」,其作用是将汇编代码编程成机器代码。
「汇编语言的弊端」
- 不同的 CPU 有着不同的指令集
- 需要了解和处理器架构相关的硬件知识
高级语言
所以, 诸如 C、C 、Java、C#、Python、JavaScript 等高级语言应用而生。
和汇编语言一样,CPU也不能直接识别由高级语言所编写的代码。
根据对高级语言转换的过程中是否生成「机器代码」,把执行高级语言分为
- 解释执行
- 编译执行
解释执行
先将源代码通过解析器编译成中间代码,之后「直接」使用解释器解释执行中间代码,然后「直接输出结果」。
并不把整个程序 变成目标码,而是按顺序,读一句,解释一句,执行一句,所以,没给完整程序,它就可以执行了。
例如,浏览器处理网页,网页程序,每下来一句,就可以解释执行一句,不用等整个网页下来后再处理。
编译执行
先将源代码通过「解析器」编译成中间代码,编译器再将中间代码编译成「机器代码」(编译成的机器代码以二进制文件形式存储), 执行这段程序的时候直接执行二进制文件。
必须给出完整程序,编译器通过几次扫描,翻译,编排,链接,变成exe文件执行。
CPU如何执行程序
我们通过针对一段C代码,进行编译执行,来看看CPU是如何执行程序的。(像JS这种解释执行,有另外一套逻辑和实现方式,后期会有介绍)
代码语言:javascript复制// test.c
int main (void)
{
int x =1;
int y = 2;
int z = x y;
return z;
}
CPU 并不能直接执行这段 C 代码,需要对其进行编译,将其转换为二进制的机器码, CPU 才能按照顺序执行编译后的机器码。
通过 GCC 编译器将 C 代码编译成二进制文件。gcc -O0 -o code_prog test.c
。(在Mac环境下,可以通过brew[1]来进行gcc[2]的下载) 随后, 文件夹中生成名为 code_prog 的「可执行程序」。
通过objdump
将编译出来的 code_prog 程序进行反汇编。objdump -d code_prog
- 左边就是编译生成的机器码,每一行其实都是一个指令,该指令可以让 CPU 执行指定的任务
- 中间的部分是汇编代码, 汇编代码采用助记符(memonic)来编写程序,原本是二进制表示的指令,在汇编代码中可以使用单词来表示。「汇编语言和机器语言是一一对应的」
编译后的程序是由一堆二进制代码组成的(二进制代码是由一条条指令构成的)
准备工作
在程序执行之前,程序需要被「装进内存」。CPU 可以通过指定内存地址,从内存中读取数据,或者往内存中写入数据。(内存是一个临时存储数据的设备, 因为断电之后,内存中的数据都会消失)
「内存中的每个存储空间都有其对应的独一无二的地址」。
在内存中,每个存放字节的空间都有其唯一的地址,而且地址是按照顺序排放的
代码被编译成可执行文件, 而可执行文件中包含了二进制的机器码。当二进制代码被加载进了内存后,内存中的每条「二进制代码」便都有了自己对应的地址。
CPU时钟周期
❝CPU时钟周期:取出指令、分析指令、执行指令这三个过程 ❞
CPU取指令
CPU 中有一个 PC 寄存器,它保存了将要执行的指令地址。当二进制代码被装载进了内存之后, 系统会将二进制代码中的第一条指令的地址写入到 PC 寄存器中。
到了下一个时钟周期时,CPU 便会根据 PC 寄存器中的地址,从内存中取出指令。
CPU分析指令
PC 寄存器中的指令取出来之后,将下一条指令的地址更新到 PC 寄存器中,并分析取出的指令。
CPU执行指令
取出的指令分两部分
- 一部分是做什么操作,例如是加法还是位移;
- 一部分是操作哪些数据。
要执行这条指令,就要把第一部分交给运算单元,第二部分交给数据单元。
数据单元根据数据的地址,从数据段里读到数据寄存器里,就可以参与运算了。运算单元做完运算,产生的结果会暂存在数据单元的数据寄存器里。最终,会有指令将数据写回内存中的数据段。
如此往复的对内存中的指令,进行取出、分析、执行。直到PC寄存器到达指令的最后。
参考资料:
- 趣谈Linux操作系统
- Google V8
Reference
[1]
brew: https://formulae.brew.sh/formula/gcc#default
[2]
gcc: https://gcc.gnu.org/