
CPU对我们来说既熟悉又陌生。熟悉的是,我们知道代码是由 CPU 执行的。当我们的在线服务出现问题时,我们可能会首先检查 CPU 负载。奇怪的是,我们不知道 CPU 是如何执行代码的,它对我们的代码做了什么。本文意在简要说明我们代码的生命周期以及代码是如何在 CPU 上运行的。
编译——让电脑认识我
一个漂亮的control+c加上一个漂亮的control+v,slap~,我们愉快的写了代码,代码保存的时候,存储在我们磁盘的某个地方,可能是java或者python之类的东西level 语言,也可能是c这样的古语言写的,但是现在一定不能运行,因为计算机不知道它们,计算机只知道0、1这样的二进制,简称机器代码,那我们为什么不直接写机器码呢?如果你有这种想法,我只能呵呵,请帮我翻译以下机器码:
001010100101001001001
100100101000101010101
复制代码
显然,作为高素质的人类,我们无法识别这段代码是写什么的,于是就有了像java这样的高级语言,给机器码披上了一层外衣,然后交给伟大的程序员去创造未来.
所以反过来,我们的代码也需要换成机器码,这样电脑才能识别,电脑才能帮我们做事。这个转换过程通常称为编译。
#include
int main()
{
printf("Hello World\n");
return 0;
}
复制代码
这是每个程序员都应该编写的一段代码(hello.c)。在 Linux 下,当我们使用 GCC 编译 Hello World 程序时,我们只需要最简单的命令:
gcc hello.c
./hello
# Hello World
复制代码
看似简单的一行,其实编译过程非常复杂。这不是我们想象的编译。它实际上分为4个步骤,即Prepressing、Compliation、Assertmbly和Linking。(链接)。
预编译:这个过程主要处理源代码中以“#”开头的预编译指令,如“#include”、“define”等。 编译:这个过程是经过词法分析、语法分析、预处理文件的语义分析和优化。这个过程是最复杂的。汇编:这个过程是将汇编代码转换成机器码,也就是上图中的目标文件hello.o链接:我们的代码程序往往是由多个代码文件组成的,当每个文件被汇编成“.o”文件时,您需要一种机制将它们组装在一起,这个过程称为链接。
嗯,原来的编译是这样的。通过这整套编译操作,我们的代码终于可以执行了。我们只需运行 ./hello.out 即可输出 Hello World。等等,这个简单的过程发生了什么?
连接 – 换乘站和高速公路
ok,ok,编译完成后,我们的程序终于可以执行了,我们从CPU的角度来看看Hello World是怎么打印出来的。
首先,编译后的文件存放在磁盘上,必须先加载到内存中。在这里你可能会问:为什么CPU不能直接读取磁盘程序运行而是通过内存来运行?答案是慢,慢的磁盘会影响我们程序执行的速度,所以我们需要一个更靠近CPU的更快的存储,也就是内存。
内存是一个很大的存储空间,可以存储大量的数据信息,那么如何找到我们要写的程序呢?答案是地址。其实每个字节在内存中都有一个地址,所以CPU在内存中读取我们的程序时,只需要根据对应的地址知道我们程序的具体内容即可。
等等……,这里似乎还有一个问题,CPU是如何与我们的内存、磁盘通信的?一定有媒介什么的。是的,这个媒介就是主板上的总线和芯片组。公共汽车很容易理解,就像高速公路一样。数据信息可以通过这条高速路传输到CPU。这是什么芯片组?电脑主板上有很多芯片,主要是南桥芯片和北桥芯片。我先解释一下:
北桥芯片:北桥负责高速设备与CPU之间的通信伪代码用什么软件写,主要是CPU、内存、显卡之间的通信,但随着技术的迭代,主板上的北桥芯片已经内置于中央处理器。南桥芯片:南桥负责低速设备与北桥的通信,主要负责I/O总线之间的通信,如USB、LAN、ATA、SATA、音频控制器、键盘控制器、实时时钟控制器, 高级电源管理等。
嗯……为什么CPU和高速设备、低速设备之间的通信需要这两个芯片?CPU不能自己做吗?这还是类似于拆分任务的功能。如果所有任务都交给 CPU,CPU 就会太忙。还有一点很重要的是,如果南桥芯片坏了,那么我们可以直接更换南桥,而无需更换整个CPU。
最后,CPU通过总线和芯片打通了磁盘和内存的通信,接下来的一切都交给了CPU。
CPU——最强大脑
CPU的全称是Central Processing Unit,也就是中央处理器,其本质是一个非常大规模的集成电路。从逻辑上讲,它的内部由寄存器、控制器、运算符和时钟组成。让我们解释一下每个组件的作用。
综上所述,CPU的一般工作流程如下:时钟信号到来时,开始工作,通过控制器将内存中的数据读入各个寄存器,然后如果有与计算相关的逻辑,它被移交给操作员。发现没有,CPU的工作其实挺简单的,本质就是不停的读取和执行指令。但是CPU是如何读取我们的代码指令的,以及我们代码中的if else和函数调用是如何进行分支判断和函数跳转的,我们来看一个例子:
a = 1 #0x0010
b = 2 #0x0011
if a > b { #0x0012
printf("%s","a") #0x0013
} else {
add(a,b) #0x0014
}
printf("%s","end") #0x0017
func add(int a,int b) { #0x0020
return a+b
}
复制代码
这是一个非常简单的伪代码,带有分支判断和函数跳转。让我们从 CPU 的角度看看它是如何执行的:
首先,每个程序都有一个起始地址0x0010,是CPU读取程序的入口,将数字a=1读入通用寄存器,程序计数器(PC寄存器)自动加1,即指向下一条指令0x0011指令寄存器获取程序计数器的指令地址,将数字b=2读入通用寄存器,程序计数器(PC寄存器)自动加1,即指向到下一条指令 0x0012 指令寄存器发现这是比较逻辑,会执行ab,这可能有大于0、等于0、小于0的三个结果,然后将结果存入flag登记。这里有一点小知识。我们常说的CPU是64位还是32位,
显然a小于b,CPU应该根据标志寄存器的状态值跳转到else。注意程序计数器的值不是加1,而是设置为else的地址0x0014。当它执行到0x0015时,如果需要发生函数跳转,程序计数器会设置为0x0020,但这不是简单的函数跳转(专业术语叫call),因为函数执行后,会返回伪代码用什么软件写,即程序计数器需要再次从0x0020改变。变为 0x0017。调用执行时,后面要执行的指令的地址0x0017存放在栈上。当我们的 add 函数执行时,会有一个返回。回来的时候,将上一步存储在堆栈中的地址 0x0017 写入程序计数器的指令寄存器,并根据程序计数器的当前地址执行最后的打印(结束)。结束。
对于顺序执行的指令代码,程序计数器会自动累加(当然不一定是累加1),然后找到下一条要执行的指令。
在判断分支时,程序计数器不是简单的地址累加,而是需要进行地址跳转。
函数调用不仅需要跳转地址,还保存了函数执行后要执行的地址,方便返回继续执行。
其实在我们的代码中还有一个循环执行,也就是for、while等。此时,程序计数器会不断在某些地址之间来回切换。
请登录后发表评论
注册
社交帐号登录