一文读懂计算机程序的执行过程

要想清楚的了解程序的执行过程就要先知道计算机的基本结构

2. 图灵和图灵机

阿兰·图灵于1912年出生于英国,擅长数理逻辑学和计算理论。

阿兰.图灵

图灵机是阿兰·图灵在24岁时提出的,发表在论文《论可计算数及其在判定问题中的应用》。图灵的基本思想是用机器来模拟人们用纸笔进行数学运算的过程,而且还定义了计算机由哪些部分组成,程序又是如何执行的。

图灵机长什么样子呢?示意图如下:

基本组成部分:

 

  • 「纸带」:纸带由一个个连续的格子组成,每个格子可以写入字符,纸带就好比内存,而纸带上的格子的字符就好比内存中的数据或程序;
  • 「读写头」:读写头可以顺序读取纸带上格子的字符,也可以把字符写入到纸带的格子;
  • 读写头部件:如存储单元、控制单元以及运算单元:1、存储单元用于存放数据;2、控制单元用于识别字符是数据还是指令,以及控制程序的流程等;3、运算单元用于执行运算指令;

 

图灵机只是一个抽象的模型,这个理想的模型是好,但是理想终归是理想,想要成为现实,我们得想其它办法。于是在此基础上,冯洛伊曼提出了电子计算机使用二进制数制系统和储存程序,并按照程序顺序执行,即冯诺依曼体系结构。

3. 冯诺依曼模型

在 1945 年冯诺依曼和其他计算机科学家们提出了计算机具体实现的报告,其遵循了图灵机的设计,而且还提出用电子元件构造计算机,并约定了用二进制进行计算和存储。

根据冯诺依曼体系结构构成的计算机,必须具有如下功能:

· 把程序和数据装入到计算机中;

· 必须具有长期记住程序、数据的中间结果及最终运算结果;

· 完成各种算术、逻辑运算和数据传送等数据加工处理;

· 根据需要控制程序走向,并能根据指令控制机器的各部件协调操作;

· 能够按照要求将处理的数据结果显示给用户。

为了完成上述的功能,计算机必须具备五大基本组成部件:

· 装载数据和程序的输入设备;

· 记住程序和数据的存储器;

· 完成数据加工处理的运算器;

· 控制程序执行的控制器;

· 显示处理结果的输出设备。

因此,定义计算机基本结构就需要 5 个部分:运算器、控制器、存储器、输入设备、输出设备,这 5 个部分也被称为冯诺依曼模型

冯诺依曼体系结构是现代计算机的基础。在该体系结构下,程序和数据统一存储,指令和数据需要从同一存储空间存取,经由同一总线传输,无法重叠执行。根据冯诺依曼体系,CPU的工作分为以下 5 个阶段:取指令阶段、指令译码阶段、执行指令阶段、访存取数和结果写回。

· 取指令(IF,instruction fetch),即将一条指令从主存储器中取到指令寄存器的过程。程序计数器中的数值,用来指示当前指令在主存中的位置。当 一条指令被取出后,程序计数器(PC)中的数值将根据指令字长度自动递增。

· 指令译码阶段(ID,instruction decode),取出指令后,指令译码器按照预定的指令格式,对取回的指令进行拆分和解释,识别区分出不同的指令类 别以及各种获取操作数的方法。现代CISC处理器会将拆分已提高并行率和效率。

· 执行指令阶段(EX,execute),具体实现指令的功能。CPU的不同部分被连接起来,以执行所需的操作。

· 访存取数阶段(MEM,memory),根据指令需要访问主存、读取操作数,CPU得到操作数在主存中的地址,并从主存中读取该操作数用于运算。部分指令不需要访问主存,则可以跳过该阶段。

· 结果写回阶段(WB,write back),作为最后一个阶段,结果写回阶段把执行指令阶段的运行结果数据“写回”到某种存储形式。结果数据一般会被写到CPU的内部寄存器中,以便被后续的指令快速地存取;许多指令还会改变程序状态字寄存器中标志位的状态,这些标志位标识着不同的操作结果,可被用来影响程序的动作。

在指令执行完毕、结果数据写回之后,若无意外事件(如结果溢出等)发生,计算机就从程序计数器中取得下一条指令地址,开始新一轮的循环,下一个指令周期将顺序取出下一条指令。 许多复杂的CPU可以一次提取多个指令、解码,并且同时执行。

运算器、控制器是在中央处理器里的,存储器就我们常见的内存,输入输出设备则是计算机外接的设备,比如键盘就是输入设备,显示器就是输出设备。

存储单元和输入输出设备要与中央处理器打交道的话,离不开总线。所以,它们之间的关系如下图:

接下来,分别介绍内存、中央处理器、总线、输入输出设备。

3.1. 内存

简称(RAM)专业的说内存是计算机中重要的部件之一,它是与CPU进行沟通的桥梁。计算机所有的程序都是在内存中运行的,如果没有内存你的电脑将开不了机。

在计算机数据存储中,存储数据的基本单位是字节(byte),1 字节等于 8 位(8 bit)。每一个字节都对应一个内存地址。

内存的地址是从 0 开始编号的,然后自增排列,最后一个地址为内存总字节数 - 1,这种结构好似我们程序里的数组,所以内存的读写任何一个数据的速度都是一样的。

RAM的优点:是存取速度快、读写方便。缺点:是数据不能长久保持,断电后自行消失,因此主要用于计算机主存储器等要求快速存储的系统。按工作方式不同,可分为静态和动态两类。

静态随机存储器(SRAM)的单元电路是触发器,存入的信息在规定的电源电压下便不会改变。SRAM速度快,使用方便。

动态随机存储器(DRAM)的单元由一个金属-氧化物-半导体(MOS)电容和一个MOS晶体管构成,数据以电荷形式存放在电容之中,需每隔2~4毫秒对单元电路存储信息重写一次(刷新)。DRAM存储单元器件数量少,集成度高,应用广泛。

3.2. 中央处理器

CPU:中央处理器,由运算器和控制器组成。计算机解决某个问题时要为它编写程序,它告诉计算机要执行什么操作,在什么地方来找到用来操作的数据一旦把程序加载到内存储器,CPU的基本工作是执行存储的指令序列,即程序。程序的执行过程实际上是不断地取出指令、分析指令、执行指令的过程。

CPU从存放程序的主存储器里取出一条指令,译码并执行这条指令,保存执行结果,紧接着又去取指令,译码,执行指令……,如此周而复始,反复循环,使得计算机能够自动地工作。除非遇到停机指令,否则这个循环将一直进行下去。其过程如图所示

CPU 中的寄存器主要作用是存储计算时的数据,你可能好奇为什么有了内存还需要寄存器?原因很简单,因为内存离 CPU 太远了,而寄存器就在 CPU 里,还紧挨着控制单元和逻辑运算单元,自然计算时速度会很快。

常见的寄存器种类:

 

  • 通用寄存器,用来存放需要进行运算的数据,比如需要进行加和运算的两个数据。
  • 程序计数器,用来存储 CPU 要执行下一条指令「所在的内存地址」,注意不是存储了下一条要执行的指令,此时指令还在内存中,程序计数器只是存储了下一条指令「的地址」。
  • 指令寄存器,用来存放当前正在执行的指令,也就是指令本身,指令被执行完成之前,指令都存储在这里。
3.3. 总线

 

总线(Bus),是指计算机设备和设备之间传输信息的公共数据通道。在城市中,车辆通行需要道路,需要交通。在计算机中,同样需要在各部件之间传输信息的通路,信息通过电线束从一个计算机部件到另一个部件,就称作总线,它是一种信号传递的布线方式。总线是用于 CPU 和内存以及其他设备之间的通信,总线按照不同的分类标准可以有不同的分类。按照传递的数据分可分为 3 种:

 

  • 地址总线,用于指定 CPU 将要操作的内存地址;
  • 数据总线,用于读写内存的数据;
  • 控制总线,用于发送和接收信号,比如中断、设备复位等信号,CPU 收到信号后自然进行响应,这时也需要控制总线;

 

当 CPU 要读写内存数据的时候,一般需要通过下面这三个总线:

 

  • 首先要通过「地址总线」来指定内存的地址;
  • 然后通过「控制总线」控制是读或写命令;
  • 最后通过「数据总线」来传输数据;

 

按照在计算机所在区域分:内部总线,系统总线,外部总线。按实现方式分:并行总线,串行总线。

3.4. 输入、输出设备

输入输出设备(IO设备),是数据处理系统的关键外部设备之一,可以和计算机本体进行交互使用。输入设备向计算机输入数据,计算机经过计算后,把数据输出给输出设备。期间,如果输入设备是键盘,按下按键时是需要和 CPU 进行交互的,这时就需要用到控制总线了。

4. 线路位宽与 CPU 位宽

我们经常听到32位或64位计算机,32 位和 64 位 CPU 最主要区别在于一次能计算多少字节数据:

 

  • 32 位 CPU 一次可以计算 4 个字节;
  • 64 位 CPU 一次可以计算 8 个字节;

 

这里的 32 位和 64 位,通常称为 CPU 的位宽。

线路位宽:数据是如何通过线路传输的呢?其实是通过操作电压,低电压表示 0,高压电压则表示 1。为了避免低效率的串行传输的方式,线路的位宽最好一次就能访问到所有的内存地址

CPU 要操作的内存地址空间就需要相应的地址总线:

 

  • 如果地址总线只有 1 条,那每次只能表示 「0 或 1」这两种地址,所以 CPU 能操作的内存地址最大数量为 2(2^1)个(注意,不要理解成同时能操作 2 个内存地址);
  • 如果地址总线有 2 条,那么能表示 00、01、10、11 这四种地址,所以 CPU 能操作的内存地址最大数量为 4(2^2)个。

 

那么,想要 CPU 操作 4G 大的内存,那么就需要 32 条地址总线,因为 2 ^ 32 = 4G。

CPU 位宽:CPU 的位宽最好等于线路位宽,比如 32 位 CPU 控制 40 位宽的地址总线和数据总线的话,处理比较麻烦。所以 32 位的 CPU 最好和 32 位宽的线路搭配,64 位的 CPU 最好和 64 位宽的线路搭配。但是并不代表 64 位 CPU 性能比 32 位 CPU 高很多,很少应用需要算超过 32 位的数字,所以如果计算的数额不超过 32 位数字的情况下,32 位和 64 位 CPU 之间没什么区别的,只有当计算超过 32 位数字的情况下,64 位的优势才能体现出来。另外,32 位 CPU 最大只能操作 4GB 内存,就算你装了 8 GB 内存条,也没用实质的作用。而 64 位 CPU 寻址范围则很大,理论最大的寻址空间为 2^64。

5. 一个程序的生与死

程序实际上就是把我们现实中的一个生活场景,通过计算机的编程语言搬到电脑中的一种实现方式罢了。

程序实际上是一条一条指令,负责执行指令的就是 CPU 了。一个程序执行的时候,CPU 会根据程序计数器里的内存地址,从内存里面把需要执行的指令读取到指令寄存器里面执行,然后根据指令长度自增,开始顺序读取下一条指令。

现代大多数 CPU 都使用来流水线的方式来执行指令,所谓的流水线就是把一个任务拆分成多个小任务,于是一条指令通常分为 4 个阶段,称为 4 级流水线,如下图:

四个阶段的具体含义:取得指令,指令译码,执行指令,数据回写。上面这 4 个阶段,我们称为指令周期(Instrution Cycle),CPU 的工作就是一个周期接着一个周期,周而复始。

每个CPU都有一套自己可以执行的专门的指令集。接下来我们选用最简单的 MIPS 指集,来看看机器码是如何生成的。MIPS 的指令是一个 32 位的整数,高 6 位代表着操作码,表示指令类型,剩下的 26 位根据不同指令类型所表示的内容也就不相同,总结下来主要有三种类型R、I 和 J。

具体含义:

 

  • R 指令:用在算术和逻辑操作,里面有读取和写入数据的寄存器地址。如果是逻辑位移操作,后面还有位移操作的「位移量」,而最后的「功能码」则是再前面的操作码不够的时候,扩展操作码来表示对应的具体指令的;
  • I 指令:用在数据传输、条件分支等。这个类型的指令,就没有了位移量和功能码,也没有了第三个寄存器,而是把这三部分直接合并成了一个地址值或一个常数;
  • J 指令:用在跳转,高 6 位之外的 26 位都是一个跳转后的地址;

 

编译器在编译程序的时候,会构造指令,这个过程叫做指令的编码。CPU 执行程序的时候,就会解析指令,这个过程叫作指令的解码。

CPU需要使用一个叫做存储器(也就是各种寄存器)的东西保存输入和输出数据。以下是几种常见的寄存器

· MAR: memory address register,保存将要被访问数据在内存中哪个地址处,保存的是地址值

· MDR: memory data register,保存从内存读取进来的数据或将要写入内存的数据,保存的是数据值

· AC: Accumulator,保存算术运算和逻辑运算的中间结果,保存的是数据值

· PC: Program Counter,保存下一个将要被执行指令的地址,保存的是地址值

· CIR: current instruction register,保存当前正在执行的指令

关于CPU上的高速缓存

 

  • 最高速的缓存是CPU的寄存器,它们和CPU的材料相同,最靠近CPU或最接近CPU,访问它们没有时延(<1ns)。但容量很小,小于1kb。32bit:32*32比特=128字节,64bit:64*64比特=512字节
  • 寄存器之下,是CPU的高速缓存。分为L1缓存、L2缓存、L3缓存,每层速度按数量级递减、容量也越来越大

 

指令的类型

指令从功能角度分为 5 大类:

 

  • 数据传输:比如 store/load 是寄存器与内存间数据传输的指令,mov 是将一个内存地址的数据移动到另一个内存地址的指令;
  • 运算类型:比如加减乘除、位运算、比较大小等等,它们是二元运算;
  • 跳转类型:通过修改程序计数器的值来达到跳转执行指令的过程,比如编程中常见的 if-else、switch-case、goto函数调用等。
  • 信号类型:比如发生中断的指令 trap;
  • 闲置类型:比如指令 nop,执行后 CPU 会空转一个周期;

 

指令的执行速度

CPU 的硬件参数都会有 GHz 这个参数,比 1 GHz 的 CPU指的是时钟频率是 1 G,代表着 1 秒会产生 1G 次数的脉冲信号,每一次脉冲信号高低电平的转换就是一个周期,称为时钟周期。

对于 CPU 来说,在一个时钟周期内,CPU完成一个指令时钟频率越高,时钟周期就越短,工作速度也就越快。

一个时钟周期一定能执行完一条指令吗?答案是不一定的,大多数指令不能在一个时钟周期完成,通常需要若干个时钟周期。不同的指令需要的时钟周期是不同的,加法和乘法都对应着一条 CPU 指令,但是乘法需要的时钟周期就要比加法多。

程序执行耗费的 CPU 时间少就说明程序执行的快。对于程序的 CPU 执行时间,我们可以拆解成 CPU 时钟周期数(CPU Cycles)和时钟周期时间(Clock Cycle Time)的乘积

时钟周期时间就是 CPU 主频,主频越高说明 CPU 的工作速度就越快,比如电脑的 CPU 是 2.4 GHz 四核 Intel Core i5,这里的 2.4 GHz 就是电脑的主频,时钟周期时间就是 1/2.4G。

要想 CPU 跑的更快,自然缩短时钟周期时间,对于 CPU 时钟周期数我们可以进一步拆解成:「指令数 x 每条指令的平均时钟周期数(Cycles Per Instruction,简称 CPI)」,于是程序的 CPU 执行时间的公式可变成如下:

因此,要想程序跑的更快,优化这三者即可:

 

  • 指令数,表示执行程序所需要多少条指令,以及哪些指令。这个依赖编译器的优化。
  • 每条指令的平均时钟周期数 CPI,表示一条指令需要多少个时钟周期数,现代大多数 CPU 通过流水线技术(Pipeline),让一条指令需要的 CPU 时钟周期数尽可能的少;
  • 时钟周期时间,表示计算机主频,取决于计算机硬件。

 

当我们输入以下程序,编译运行,计算机从屏幕输出Hello,World!整个过程计算机都怎么运作的呢?

计算机内部存储的是0和1,计算机通过位信息以及上下文来解读这些0、1信息的。Hellow,World是由0和1组成的序列,将这些程序代码转换成相应的文本字符,每8位表示一个字节,用来存储一个字符。

Hello,World的ASCII码表示

因为我们输入的Hello,World是人可以阅读和编写的,但是机器并不能直接识别,它需要把这些文字翻译成机器可执行的二进制文件,这一部分的工作是由编译系统完成的。编译系统由预处理器、编译器、汇编器、连接器四部分组成。以hello, world程序为例,各部分共同完成将源文件编译成二进制可执行文件。各个部分完成的具体工作如下:

 

  • 预处理器:根据以#开头的命令,将包含的头文件加载进入源程序源程序。预处理器是一个文本处理程序,它在程序编译的第一个阶段处理源代码的文本。当然预处理器不只是编译之前才被调用处理源代码,它也可以被其他程序单独的调用以实现文本的处理。
  • 编译器:将预处理后的.i文件转换成汇编程序。编译器将不同的高级语言(如c语言,C++语言)转换成严格一致的汇编语言格式进行输出。汇编语言以标准的文本格式确切的描述每机器语言指令。编译器得到的文件通常以.s为后缀保存。
  • 汇编器:将汇编语言(.s文件)翻译成机器语言指令,并将这些指令打包成一种可定位目标程序格式。汇编后得到的文件即为二进制文件,通常以.o为后缀。
  • 链接器:Hello, World程序中调用过printf函数,它是一个c标准库里的函数。Printf函数存放在一个名为printf.o的单独预编译的文件中。而这个文件必须以适当的方式并入到我们的程序中,这个工作由链接器完成。将外部的.o文件并入后,得到一个完整的hello, world可执行文件。可执行文件加载到存储器后,由系统复制执行。

 

程序加载进入CPU的过程

Shell:命令行解释器。当用户输入一行命令后,shell先判断它是不是一个shell内置命令;如果不是,shell会假定为一个可执行文件的名字,直接去加载并执行该文件。因此,当我们通过编译系统将源文件编译成可执行二进制文件后,在shell中输入我们得到的可执行二进制文件名,shell将其从磁盘中加载到主存当中,通过CPU进行解释运行,最终通过终端设备(屏幕)将他显示出来,程序运行结束。

主内存对指令的处理分为多级缓存,其中比较重要的就是各类寄存器。寄存器是中央处理器内主要组成结构成分,它是CPU当中有限存贮容量的高速存贮部件,它在工作时能将计算机指令数据进行暂时的存储。

内存地址=基质+变址

CPU中的主要寄存器:

 

  • 累加寄存器(AC) :主要进行加法运算。
  • 标志寄存器(PSW) :记录状态,做逻辑运算。
  • 程序计数器(PC) :是用于存放下一条指令所在单元的地址的地方。
  • 基质寄存器(BX) :储存当前数据内存开始的位置。
  • 变址寄存器 :储存基质寄存器的相对位置。
  • 通用寄存器(GPRs) :支持有所的用法。
  • 指令寄存器(IR) :CPU专用,储存指令。
  • 堆栈寄存器(SP) :记录堆栈的起始位置。

 

处理器读取并解释存储在存储器中的指令处理器的操作主要是围绕程序计数器、算术/逻辑运算单元、主存来进行运作的。处理器首先从PC所指向的主存存储单元读取指令,解释指令中的位,执行该指令指示的简单操作,然后更新PC寄存器,使其指向下一条要执行的指令。CPU会执行的操作有:

加载:把一个字节或一个字从主存复制到寄存器,覆盖掉寄存器中原来的值。

存储:把一个字节或一个从寄存器复制到主存,并覆盖主存中原来的值。

操作:把两个寄存器的内容复制到ALU,ALU对两个字做算术运算后存回其中的一个寄存器,该寄存器中原来的值会被覆盖。

跳转:从cpu执行的指令抽取一个字的内容存入PC,覆盖掉原来的值,从而改变下一条要执行的指令,达到跳转的目的。

Hello,World程序首先被加载,从磁盘中复制到寄存器中,寄存器将Hello,World程序复制到主存中进行存储。程序运行过程中,CPU执行Hellow,World机器指令,指令的结果是将”Hellow,World”字符由内存复制到寄存器,寄存器再将结果复制到显示设备上显示出来。

下面我们详细的给出一个代码段在内存中的存储示意图。

更多详细信息,请您微信关注“计算网”公众号: