程序是怎样跑起来的
本文主要学习自 《c程序是怎样跑起来》一书,再添加了一些自己的理解和注释,请各位观者支持原版
CPU 对于程序员的意义
- 指示计算机每一步动作的一组指令叫做程序,程序是由指令和数据构成的,
- 正在运行的程序是存储在内存中的,磁盘和硬盘等媒介中保存的程序被复制到内存后才能运行,
- 内存中,用来表示命令和数据存储位置的数值叫做内存地址,地址由整数值来表示. CPU 负责程序的解释和运行。
- CPU 能够识别和执行的只有机器语言,使用C 或者 Java.编写的代码,最终都会被转化成机器语言。
作为一个程序员只知道 CPU 是中央处理器是不够的,还需要知道 CPU 是如何运行的,特别是要弄清楚负责保存指令和数据的寄存器的机制,了解了寄存器,就会理解程序的运行机制。
CPU 的内部结构解析
程序运行的一般流程
- 程序员用 C 语言等高级语言编写程序
int a;
a = 1+2;
printf("%d",a);
- 将程序编译后转换成机器语言的 EXE 文件
010000101001010101010010101010100101010101010010101
- 程序运行时,在内存中生成 EXE 文件的副本
- cpu 解释并执行程序内容
CPU 内部组成
寄存器
用来暂存指令,数据等处理对象,可以将它看成内存,一个 CPU 内部内部会有 20-100 个寄存器
控制器
负责把内存上的指令,数据读入寄存器,并根据指令的执行结果来控制整个计算机。、
运算器
负责运算从内存读入寄存器的数据
时钟
负责发出 CPU 开始计时的时钟信号
内存
通常说的内存是指计算机的主存储器,简称 主存,主存通过控制芯片等与 cpu 相连,主要负责存储指令和数据,主存由可读写的元素构成,每个字节(1个字节 =8 位)都带有一个地址编号,CPU 可以通过改地址读取主存中的指令和数据,当然也可以写入数据,但是需要注意的是,主存中存储的指令和数据会随着计算机的关机而自动清除。
程序的运行机制:程序启动后,根据时钟信号,控制器会从内存中读取指令和数据,通过这些指令加以解释和运行,运算器会对数据进行运算,控制器根据该运算结果来控制计算机(所谓的控制就是值数据运算以外的处理,比如:数据输入和输出事件的控制,键盘,显示器等的输入输出。)
CPU 是寄存器的集合体
CPU 的四个构成部分中,其实我们只需要了解寄存器即可。原因是:程序把寄存器作为对象来描述的。
备注: 内存的存储场所通过地址编号来区分,而寄存器的种类则通过名字来区分。
通常可以将寄存器分为8类:
- 累加寄存器: 存储执行运算的数据和运算后的数据 (1个)
- 标志寄存器: 存储运算处理后的 CPU 状态 (1个)
- 程序计数器: 存储下一条指令所在的内存的地址 (1个)
- 基址寄存器: 存储数据内存的起始地址
- 变址寄存器: 存储基址寄存器的相对地址
- 通用寄存器: 存储任意数据
- 指令寄存器: 存储指令, CPU 内部使用,程序员无法通过程序对该寄存器进行读写操作。(1个)
- 栈寄存器: 存储栈区域的起始地址 (1个)
决定程序流程的程序计数器
顺序执行程序
条件分枝和循环机制
程序的流程分为: 顺序执行,条件分枝和循环三种,顺序执行:是指按照地址内容的顺序执行指令,条件分枝:是指根据条件执行任意地址的指令,循环: 是指重复执行同一地址的指令。
CPU 在进行运算时,标志寄存器的数值会根据运算结果自动设定,至于是否执行跳转指令,则由 CPU 在参考标志寄存器的数值后进行判断
cpu 执行比较的机制:将要比较的两个寄存器中的数据做减法,得到的结果保存到标志寄存器中,判断正负。
函数的调用机制
函数的调用是通过把程序计数器的值设定成函数的存储地址来实现。函数的调用需要再完成函数内部的处理后,处理流程在返回到函数的调用点(函数调用指令的下一个地址),因此,如果只是跳转到函数的入口地址,处理流程就不知道应该返回至哪里了。
机器语言的 call
指令和 return
指令能够解决这个问题解决当函数调用后执行的返回。函数调用使用的是 call 指令
而不是跳转指令,在将函数的入口地址设定到程序计数器之前, call 指令
会把调用函数后要执行的指令地址存储在名为栈的主存中,函数处理完毕后,再通过函数的出口来执行 return 命令, return 命令的功能是把保存在栈中的地址设定到程序计数器中。
具体请参考我稍后会带来的递归算法的处理原理
通过地址和索引实现数组
通过基址寄存器和变址寄存器可以对主存上特定的内存区域进行划分。从而实现类似于数组的操作。
如果想要像数组一样分割特定的内存区域以达到连续查看的目的,使用两个寄存器会更方便些。 CPU 会把 基址寄存器 + 变址寄存器的值解释为实际查看的内存地址,变址寄存器的值就相当于变成语言中数组的索引。
机器语言的主要类型和功能
数据是通过二进制表示的
原码,反码,补码,移码
正数: 反码 = 原码 = 补码
负数: 反码 = 其原码除符号之外的各位求反
补码 = 反码 + 1
运算实例
正零: 00000000
负零: 10000000
补码:
00000000 11111111+1 -> 00000000
需要注意的是:如果 +1 之后有进位,要一直往前进位,包括符号位。
例如:
原码: 01011
反码: 01011
补码: 01011
原码: 11011
反码: 10100
补码: 10101
在计算机中,数据一律通过补码来存储
使用补码的原因
我总结一下大概的原因如下:
- 计算机里面只有加法器,没有减法器,所有的减法都必须使用加法来进行。
- 所谓的补码算法是有编译器在编译的时候处理负数用的。
- 使用补码的原因是模运算. 具体可以解释为 补码 = 模 - |X|
- 模 1 + 运算位数个0
- 将二进制的值求反后加1的结果和原来的值相加,结果为0.
数值,字符串和图像信息在计算机内都是以二进制数值的形式来表现的
用二进制表示计算机信息
原因
IC 是集成电路,IC 的引脚只有良种繁育状态 0V 和 5V, 也就是说,一个引脚只能表示两个状态。这种特性决定了计算机只能使用二进制数来处理信息数据。
字节是最基本的信息计量单位,位是最小单位,字节是基本单位。
除法运算和移位运算的关系
移位运算就是将二进制数值的各数位进行左右移动的运算,移位有左移和右移两种,再一次运算中,可以进行多个数位的移位操作。
计算机进行小数运算时出错的原因
举例查看
float sum;
int i;
sum = 0;
for (i = 0; i <100; ++i)
{
sum += 0.1;
}
printf("%f\n", sum);
打印出来竟然是 10.000002, 而不是10.
原因是什么呢,下面就来分析分析
用二进制数表示小数
1011.0011 -- 十进制 --> 11.1875
计算机运算出错的原因
计算机之所以会出现运算错误的原因是因为一些小数无法转换二进制数,例如上述的 0.1 ,就无法用二进制数正确表示,小数点后面即使有几百位也无法表示。
内存
高级语言中数据类型表示的是: 占据内存区域的大小和存储在该区域的数据类型
计算中是进行数据处理的设备,而程序表示的就是处理顺序和数据结构,由于处理对象数据是存储在内存和磁盘上的,因此程序必须能自由地使用内存和磁盘。
内存的物理机制
内存实际上是一种名为内存 IC 的电子元件,内存 IC 中有电源,地址信号,数据信号,控制信号等用于输入输出的大量引脚(IC 的引脚),通过为其制定地址,来进行数据的读写。
内存的逻辑
虽然内存的实体是内存 IC, 但是在程序员眼里的内存模型中,还包含着物理内存中不存在的概念-> 数据类型。
编程语言中的数据类型表示存储的是何种类型的数据,从内存上来看,就是占用的内存大小,即使是物理上以字节为单位来逐一读写的数据的内存,在程序中,通过为其制定类型,也能实现以特定字节数为单位来进行读写。
char a;
short b;
long c;
a = 123;
b = 123;
c = 123;
这三个变量的数据都是123,但所占用的内存不一样,
指针
指针是 C 语言的重要特征,理解指针的关键点是弄清楚数据类型这个概念。
指针也是一种变量,他所表示的不是数据的值,而是存储着数据的内存的地址,通过使用指针,就可以对任意地址的数据进行读写。
char *d;
short *e;
long *f;
假设 d,e,f 的值都是 100, 再这种情况下,使用 d 时就能够从编号 100 的地址读写1个字段,使用 e 时就是两个字段,f 就是4个字节。
数组是高效实用内存的基础
数组是多个同样数据类型的数据在内存中连续排列的形式,作为数组元素的各个数据会通过连续的编号被区分开来。这个编号称为索引。制定索引后,就可以对该索引所对应的地址的内存进行读写操作,而索引和内存地址的变换工作则是由编译器自动实现的。
之所以说 数组是内存的使用方法的基础,是因为数组和内存的物理构造是一样的。
栈,队列以及环形缓冲区
栈和队列,都可以不通过制定地址和索引来对数组的元素进行读写,需要临时保存计算过程中的数据,连接在计算机上的设备或者输入输出的数据时,都可以通过这些方法来使用内存.
栈和队列的区别就是数据出入的顺序是不同的,在对内存数据进行读写时,栈使用先进后出,队列使用先进先出。
队列一般是以环形缓冲区的方式来实现的。