❝Only the disciplined in life are free. 唯自律者得自由
❞
大家好,我是「柒八九」。
想必能看到这篇文章的小朋友,大都是有一定编程能力的「程序媛、程序猿」。无论,你是从事切图的前端工作,还是对数据有一种爱而不得的后端开发。更甚者,是和底层打交道的嵌入式开发人员。无论你平时在工作环节中,对编程语言API
做到如何的得心应手,但是在遇到一些比较「底层」的逻辑和知识时。或多或少,有点「捉襟见肘」。
而今天,我们又准备开辟一个新的知识体系 --「计算机底层知识」。老话说的好,「不想当将军的士兵不是好士兵」。但是,在你想成为将军的时候,你需要拥有成为将军的知识储备和能力。这也是我们常说的「未雨绸缪」。
如果你对前端一些前沿技术比较了解的话,像WebAssembly
/SWC
/Rust
(硬放到前端也不是不可以)等。他们内核中,无一不透露出,计算机底层的知识。套用唯心主义的话,「存在即合理」,既然是大势所趋,那么我们为什么不顺势而为呢。
而真正的想了解上述前沿技术,拥有扎实的计算机底层方法论是「必不可少」的。而该系列文章就是为了,帮助大家来夯实基础,为了能够在以后的编程道路中,走的更远。
该系列文章的第一篇文章,我们来讲讲「计算机CPU」的常规知识。
好了,天不早了,干点正事哇。
❝❞
- CPU的内部结构 「推荐阅读指数」 ⭐️⭐️⭐️⭐️⭐️
- CPU是寄存器的集合体 「推荐阅读指数」 ⭐️⭐️⭐️⭐️
- 决定程序流程的程序计数器 「推荐阅读指数」 ⭐️⭐️⭐️⭐️⭐️
- 条件分支和循环机制 「推荐阅读指数」 ⭐️⭐️⭐️⭐️⭐️
- 函数的调用机制 「推荐阅读指数」 ⭐️⭐️⭐️⭐️⭐️
- 通过地址和索引实现数组
❝CPU是中央处理器 Central Processing Unit的缩写,相当于计算机的大脑,它的内部由数百万至数亿个「晶体管」构成。
❞
在「程序运行流程」中,CPU
所负责的就是「解释和运行」最终转换成「机器语言」的程序内容。
CPU
和内存是由许多晶体管组成的「电子部件」,通常成为集成电路 Integrated Circuit。
❝从功能方面来看,
❞CPU
的内部是由「寄存器」、「控制器」、「运算器」、「时钟」等四个部分组成,各个部分之间由「电流信号」相互连通。
CPU
内部户有 20~100
个寄存器 CPU
开始计时的 「时钟信号」 ❝通常所说的「内存」指的是计算机的主要存储器 Main Memory,简称「主存」。
❞
主存
通过「控制芯片」等与CPU
相连,主要负责「存储指令和数据」。主存由「可读写」的元素构成,每个字节(1字节=8位
)都带有一个「地址编号」。CPU
可以通过该地址「读取」主存中的指令和数据,当然也可以「写入」数据。
程序启动后,根据「时钟信号」,「控制器」会从「内存」中读取指令和数据。通过对这些指令加以解释和运行,「运算器」就会对数据进行运算,「控制器」根据该运算结果来控制计算机。
CPU
的四个构成部分中,我们只需要了解寄存器即可。这是因为,「程序是把寄存器作为对象来描述的」。
假设,我们存在如下用汇编语言编写的代码。
❝「汇编语言」采用助记符 Memonic来编写程序,每一个原本是「电气信号」的「机器语言指令」都有有一个与其「相对应的助记符」。
❞
助记符通常为指令功能的英语单词的缩写。
例如,mov
和add
分别是数据的存储和相加的简写。
❝「汇编语言和机器语言基本上是一一对应的」
❞
从上述的「汇编代码」中,我们可以看出,「机器语言级别的程序是通过寄存器来处理的」,也就是说,「CPU是寄存器的集合体」。eax
和ebp
表示的都是寄存器。并且,内存的存储场所「通过地址编号来区分」,而寄存器的种类「通过名字来区分」。
CPU
处理程序的大致过程如下:
❝使用「高级语言」编写的程序会在「编译」后转化成「机器语言」,然后再通过
❞CPU
内部的寄存器来处理。
❝不同类型的
❞CPU
,其内部寄存器的数量、种类以及寄存器存储的数值范围都是不同的。
不过,根据功能的不同,我们可以将寄存器大致分为「8类」。
可以看出,寄存器中存储的内容既「可以是指令也可以是数据」。其中,数据分为「用于运算的数据」和「表示内存地址的数据」
只有1行的有用程序是很少见的,机器语言的程序也是如此。接下来,我们看一下程序是如何按照流程运行的。
下图是程序启动后的内存内容的模型。
❝用户发出启动程序的指示后,「操作系统」会把「硬盘」中保存的程序「复制」到「内存」中。
❞
实例中的程序实现的是将123
和456
两个数值相加,并将结果输出到显示器上。
前面我们已经介绍过,存储指令和数据的内存,是通过地址来划分的。由于使用机器语言难以清晰地表明各地址存储的内容,因此我们对各地址的存储内容添加注释。实际上,「一个命令和数据通常被存储在多个地址上」,但是为了便于说明,上面的图例中,把指令、数据分配到一个地址中。
大致流程如下:
0100
是程序运行的开始位置。 CPU
寄存器的一种)设定为 0100
,然后程序便开始运行。 CPU
每执行一个指令,程序计数器的值就会自动加1」 CPU
的 「控制器」就会参照程序计数器的数值,从内存中读取命令并执行。 ❝程序计数器决定着程序的流程
❞
程序的流程分为「顺序执行」、「条件分支」和「循环」三种。
「顺序执行」的情况比较简单,每执行一个指令「程序计数器」的值就「自动加1」.但若程序中存在「条件分支」和「循环」,机器语言的指令就可以将「程序计数器」的值设定为「任意地址」(不是加1)。这样一来,程序便可以返回到上一个地址来重复执行同一个指令,或者跳转到任意地址。
上图表示把内存中存储的数值(示例中是123)的绝对值输出到显示器的程序的内存状态。
大致流程如下:
0100
地址 0102
地址时,如果 「累加寄存器」的值是 「正数」,则执行 「跳转指令」( jump
指令)跳转到 0104
地址 123
,为 「正数」,因此 0103
地址的指令被跳过,程序的流程 「直接」跳转到了 0104
地址 ❝「条件分支」和「循环」中使用的「跳转指令」,会参照当前执行的「运算结果」来判断是否跳转。
❞
前面我们提到过「标志寄存器」。无论当前「累加寄存器」的运算结果是负数、零还是正数,「标志寄存器」都会将其保存。
CPU
在进行运算时,「标志寄存器」的数值会根据运算结果「自动设定」。至于是否执行「跳转指令」,则由CPU
在参考「标志寄存器」的数值后进行判断。运算结果的正、零、负「三个状态」由「标志寄存器」的三个位表示。
CPU
(寄存器的长度是32位)的标志寄存器的示例
「标志寄存器」的第一个字节位、第二个字节位和第三个字节位的值为1时,表示的运算结果分别为正数、零和负数。
假设要比较「累加寄存器」中存储的XXX
值和「通用寄存器」中存储的YYY
值,执行比较的指令后,CPU
的运算装置就会在内部进行XXX-YYY
的「减法运行」。
无论减法运算的结果是正数、零还是负数,都会被保存到「标志寄存器」中。
XXX
比 YYY
大 XXX
和 YYY
相等 XXX
和 YYY
小 ❝程序中的比较指令,就是在
❞CPU
内部做减法运算
❝函数调用处理也是通过把「程序计数器」的值设定成函数的存储地址来实现的
❞
和「条件分支」、「循环」的机制不同,因为单纯的跳转指令无法实现函数的调用。
❝函数的调用需要在完成函数内部的处理后,处理流程再返回到函数调用点(「函数调用指令的下一个地址」)
❞
上图的示例为 变量a
和b
分别代入123
和456
后,将其赋值给参数来调用MyFunc
函数的C
语言程序。图中的地址是将C
语言编译成机器语言后运行时的地址。由于1行C
语言程序在编译后通常会变成多行的机器语言,所以图中的地址是「离散」的。
此外,通过「跳转指令」把「程序计数器」的值设定为0260
也可以实现调用MyFunc
函数。函数的「调用原点」(0132
地址)和「被调用函数」(0260
地址)之间的数据传递,可以通过内存或寄存器来实现。
当函数处理进行到最后的0354
地址时,我们应该将「程序计数器」的值设定成函数调用后要执行的0154
地址。我们通过机器语言的call
指令和return
指令能实现该功能。
❝函数调用使用的是
❞call
指令,而不是跳转指令。
在将函数的入口地址设定到「程序计数器」之前,「call指令」会把调用函数后要执行的指令地址存储在名为「栈」的内存内。「return 指令」的功能是把保存在栈中的地址设定到「程序计数器」中。
❝通过「基址寄存器」和「变址寄存器」可以对「主内存」上特定的内存区域进行划分,从而实现类似于数组的操作
❞
00000000~FFFFFFFF
的地址划分出来
❝❞
CPU
会把「基址寄存器」+「变址寄存器」的值解释为实际查看的内存地址。
「变址寄存器」的值相当于高级程序语言程序中数组的「索引功能」
「分享是一种态度」。
参考资料:《程序是怎样跑起来的》
「全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。」
本文由 mdnice 多平台发布