开篇补充
这篇文章大概写于两三个月前,但一直忘记发布文章。今天用了半小时把这篇文章大概浏览了下,依然感觉能学到很多东西,因为时隔两三个月有些内容忘的差不多了。但是因为之前写过这篇文章,所以再次回忆这些知识点还是很快的。
记得三个月前找工作面试时,很多面试官都会对着我的文章来提问。其中有一面试官,最开始问到我文章内容中涉及的几个知识,但是当时没想起来。进而面试官就带着质疑的态度说"这些文章是不是你写的"。笔者当时很生气,但未表现出丝毫。然后回答到:"是自己写的,只是时间久了有些内容记不清了"。面试官接着按笔者写的文章问了不少,基本回答出百分之八九十那样。说这件事,主要是想说"好记性不如烂笔头",但是却没多少人真正将这句话记到心中。倘若每个人都有着超凡的记忆力,那么是不是都要考清华北大。现在这样说,以后在面试过程中,若被面试官质疑文章是不是自己原创,笔者可能先会说上这些,然后让他继续再问些问题便可知晓。
有些知识点随着时间久远,很可能会记不清,但是当我翻看自己的博客文章,会以最快的速度想起那些知识点,毕竟文章的每一个字都是出自我自己的手,整个写作都是出自自己以往的思路。
前言
对于很多半路出家转行到 IT 行业的技术小白而言,说自己是程序员,但是请问问自己是否可以讲清楚什么是编程。敲了那么多代码,又能否解释清楚程序到底是怎样跑起来的(包括软件到硬件整个执行流程)。
是不是有点困难!没关系,笔者向你推荐一本很基础的书 《程序是怎样跑起来的》。笔者用了大概两天多的时间简单过了下这本书,现在对程序的整个运行流程有了简单的认识,包括CPU、虚拟内存、物理内存、操作系统、硬件和驱动等相关。整本书的内容通俗简单易懂,虽说深度不是很深,但很适合对计算机硬件一窍不懂的小白。如果有需要笔者这有电子版的书。
还是保持以前的习惯,学习后就要记录点内容。
一、CPU是什么
1.1 程序运行流程
- a. 程序员编写高级语言。
int a; a = 1+2; printf("%d",a);
- b、将程序编译后转换成机器语言的 EXE 文件。
010000101001010101010010101010100101010101010010101
- c、程序运行时,在内存中生成 EXE 文件的副本。
- d、CPU 解释并执行程序。
1.2 CPU结构
CPU 和内存都是有许多晶体组成的电子部件,通常称之为 IC(集成电路)。从功能方面来说,CPU 主要由四部分构成,其中控制器和运算器最为核心。
- 寄存器:用来暂存指令,数据等处理对象,可以将它看成内存的一种,CPU 内部会有 20-100 个寄存器。
- 控制器:负责把内存上的指令,数据读入寄存器,并根据指令的执行结果来控制整个计算机。
- 运算器:负责运算从内存读入寄存器的数据。
- 时钟:负责发出 CPU 开始计时的时钟信号。
程序启动后,根据时钟
信号,控制器
从内存中读取指令和数据,通过这些指令加以解释和运行,运算器
会对数据进行运算,控制器
根据该运算结果来控制计算机(所谓的控制就是指数据运算以外的处理,如:数据输入和输出事件的控制、键盘、显示器等的输入输出。
这里顺便说一下内存的概念: 通常说的内存是指计算机的主存储器,简称 主存,主存通过控制芯片等与 CPU 相连,主要负责存储指令和数据,主存由可读写的元素构成,每个字节(1个字节 =8 位)都带有一个地址编号,CPU 可以通过改地址读取主存中的指令和数据,当然也可以写入数据,但是需要注意的是,主存中存储的指令和数据会随着计算机的关机而自动清除。
1.3 CPU结构中的寄存器
CPU 是寄存器的集合体。不同类型的 CPU 内部寄存器的种类和数量都是不同的。不过,一般情况下可将寄存器大致分为八类:
- 累加寄存器: 存储执行运算的数据和运算后的数据 (1个)
- 标志寄存器: 存储运算处理后的 CPU 状态 (1个)
- 程序计数器: 存储下一条指令所在的内存的地址 (1个)
- 指令寄存器: 存储指令, CPU 内部使用,程序员无法通过程序对该寄存器进行读写操作。(1个)
- 栈寄存器: 存储栈区域的起始地址 (1个)
- 基址寄存器: 存储数据内存的起始地址
- 变址寄存器: 存储基址寄存器的相对地址
- 通用寄存器: 存储任意数据
程序计数器、累加寄存器、标志寄存器、指令寄存器和栈寄存器只有一个,而像基址寄存器
、变址寄存器
、通用寄存器
通常都有多个。
1.4 程序计数器决定程序流程
1.4.1 顺序执行
比如实现 123 和 456 两个数值相加,并显示到屏幕上。实际上,一个命令和数据通常被存储在多个地址山,但是这里为了便于说明,就把指令和数据假设分配到一个地址上。
地址 0100 是程序运行的开始位置。操作系统首先把程序复制到内存中,然后程序计数器(CPU寄存器的一种)设定为0100,便开始运行。CPU 每执行一个指令,程序计数器就加 1 。
1.4.2 条件分支和循环执行
条件分支根据条件执行任意地址的指令。循环则会重复执行同一地址的指令。
CPU 在进行运算时,
标志寄存器
的数值会根据运算结果自动设定,至于是否执行跳转指令,则由 CPU 参考标志寄存器的数值进行判断。
1.4.3 函数调用机制
如果只有顺序、分支、循环顺序执行函数,当执行到
c = MyFunc(a,b)
并进入该函数,当该函数执行到完,如何确定下一个地址?关于这个问题,机器语言的 call
指令和 return
指令能够解决当函数调用后执行后返回的问题。函数调用使用的是 call 指令 而不是跳转指令,在将函数的入口地址设定到程序计数器之前, call 指令会把调用函数后要执行的指令地址存储在名为栈的主存中,函数处理完毕后,再通过函数的出口来执行 return 命令, return 命令的功能是把保存在栈中的地址设定到程序计数器中。
二、关于二进制
2.1 用二进制表示计算机信息的原因
计算机内部是由 IC(集成电路) 这种电子部件构成的。IC有集中不同的醒转,有的像一条黑色蜈蚣,在其两侧有很多引脚。IC 的所有引脚,只有直流电压0V或5V两个状态,也就是说IC的一个引脚只能表示两个状态(0或1)。IC 的这个特性决定了计算机的信息数据只能用二进制来处理。虽然二进制并不是专门为IC而设计的,但是和IC的特性非常吻合。计算机最小的处理单位是---位,每个引脚相当于二进制中的一位,1 字节 = 8位。
2.2 原码、反码、补码
- 正数: 反码 = 原码 = 补码
- 负数:
反码 = 其原码除符号之外的各位求反
补码 = 反码 + 1
正数的原码、反码、补码
原码: 01011
反码: 01011
补码: 01011
负数的原码、反码、补码
原码: 11011
反码: 10100
补码: 10101
另外,有一点非常重要的,计算机中,数据一律通过补码来存储。
2.3 乘除运算与移位运算
00100111 左移两位的结果是 10011100,用十进制表示的话,从39变为156,是之前的 4 倍。类比的话,十进制左移会变为原来的10倍、100倍.....,二进制左移会变为原来的2倍、4倍、8倍.....反之,右移会变为原来的1/2、1/4.......如此就能解释为什么移位运算可以代替乘除法。
2.4 计算机进行小数运算原因
2.4.1 如何用二进制表示小数
先看看小数如何通过二进制小数表示。实际上适合表示整数的方式类似。具体请参照下图。
2.4.2 计算机运算出错的原因
计算机之所以会出现运算错误的原因是因为一些小数无法转换二进制数,例如 0.1 ,就无法用二进制数正确表示,小数点后面即使有几百位也无法表示。按照下表的规律可知道,十进制0.1 转换成二进制后,会变成0.00011001100......(1100)会这样一直循环下去。这种情况就像1/3无法用十进制来表示一样。
2.4.3 如何避免计算机出错
把小数转成整数计算。计算机在进行小数计算时可能会出错,但是在计算整数的时候,只要不超过可处理数值的范围一定不会出现问题。
三、内存
通常说的内存是指计算机的主存储器,简称 主存,主存通过控制芯片等与 CPU 相连,主要负责存储指令和数据,主存由可读写的元素构成,每个字节(1个字节 =8 位)都带有一个地址编号,CPU 可以通过改地址读取主存中的指令和数据,当然也可以写入数据,但是需要注意的是,主存中存储的指令和数据会随着计算机的关机而自动清除。
3.1 内存的物理机制
内存实际上是一种名为内存 IC 的电子元件,内存 IC 中有电源、地址信号、数据信号、控制信号等用于输入输出的大量引脚(IC 的引脚),通过为其制定地址,来进行数据的读写。
3.2 内存的逻辑模型
char a;
short b;
long c;
a = 123;
b = 123;
c = 123;
a 表示一个字节长度的 char,b 表示 2 个字节的short,c 表示 4 个字节的 long。下图的地址从上往下变大,但实际也会有相反的情况。
四、磁盘和内存的关系
4.1 不读入内存就无法运行
磁盘中存储的程序,必须要加载到内存中才能运行,在磁盘中保存的原始程序是无法直接运行的,这是因为,负责解析。
4.2 磁盘缓存加快了磁盘访问速度
磁盘缓存
是从磁盘中读取的数据存储在内存空间。如此,当接下来需要读取同一数据,就可以直接从内存中读取,而不用再次通过磁盘读取。磁盘缓存
这种方式可以加快磁盘数据的访问速度。
4.3 虚拟内存
虚拟内存
是指把磁盘的一部分作为假想的内存来使用。虚拟内存实际是假想的内存(实际上是磁盘)。借助虚拟内存
,在内存不足时也可以运行程序。例如在只剩下 5MB 内存空间的情况下也能运行 10MB 大小的程序,由于 CPU 只能执行加载到内存中的程序,虚拟内存
虽说是把磁盘作为内存的一部分来使用,但实际上正在运行的程序部分,在这个时间点上必须存在在内存中,也就是说,为了实现虚拟内存,就必须把实际内存
的内容和磁盘上的虚拟内存
的内容进行部分置换(swap),并同时运行程序。通常情况下,PC端都有swap机制,所以一般情况下,PC端的应用不会被杀死。但是移动端却不同,由于没有swap机制,为了运行更多的程序,只能选择杀死之前的程序。
为了实现虚拟内存功能, Windows 在磁盘上提供了虚拟内存用的文件(page file, 页文件),该文件由 Windows 自动做成和管理,文件的大小也就是虚拟内存的大小,通常是实际内存的相同程度至两倍程度。
4.4 系统节约内存的方法
通过DLL(Dynamic Link Library)文件,在程序运行时动态加载Library。多个应用可以共用一个DLL文件,达到节约内存的效果。
4.5 磁盘的物理结构
磁盘是通过把其物理表面划分成多个空间来使用的,划分的方式有扇区方式和可变长方式,前者指将磁盘划分为固定长度的空间,后者则是把磁盘划分为长度可变的空间,扇区方式中,把磁盘的表面划分成若干个同心圆空间的就是磁道,把磁道按照固定大小(能存储的数据长度相同)划分而成的空间就是扇面。
扇区是磁盘的最小读写单位,一般一个扇区是512字节。磁盘读写的单位是扇区整数倍的
簇
(1簇可以是1扇区既512字节、也可以是2扇区既1KB.......以此类推)。通常磁盘的容量越大,簇的容量也越大。
- 不同的文件文件不能存在同一个簇中,否则会导致乙方的文件不能被删除。
- 不管多么小的文件,至少会占1簇的空间。其他文件所占的空间都是簇的整数倍。
五、文件压缩
关于这个就不结合这本书的内容来说了,之前写过一篇相关文章,可做简单参考。
六、程序的运行环境
程序的运行环境 = 操作系统 + 硬件
七、从源文件到可执行文件
关于这个就不结合这本书的内容来说了,之前写过一篇相关文章,可做简单参考。注意文中提到的编译的前后端问题。
八、操作系统和应用的关系
8.1 操作系统是多个应用的集合
在计算机不存在操作系统一说的年代,开发人员用机器语言编写程序,然后使用硬件开发将程序输入,这一过程非常麻烦。于是,有人开发出仅具有加载和运行功能的监控程序
,这就是操作系统的原型。通过实现启动监控程序,程序员就可以根据需要将各种程序加载到内存中运行。
随着后期的发展,人们意思到很多程序都有共通的部分。如通过键盘输入文字、显示器输出文字等。如果每个程序都额外加上这些相同的处理,就太浪费时间了。所以,后来就将这些基本的输入输出等程序加到监控程序中。初期的操作系统就这样诞生了。
8.2 要意识到操作系统的存在
之所以说要意识到操作系统的存在,是因为程序员在开发应用,而不是在开发硬件。因为操作系统的存在,程序员无需考虑硬件问题,哪怕是对硬件不懂的人,也同样能开发出有模有样的应用。因为应用不是直接控制硬件,而是通过操作系统来间接控制硬件。
九、关于汇编
9.1 汇编语言和本地代码一一对应
高级语言经过编译器被翻译成汇编语言,这个过程是一条高级语言可能被翻译成多条汇编语言。而汇编语言经过汇编器被翻译成机器语言,这个过程是1条汇编语言翻译成1条二进制的机器语言。计算机 CPU 能直接解释运行的只有本地代码程序,用 C 语言等编写的代码,需要通过各自的编译器编译后,转换成本地代码。
9.2 汇编语言的语法是‘操作码+操作数’
在汇编语言中,一行表示对 CPU 的一个指令。汇编语言指令的语法结构是操作码+操作数。操作码表示的是指令动作,操作数表示的是指令对象。操作码和操作数罗列在一起的语法,就是一个英文的指令文本。操作码是动词,操作数相当于宾语。如:Give me money, Give 相当于操作码,me和money就是操作数。汇编语言中如果存在多个操作数,就用逗号分割开,就像 Give me, money 这样。
9.3 对栈进行 push 和 pop
程序运行时,会在内存上申请分配一个称为栈的数据空间,数据在存储时是从内存的下层(大号地址)逐渐往上层累加的,读出时是按照从上往下的顺序进行的。栈是存储临时数据的区域,32位 x86 系列的 CPU 中,进行一次 push 或 pop操作,即可处理32位(4字节)的数据。
push 和 pop 指令中只有一个操作数,该操作数表示的是 push 的是什么或 pop 的是什么,而不需要指定对哪一个地址编号的内存进行 push 和 pop 。这是因为,对栈进行读写的内存地址编号是由 esp 寄存器(栈指针)进行管理的。push 指令和 pop指令运行后,esp 寄存器的值会自动进行更新,push指令减4,pop命令是加4,因而程序员就没有必要指定内存地址了。
9.4 函数调用机制
汇编语言中,函数名表示的是函数所在的内存地址,当 call 命令调用的函数运行结束后,程序流程会返回编号 (6) 的这一行。 call 指令运行开始后, call 指令的内部执行方法的内存地址 (6 的这一行) 会自动 push 入栈,该值会在 AddNum 函数处理完成后,最后通过 ret 指令 pop 出栈,然后流程回到编号为 6 这一行。
9.5 函数内部的处理
函数是的参数是通过栈来传递的,返回值是通过寄存器来返回的。
ebp 寄存器的值在(1)中入栈,在(5)中出栈,主要是为了把函数中用到的 ebp 寄存器的内容,恢复到函数调用之前的状态。在进入函数处理之前,无法确定 ebp 寄存器用到了什么地方,但由于函数内部也会用到 ebp 寄存器,所以就暂时将改值保存起来。
(2)中把负责管理地址的 esp 寄存器的值赋到了 ebp 寄存器中,这是因为 mov 指令中方括号的参数,是不允许指令 esp 寄存器的。因此这里就采用不直接通过 esp ,而是用 ebp 寄存器来读写栈内容的方法。
(6)中 ret 指令运行后,函数返回目的地的内存地址会自动出栈。跳出函数内部。
9.6 全局变量和临时变量
编译后的程序,会被归类到名为段定义的组,初始化的全局变量,会被定义到名为 _DATA 的段定义中,没有初始化的全局变量会被汇总到 _BSS 的段定义中,指令会被汇总到名为 _TEXT 的段定义中。
局部变量临时保存在寄存器和栈中,所以局部变量只能在定义该函数的内部进行参阅。函数内部利用栈,在函数处理完毕后会恢复到初始状态,因此局部变量的值也就被销毁了。而寄存器也可能会被用于其他目的,因此,局部变量只是在函数处理运行期间临时存储在寄存器和栈上。
十、硬件控制方法
10.1 应用和硬件无关
应用不是直接控制硬件,而是通过操作系统来间接控制硬件。所以说应用和硬件无关。
10.2 支持硬件输入和输出的IN和OUT指令
- IN指令通过端口号的端口输入数据,并将其存储在 CPU 内部的寄存器中。
- OUT 指令则是把 CPU 寄存器中的存储的数据,输出到指定端口号的端口。
什么是端口?端口号?I/O控制器?
计算机主机,附带了显示器、键盘等外围设备的连接器
。连接器
内部都有用来交换计算机同外围设置之间电流特性的I/O控制器
。I/O控制器
之所以存在,是为了解决因电压不同,数字信号和模拟信号的电流特性也不同,计算机主机和外围设备无法直接相连的问题。 而I/O控制器
中有用于临时保存输入输出数据的内存,该内存就是端口。I/O控制器
内部的内存也称为寄存器,但是该寄存器不同于 CPU 内部的寄存器。CPU 内部的寄存器主要是用来计算,而这里的寄存器主要是用来临时存储数据。 一个I/O控制器
可以控制一个也可以控制多个外围设备,各端口之间通过端口号区分,端口号也称为I/O地址
。
10.3文字和图片显示机制
简单一句话概括:显示器中显示的信息一直存储在某内存中,该内存称为VRAM(Video RAM)。只要往VRAM中写入数据,数据就会显示出来。
在MS_DOS时代,VRAM是主内存的一部分。不过当时因为VRAM内存空间太小,最多只能有16中颜色。现代计算机,显卡等专用硬件中一般都配置与主内存独立的VRAM和GPU。
额外扩充
计算机系统性能指标
基本字长
即一次数据操作的基本位数,通常是4位、8位、16位、64位等,它会影响到计算的精度、指令的功能。位数越大,计算精度越高,指令越丰富,性能越好。如32位和64位操作系统相比。运算速度不同:64位CPU GPRs(General-Purpose Registers,通用寄存器)的数据宽度为64位,64位指令集可以运行64位数据指令,也就是说处理器一次可提取64位数据(只要两个指令,一次提取8个字节的数据),比32位(需要四个指令,一次提取4个字节的数据)提高了一倍,理论上性能会相应提升1倍;寻址能力不同:64位处理器的优势还体现在系统对内存的控制上。由于地址使用的是特殊的整数,因此一个ALU(算术逻辑运算器)和寄存器可以处理更大的整数,也就是更大的地址。比如,Windows Vista x64 Edition支持多达128 GB的内存和多达16 TB的虚拟内存,而32位CPU和操作系统最大只可支持4G内存。CPU的性能指标
cpu的主频=外频*倍频系数;
IPS表示每秒执行指令数;
FLOPS表示每秒执行浮点运算的次数;天河2号实测速度为33.86PFLOPS。
CPU的功耗(动态功耗和静态功耗),动态功耗是指实际运行计算产生的功耗,静态功耗是指半导体材料在电流流动中的泄露和挥发;存储器的容量
内存容量和外存容量