Only the disciplined in life are free. 唯自律者得自由
大家好,我是柒八九。
想必能看到这篇文章的小朋友,大都是有一定编程能力的程序媛、程序猿。无论,你是从事切图的前端工作,还是对数据有一种爱而不得的后端开发。更甚者,是和底层打交道的嵌入式开发人员。无论你平时在工作环节中,对编程语言API
做到如何的得心应手,但是在遇到一些比较底层的逻辑和知识时。或多或少,有点捉襟见肘。
而今天,我们又准备开辟一个新的知识体系 --计算机底层知识。老话说的好,不想当将军的士兵不是好士兵。但是,在你想成为将军的时候,你需要拥有成为将军的知识储备和能力。这也是我们常说的未雨绸缪。
如果你对前端一些前沿技术比较了解的话,像WebAssembly
/SWC
/Rust
(硬放到前端也不是不可以)等。他们内核中,无一不透露出,计算机底层的知识。套用唯心主义的话,存在即合理,既然是大势所趋,那么我们为什么不顺势而为呢。
而真正的想了解上述前沿技术,拥有扎实的计算机底层方法论是必不可少的。而该系列文章就是为了,帮助大家来夯实基础,为了能够在以后的编程道路中,走的更远。
该系列文章的第一篇文章,我们来讲讲计算机CPU的常规知识。
好了,天不早了,干点正事哇。
你能所学到的知识点
- CPU的内部结构 推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️
- CPU是寄存器的集合体 推荐阅读指数 ⭐️⭐️⭐️⭐️
- 决定程序流程的程序计数器 推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️
- 条件分支和循环机制 推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️
- 函数的调用机制 推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️
- 通过地址和索引实现数组
CPU的内部结构
CPU是{中央处理器| Central Processing Unit}的缩写,相当于计算机的大脑,它的内部由数百万至数亿个晶体管构成。
在程序运行流程中,CPU
所负责的就是解释和运行最终转换成机器语言的程序内容。
CPU
和内存是由许多晶体管组成的电子部件,通常成为{集成电路| Integrated Circuit}。
从功能方面来看,
CPU
的内部是由寄存器、控制器、运算器、时钟等四个部分组成,各个部分之间由电流信号相互连通。
- 寄存器
- 用来缓存指令、数据等处理对象,可以将其看作是内存的一种
- 根据种类的不同,一个
CPU
内部户有20~100
个寄存器
- 控制器
- 负责把内存上的指令、数据等读入寄存器
- 并根据指令的执行结果来控制整个计算机
- 运算器
- 负责运算从内存读入寄存器的数据
- 时钟
- 负责发出
CPU
开始计时的时钟信号
内存
通常所说的内存指的是计算机的{主要存储器| Main Memory},简称主存。
主存
通过控制芯片等与CPU
相连,主要负责存储指令和数据。主存由可读写的元素构成,每个字节(1字节=8位
)都带有一个地址编号。CPU
可以通过该地址读取主存中的指令和数据,当然也可以写入数据。
程序运行机制
程序启动后,根据时钟信号,控制器会从内存中读取指令和数据。通过对这些指令加以解释和运行,运算器就会对数据进行运算,控制器根据该运算结果来控制计算机。
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
在参考标志寄存器的数值后进行判断。运算结果的正、零、负三个状态由标志寄存器的三个位表示。
标志寄存器的第一个字节位、第二个字节位和第三个字节位的值为1时,表示的运算结果分别为正数、零和负数。
CPU比较机制
假设要比较累加寄存器中存储的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 指令和return 指令
函数调用使用的是
call
指令,而不是跳转指令。
在将函数的入口地址设定到程序计数器之前,call指令会把调用函数后要执行的指令地址存储在名为栈的内存内。return 指令的功能是把保存在栈中的地址设定到程序计数器中。
通过地址和索引实现数组
通过基址寄存器和变址寄存器可以对主内存上特定的内存区域进行划分,从而实现类似于数组的操作
- 用十六进制数将计算机内存上
00000000~FFFFFFFF
的地址划分出来
- 凡是该范围的内存区域,只要有一个32位的寄存器,即可查看全部的内存地址
- 如果想要像数组那样分割特定的内存区域以达到连续查看的目的,使用两个寄存器会更方便
CPU
会把基址寄存器+变址寄存器的值解释为实际查看的内存地址。
变址寄存器的值相当于高级程序语言程序中数组的索引功能
后记
分享是一种态度。
参考资料:《程序是怎样跑起来的》
全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。