程序是怎么跑起来的
作者:互联网
程序是怎么跑起来的
CPU
正文开始前需要先明确几个概念:
程序是什么?指示计算机的每一步动作的一组指令
程序是由什么组成的?指令和数据
什么是机器语言?CPU可以直接识别并使用的语言
正在运行的程序存储在什么位置?内存
什么是内存地址?内存中用来表示命令/数据存储位置的数值
CPU(Central Processing Unit、中央处理器)
CPU相当于计算机的大脑,内部由许多晶体管构成。从功能方面看,CPU的内部由寄存器、控制器、运算器和时钟构成,各部分之间由电流信号相互连通。
-
寄存器:暂存指令、数据等处理对象
-
控制器:将内存中的指令、数据读入寄存器,并根据指令的执行结果来控制整个计算机
-
运算器:从内存读入寄存器的数据
-
时钟:发出CPU开始计时的时钟信号
程序执行流程大致如下:
graph LR 高级语言程序--编译-->可执行文件--生成副本-->内存--CPU解释并执行程序内容-->CPU程序启动后,根据时钟信号,控制器从内存中读取指令和数据。通过对这些指令加以解释和运行,运算器就会的数据进行运算,控制器根据运算结果来控制计算机。
寄存器
程序是把寄存器作为对象来描述的。汇编语言采用助记符来编写程序,每个院跟事电气信号的机器语言都会有一个与其相对应的助记符,例如add,mov
。通常我们将汇编语言转化成机器语言的过程称为汇编,相反,机器语言转化成汇编语言程序的过程称为反汇编。
不同类型的CPU,其内部寄存器的数量和种类以及存储范围都不相同。根据寄存器的功能,大致将它们分成八类,见下表
种类 | 功能 |
---|---|
累加寄存器(accumulator register) | 存储运算的数据和运算后的结果 |
标志寄存器(flag register) | 存储运算处理后的CPU的状态 |
程序计数器(program counter) | 存储下一条指令的内存地址 |
基址寄存器(base register) | 存储数据内存的起始地址 |
变址寄存器(index register) | 存储基址寄存器的相对地址 |
通用寄存器(general purpose register) | 存储任意数据 |
指令寄存器(instruction register) | 存储指令 |
栈寄存器(stack register) | 存储栈的起始地址 |
CPU是具有各种功能的寄存器的集合。
程序计数器
程序计数器决定程序的流程,在用户发出启动程序的指令后,操作系统将硬盘中保存的程序复制到内存中。例如,地址0100是程序运行的开始位置,操作系统会将程序计数器这定为0100,然后程序开始执行,CPU每执行一条指令,程序计数器的指令加1(当执行的指令占据多个内存地址时,增加与指令长度相对应的数值),之后,CPU的控制器按照程序计数器的数值从内存中读取命令并执行。
顺序、条件和循环机制
-
顺序执行:按照地址内容的顺序执行指令
-
条件分支:根据条件执行任意地址的指令
-
循环:重复执行同一地址的指令
如果程序中存在条件分支和循环,则执行跳转指令(jump)会将程序计数器的值设定为相应的地址。跳转指令会根据当前执行的运算结果来判断是否跳转,同时标志寄存器会将结果(正、负、零)保存(包括溢出和奇偶校验的结果)。例如,程序执行比较指令时,在CPU内部进行减法运算,将结果的正/负、零保存到标志寄存器中。
函数的调用机制
函数调用处理是将程序计数器的值设定为函数的存储地址,但是与条件分支和循环的机制有所不同,因为单纯的跳转指令无法实现函数的调用。函数的调用需要在完成函数内部处理后返回到函数的调用点,因此,如果只知道函数的入口地址,就不知道处理结束后返回哪里。
函数调用使用的是call
指令,将函数的入口地址设定道程序计数器之前,call
指令会把调用函数后要执行的指令地址存储在栈内存中,函数处理完毕后,再通过函数的出口来执行return
命令,return
的功能是把保存在栈中的地址设定到程序计数器中。
CPU的处理
机器语言指令
类型 | 功能 |
---|---|
数据转送指令 | 寄存器与内存、内存之间、寄存器与外部设备之间数据读写 |
运算指令 | 用累加寄存器进行算数、逻辑、比较、移位运算 |
跳转指令 | 实现条件分支、循环 |
call/return 指令 | 函数调用 |
计算机中的小数
进行小数运算出错
将0.1累加100次,发现结果并不是10
在计算机中进行小数运算出错的原因是“有一些十进制的小数无法转换称二进制数”,如下表(以小数点后4位为例)
二进制 | 十进制 | 二进制 | 十进制 |
---|---|---|---|
0.0000 | 0 | 0.1000 | 0.5 |
0.0001 | 0.0625 | 0.1001 | 0.5625 |
0.0010 | 0.125 | 0.1010 | 0.625 |
0.0011 | 0.1875 | 0.1011 | 0.6875 |
0.0100 | 0.25 | 0.1100 | 0.75 |
0.0101 | 0.3125 | 0.1101 | 0.8125 |
0.0110 | 0.375 | 0.1110 | 0.875 |
0.0111 | 0.4375 | 0.1111 | 0.9375 |
可以发现,二进制数是连续的,但是十进制数是非连续的,这样就可以解释为什么100个0.1累加结果不是10:不管增加多少位,也无法得到0.1这个结果,实际上,0.1转换成十进制后。会转换成0.000110011001100…这样的循环小数。类似于无法用十进制表示\(\frac{1}{3}\)。而计算机无法处理循环小数,因此计算机会根据变量数据类型所对应的长度将数值截断得到近似值,计算机计算小数时出错就是这个原因。
浮点数的表示
在计算机中,使用符号、尾数、基数(2)、指数四部分表示小数,形如\(\plusmn m\times n^e\)
其中双精度浮点数有64位,单精度浮点数有32位,如下图
-
符号部分:使用一个位表示数值的符号,0表示“正或0”,1表示“负”
-
尾数部分:"将小数点前面的值固定为1的正则表达式"
-
指数部分:“EXCESS系统表现”
正则表达式和EXCESS系统
正则表达式
广义的正则表达式是指按照特定规则表示数据的形式
十进制中的浮点数应当遵循小数点前十0,小数点后的第一位不是0的原则。,例如0.75是\(0.75\times10^0\),即尾数部分是0.75,指数部分是0。二进制也是同样的道理,将小数点前面的值固定为1,例如
graph TB 1011.0011--第一位变成1-->001.0110011--小数点后长度为23位-->001.01100110000000000000000--保留小数点后部分-->01100110000000EXCESS系统
使用这种表示方法是为了表示负数时不使用符号位。EXCESS系统表现是指将指数表示范围的中间值设为0。例如,当指数部分是8位单精度浮点数时,最大值11111111=255的1/2即01111111=127设置为0,具体如下表
实际值 | EXCESS系统表现 |
---|---|
255 | 128 |
254 | 127 |
… | … |
127 | 0 |
126 | -1 |
… | … |
1 | -126 |
0 | -127 |
下面程序实现使用单精度浮点数表示0.75
-
符号部分:0,表示正数
-
指数部分:01111110,十进制是126,用EXCESS系统表现为-1
-
尾数部分:10000000000000000000000,根据正则表达式规则,尾数表示1.1这个二进制数,转换为10进制为1.5,
整体表示“\(+1.5\times2^{-1}=+0.75\)”
如何避免计算机出错
# include "stdio.h"
# include "string.h"
int main()
{
float data = 0.0;
unsigned long buff;
char s[34];
data = (float)0.1;
// 把数据复制到 4 字节长度的整数变量 buff 中以逐个提取出每一位。
memcpy(&buff, &data, 4);
// 逐一提取出每一位
for (int i = 33; i >= 0; --i)
{
if(i == 1 || i == 10)
{
// 破折号断开符号部分、指数部分和尾数部分。
s[i] = '-';
}
else
{
// 为各个字节赋值
if (buff % 2 == 1)
{
s[i] = '1';
}
else
{
s[i] = '0';
}
buff /= 2;
}
}
s[34] = '\0';
printf("%s\n", s);
return 0;
}
我们利用该程序实现用单精度浮点数表示十进制的0.1得到0-01111011-10011001100110011001101
,反过来计算这个数值的十进制数,结果并不是0.1
为了避免运算小数时出错,可以将小数转换成整数进行运算,再把计算结果转换成整数即可。下面程序解决这一部分开始时提出的问题:0.1累加100次得到10
# include "stdio.h"
int main()
{
int sum = 0;
for (int i = 0; i < 100; ++i)
{
sum += 1;
}
sum /= 10;
printf("%d\n", sum);
return 0;
}
使用内存
内存的物理机制
内存是一中名为内存IC的电子元件,包括DRAM、SRAM、ROM,内存IC中有电源、地址信号、数据信号、控制信号等用于输入输出的引脚,通过为其指定地址来进行数据的读写。内存IC的引脚配置如下图
-
VCC、GND:电源
-
A0~A9:地址信号
-
D0~D7:数据信号
-
RD、WR:控制信号
数据信号引脚有8个,表示1次可以输入输出8位的数据;地址信号引脚有10个,可以指定0000000000~1111111111共1024个地址,因此可以得出改内存IC的容量是1KB。
假设我们需要写入1B的数据,可以先给VCC接入+5V,给GND接入0V,用地址信号指定数据的存储位置,再把数据输入给数据信号,将WR设定为1。如果要读入数据,将RD设定为1即可。
内存的逻辑模型
有些参考书会用楼房的图形来表示内存的逻辑模型,每一层可以存储1B的数据,楼层号是地址。
地址 | 内容 |
---|---|
0000000000 | 1B的数据 |
0000000001 | 1B的数据 |
… | 1B的数据 |
1111111111 | 1B的数据 |
编程语言中的数据类型表示存储的是何种类型的数据,从内存上开就是占用内存的大小。
内存和磁盘的关系
内存和磁盘都具有存储程序命令和数据的功能。不过,内存利用电流实现存储,磁盘利用磁效应实现存储;
存储程序方式(程序内置方式)
计算机的主要存储部件是内存和磁盘,磁盘中存储的程序必须要加载到内存才能运行,而磁盘中保存的原始程序是无法直接运行的,因此负责解析和运行程序的CPU需要通过内部的程序计数器指定内存地址才能读出程序,如果CPU直接读取磁盘中的程序,会极大降低执行效率。
磁盘缓存
磁盘缓存指的是从磁盘中读取的数据存储到内存空间的方式,接下来读取同一数据的时候,就不需要通过实际的磁盘,而是从磁盘缓存中将内容读出。
虚拟内存
虚拟内存是把磁盘的一部分作为假想的内存使用,借此,在内存不足时也可以运行程序。
CPU只能执行加载到内存中的程序,虚拟内存虽然把磁盘作为内存的一部分使用,但是正在运行的程序必须在内存中,有也就是说,为了实现虚拟内存,需要把物理内存的内容与虚拟内存的内容部分置换。
磁盘的物理结构
磁盘通过将其表面分成多个空间使用,划分的方式有扇区方式和可变长方式两种。一般Windows采用的是扇区方式。
扇区方式中,把磁盘表面分成若干个同心圆的空间是磁道,把磁道按照固定大小划分成的空间是扇区
扇区是对磁盘进行物理读写的最小单位。Windows使用的磁盘一个扇区是512B,Windows在逻辑方面对磁盘进行读写的单位是扇区整数倍簇,根据磁盘容量不同,簇的容量也不同。
程序的运行环境
运行环境 = 操作系统 + 硬件
如上表,从英雄联盟所需的游戏配置可以看出,运行环境类出了硬件要求和操作系统的要求两类。
同一类型的硬件可以选择安装多种操作系统,根据应用的具体情况,有的只能在特定的操作系统上运行。
从上表可以看出,从程序运行环境这一角度来考量硬件时,CPU时特别重要的参数。CPU只能解释其自身固有的机器语言,例如CPU有x86、MIPS、SPARC、PowerPC等。(Windows系统克服了CPU以外的硬件差异)
操作系统的API
应用软件必须根据不同的操作系统的类型来开发,CPU的类型不同,对应的机器语言不同,操作系统的类型不同,应用想操作系统传递指令的途径也不同。
应用程序向操作系统传递指令的途径称为API,不同操作系统的API不同,因此将同一个应用程序移植到其它操作系统时必须重写应用中利用API的部分,例如同外部设备进行输入输出操作的功能。
BIOS和引导
程序的运行环境中存在这名为BIOS(Basic Input/Output System)的系统,它存储在ROM中,除了显卡、键盘等基本控制程序外,还有“引导程序”的功能。
计算机开机后,BIOS会确认硬件是否正常运行,没有问题就会启动引导程序。引导程序将硬盘记录的OS加载到内存中运行。
Bootstrap原意是指靴子上的“拔靴带”,BIOS这样小的程序可以启动操作系统这样大的程序,类似于Bootstrap的功能。
操作系统与应用
在操作系统的运行环境下,应用并不是直接控制硬件,而是通过操作系统简介控制硬件。程序中涉及的内存分配是面向操作系统的,操作系统接受到应用发出的指令后,首先对该指令进行解释,然后对时钟IC和显示器的I/O进行控制。
操作系统的硬件控制功能是通过一些笑的函数集合体的形式提供的,这些函数及调用函数的行为统称为系统调用,通过系统调用,就可以使用高级编程语言。
下面这个程序实现想文件中写入字符串“Hello World!”
# include "stdio.h"
int main()
{
// 打开文件
FILE *fp = fopen("test.txt", "a");
// 写入字符串
fputs("Hello World!", fp);
// 关闭文件
fclose(fp);
return 0;
}
文件是操作系统对磁盘媒介空间的抽象化。程序中并没有出现对硬件的直接操作,而是将整个流程抽象为了文件的打开、写入和关闭。
硬件控制方法
利用操作系统提供的系统调用功能可以实现对硬件的控制。
IN和OUT指令
Windows系统控制硬件借助的是输入输出指令,IN
通过指定端口号和端口输入数据,并将其存储在寄存器中,OUT
将寄存器中存储的数据输出到指定端口号的端口
IN 寄存器, 端口号
OUT 端口号, 寄存器
中断处理机制
IRQ(中断请求):用来暂停正在运行的程序,并跳转到其他程序运行的机制。
比如自己在刷知乎时有电话打进来,我们需要暂停知乎接电话。电话就相当于中断处理机制,如果电话不能起中断作用,就必须等到关闭知乎结束才能接电话,这样就不方便了。中断处理机制也是同样的道理。
实施中断请求的是连接外部设备的I/O控制器,负责实施中断处理程序的是CPU。外部设备请求会使用不同于I/O端口的编号,称为中断编号;假如有多个外设进行中断请求,中断控制器会将多个中断请求有序传递给CPU。
CPU接到中断请求后,会中断当前额主程序,切换到中断处理程序,具体步骤如下:
-
把CPU中所有寄存器的数值保存到栈中
-
完成外部设备的输入输出
-
把栈中保存的数值还原到CPU寄存器中
-
继续处理主程序
为了实时处理从外部设备输入的数据,大部分的外部设备会频繁发出中断请求。
DMA机制
DMA是指在不通过CPU的情况下,外部设备直接与主存进行数据传送,利用这一机制,大量数据可以在短时间内传送到主存。
I/O端口、IRQ、DMA是识别外部设备的三点组合。但是IRQ只对需要中断处理的外部设备才是必需的,DMA只对需要DMA机制的外部设备来说是必需的,加入多个外部设备设定了同样的端口号、IRQ、DMA通道,会出现设备冲突
标签:怎么,起来,程序,指令,内存,寄存器,磁盘,CPU 来源: https://www.cnblogs.com/euler0525/p/16525644.html