咱们每一个程序员或许都有一个梦,那就是成为大牛,咱们或许都沉浸在各类框架中,觉得框架就是一切,觉得应用层才是最重要的,你错了。在当今计算机行业中,会应用是基本素质,若是你懂其原理才能让你在行业中走的更远,而计算机基础知识又是重中之重。下面,跟随个人脚步,为你介绍一下计算机底层知识。html
还不了解 CPU 吗?如今就带你了解一下 CPU 是什么git
CPU 的全称是 Central Processing Unit
,它是你的电脑中最硬核
的组件,这种说法一点不为过。CPU 是可以让你的计算机叫计算机
的核心组件,可是它却不能表明你的电脑,CPU 与计算机的关系就至关于大脑和人的关系。CPU 的核心是从程序或应用程序获取指令并执行计算。此过程能够分为三个关键阶段:提取,解码和执行。CPU从系统的主存中提取指令,而后解码该指令的实际内容,而后再由 CPU 的相关部分执行该指令。程序员
下图展现了通常程序的运行流程(以 C 语言为例),能够说了解程序的运行流程是掌握程序运行机制的基础和前提。算法
在这个流程中,CPU 负责的就是解释和运行最终转换成机器语言的内容。数据库
CPU 主要由两部分构成:控制单元
和 算术逻辑单元(ALU)
编程
CPU 是计算机的心脏和大脑,它和内存都是由许多晶体管组成的电子部件。它接收数据输入,执行指令并处理信息。它与输入/输出(I / O)设备进行通讯,这些设备向 CPU 发送数据和从 CPU 接收数据。windows
从功能来看,CPU 的内部由寄存器、控制器、运算器和时钟四部分组成,各部分之间经过电信号连通。数组
寄存器
是中央处理器内的组成部分。它们能够用来暂存指令、数据和地址。能够将其看做是内存的一种。根据种类的不一样,一个 CPU 内部会有 20 - 100个寄存器。控制器
负责把内存上的指令、数据读入寄存器,并根据指令的结果控制计算机运算器
负责运算从内存中读入寄存器的数据时钟
负责发出 CPU 开始计时的时钟信号在 CPU 的四个结构中,咱们程序员只须要了解寄存器
就能够了,其他三个不用过多关注,为何这么说?由于程序是把寄存器做为对象来描述的。缓存
不一样类型的 CPU ,其内部寄存器的种类,数量以及寄存器存储的数值范围都是不一样的。不过,根据功能的不一样,能够将寄存器划分为下面这几类安全
种类 | 功能 |
---|---|
累加寄存器 | 存储运行的数据和运算后的数据。 |
标志寄存器 | 用于反应处理器的状态和运算结果的某些特征以及控制指令的执行。 |
程序计数器 | 程序计数器是用于存放下一条指令所在单元的地址的地方。 |
基址寄存器 | 存储数据内存的起始位置 |
变址寄存器 | 存储基址寄存器的相对地址 |
通用寄存器 | 存储任意数据 |
指令寄存器 | 储存正在被运行的指令,CPU内部使用,程序员没法对该寄存器进行读写 |
栈寄存器 | 存储栈区域的起始位置 |
其中程序计数器、累加寄存器、标志寄存器、指令寄存器和栈寄存器都只有一个,其余寄存器通常有多个。
下面就对各个寄存器进行说明
程序计数器(Program Counter)
是用来存储下一条指令所在单元的地址。
程序执行时,PC的初值为程序第一条指令的地址,在顺序执行程序时,控制器
首先按程序计数器所指出的指令地址从内存中取出一条指令,而后分析和执行该指令,同时将PC的值加1指向下一条要执行的指令。
咱们仍是以一个事例为准来详细的看一下程序计数器的执行过程
这是一段进行相加的操做,程序启动,在通过编译解析后会由操做系统把硬盘中的程序复制到内存中,示例中的程序是将 123 和 456 执行相加操做,并将结果输出到显示器上。
地址 0100
是程序运行的起始位置。Windows 等操做系统把程序从硬盘复制到内存后,会将程序计数器做为设定为起始位置 0100,而后执行程序,每执行一条指令后,程序计数器的数值会增长1(或者直接指向下一条指令的地址),而后,CPU 就会根据程序计数器的数值,从内存中读取命令并执行,也就是说,程序计数器控制着程序的流程。
高级语言中的条件控制流程主要分为三种:顺序执行、条件分支、循环判断
三种,顺序执行是按照地址的内容顺序的执行指令。条件分支是根据条件执行任意地址的指令。循环是重复执行同一地址的指令。
下面以条件分支为例来讲明程序的执行过程(循环也很类似)
程序的开始过程和顺序流程是同样的,CPU 从0100处开始执行命令,在0100和0101都是顺序执行,PC 的值顺序+1,执行到0102地址的指令时,判断0106寄存器的数值大于0,跳转(jump)到0104地址的指令,将数值输出到显示器中,而后结束程序,0103 的指令被跳过了,这就和咱们程序中的 if()
判断是同样的,在不知足条件的状况下,指令会直接跳过。因此 PC 的执行过程也就没有直接+1,而是下一条指令的地址。
条件和循环分支会使用到 jump(跳转指令)
,会根据当前的指令来判断是否跳转,上面咱们提到了标志寄存器
,不管当前累加寄存器的运算结果是正数、负数仍是零,标志寄存器都会将其保存
CPU 在进行运算时,标志寄存器的数值会根据当前运算的结果自动设定,运算结果的正、负和零三种状态由标志寄存器的三个位表示。标志寄存器的第一个字节位、第二个字节位、第三个字节位各自的结果都为1时,分别表明着正数、零和负数。
CPU 的执行机制比较有意思,假设累加寄存器中存储的 XXX 和通用寄存器中存储的 YYY 作比较,执行比较的背后,CPU 的运算机制就会作减法运算。而不管减法运算的结果是正数、零仍是负数,都会保存到标志寄存器中。结果为正表示 XXX 比 YYY 大,结果为零表示 XXX 和 YYY 相等,结果为负表示 XXX 比 YYY 小。程序比较的指令,其实是在 CPU 内部作减法
运算。
接下来,咱们继续介绍函数调用机制,哪怕是高级语言编写的程序,函数调用处理也是经过把程序计数器的值设定成函数的存储地址来实现的。函数执行跳转指令后,必须进行返回处理,单纯的指令跳转没有意义,下面是一个实现函数跳转的例子
图中将变量 a 和 b 分别赋值为 123 和 456 ,调用 MyFun(a,b) 方法,进行指令跳转。图中的地址是将 C 语言编译成机器语言后运行时的地址,因为1行 C 程序在编译后一般会变为多行机器语言,因此图中的地址是分散的。在执行完 MyFun(a,b)指令后,程序会返回到 MyFun(a,b) 的下一条指令,CPU 继续执行下面的指令。
函数的调用和返回很重要的两个指令是 call
和 return
指令,再将函数的入口地址设定到程序计数器以前,call 指令会把调用函数后要执行的指令地址存储在名为栈的主存内。函数处理完毕后,再经过函数的出口来执行 return 指令。return 指令的功能是把保存在栈中的地址设定到程序计数器。MyFun 函数在被调用以前,0154 地址保存在栈中,MyFun 函数处理完成后,会把 0154 的地址保存在程序计数器中。这个调用过程以下
在一些高级语言的条件或者循环语句中,函数调用的处理会转换成 call 指令,函数结束后的处理则会转换成 return 指令。
接下来咱们看一下基址寄存器和变址寄存器,经过这两个寄存器,咱们能够对主存上的特定区域进行划分,来实现相似数组的操做,首先,咱们用十六进制数将计算机内存上的 00000000 - FFFFFFFF 的地址划分出来。那么,凡是该范围的内存地址,只要有一个 32 位的寄存器,即可查看所有地址。但若是想要想数组那样分割特定的内存区域以达到连续查看的目的的话,使用两个寄存器会更加方便。
例如,咱们用两个寄存器(基址寄存器和变址寄存器)来表示内存的值
这种表示方式很相似数组的构造,数组
是指一样长度的数据在内存中进行连续排列的数据构造。用数组名表示数组所有的值,经过索引来区分数组的各个数据元素,例如: a[0] - a[4],[]
内的 0 - 4 就是数组的下标。
几乎全部的冯·诺伊曼型计算机的CPU,其工做均可以分为5个阶段:取指令、指令译码、执行指令、访存取数、结果写回。
取指令
阶段是将内存中的指令读取到 CPU 中寄存器的过程,程序寄存器用于存储下一条指令所在的地址指令译码
阶段,在取指令完成后,立马进入指令译码阶段,在指令译码阶段,指令译码器按照预约的指令格式,对取回的指令进行拆分和解释,识别区分出不一样的指令类别以及各类获取操做数的方法。执行指令
阶段,译码完成后,就须要执行这一条指令了,此阶段的任务是完成指令所规定的各类操做,具体实现指令的功能。访问取数
阶段,根据指令的须要,有可能须要从内存中提取数据,此阶段的任务是:根据指令地址码,获得操做数在主存中的地址,并从主存中读取该操做数用于运算。结果写回
阶段,做为最后一个阶段,结果写回(Write Back,WB)阶段把执行指令阶段的运行结果数据“写回”到某种存储形式:结果数据常常被写到CPU的内部寄存器中,以便被后续的指令快速地存取;CPU 和 内存就像是一堆不可分割的恋人同样,是没法拆散的一对儿,没有内存,CPU 没法执行程序指令,那么计算机也就失去了意义;只有内存,没法执行指令,那么计算机照样没法运行。
那么什么是内存呢?内存和 CPU 如何进行交互?下面就来介绍一下
内存(Memory)是计算机中最重要的部件之一,它是程序与CPU进行沟通的桥梁。计算机中全部程序的运行都是在内存中进行的,所以内存对计算机的影响很是大,内存又被称为主存
,其做用是存放 CPU 中的运算数据,以及与硬盘等外部存储设备交换的数据。只要计算机在运行中,CPU 就会把须要运算的数据调到主存中进行运算,当运算完成后CPU再将结果传送出来,主存的运行也决定了计算机的稳定运行。
内存的内部是由各类 IC 电路组成的,它的种类很庞大,可是其主要分为三种存储器
丢失
。更快
的存储器。当 CPU 向内存写入数据时,这些数据也会被写入高速缓存中。当 CPU 须要读取数据时,会直接从高速缓存中直接读取,固然,如须要的数据在Cache中没有,CPU会再去读取内存中的数据。内存 IC 是一个完整的结构,它内部也有电源、地址信号、数据信号、控制信号和用于寻址的 IC 引脚来进行数据的读写。下面是一个虚拟的 IC 引脚示意图
图中 VCC 和 GND 表示电源,A0 - A9 是地址信号的引脚,D0 - D7 表示的是控制信号、RD 和 WR 都是好控制信号,我用不一样的颜色进行了区分,将电源链接到 VCC 和 GND 后,就能够对其余引脚传递 0 和 1 的信号,大多数状况下,+5V 表示1,0V 表示 0。
咱们都知道内存是用来存储数据,那么这个内存 IC 中能存储多少数据呢?D0 - D7 表示的是数据信号,也就是说,一次能够输入输出 8 bit = 1 byte 的数据。A0 - A9 是地址信号共十个,表示能够指定 00000 00000 - 11111 11111 共 2 的 10次方 = 1024个地址
。每一个地址都会存放 1 byte 的数据,所以咱们能够得出内存 IC 的容量就是 1 KB。
让咱们把关注点放在内存 IC 对数据的读写过程上来吧!咱们来看一个对内存IC 进行数据写入和读取的模型
来详细描述一下这个过程,假设咱们要向内存 IC 中写入 1byte 的数据的话,它的过程是这样的:
A0 - A9
来指定数据的存储场所,而后再把数据的值输入给 D0 - D7
的数据信号,并把 WR(write)
的值置为 1,执行完这些操做后,便可以向内存 IC 写入数据为了便于记忆,咱们把内存模型映射成为咱们现实世界的模型,在现实世界中,内存的模型很想咱们生活的楼房。在这个楼房中,1层能够存储一个字节的数据,楼层号就是地址
,下面是内存和楼层整合的模型图
咱们知道,程序中的数据不只只有数值,还有数据类型
的概念,从内存上来看,就是占用内存大小(占用楼层数)的意思。即便物理上强制以 1 个字节为单位来逐一读写数据的内存,在程序中,经过指定其数据类型,也能实现以特定字节数为单位来进行读写。
咱们都知道,计算机的底层都是使用二进制数据进行数据流传输的,那么为何会使用二进制表示计算机呢?或者说,什么是二进制数呢?在拓展一步,如何使用二进制进行加减乘除?下面就来看一下
那么什么是二进制数呢?为了说明这个问题,咱们先把 00100111
这个数转换为十进制数看一下,二进制数转换为十进制数,直接将各位置上的值 * 位权便可,那么咱们将上面的数值进行转换
也就是说,二进制数表明的 00100111
转换成十进制就是 39,这个 39 并非 3 和 9 两个数字连着写,而是 3 * 10 + 9 * 1,这里面的 10 , 1
就是位权,以此类推,上述例子中的位权从高位到低位依次就是 7 6 5 4 3 2 1 0
。这个位权也叫作次幂,那么最高位就是2的7次幂,2的6次幂 等等。二进制数的运算每次都会以2为底,这个2 指得就是基数,那么十进制数的基数也就是 10 。在任何状况下位权的值都是 数的位数 - 1,那么第一位的位权就是 1 - 1 = 0, 第二位的位权就睡 2 - 1 = 1,以此类推。
那么咱们所说的二进制数其实就是 用0和1两个数字来表示的数,它的基数为2,它的数值就是每一个数的位数 * 位权再求和获得的结果,咱们通常来讲数值指的就是十进制数,那么它的数值就是 3 * 10 + 9 * 1 = 39。
在了解过二进制以后,下面咱们来看一下二进制的运算,和十进制数同样,加减乘除也适用于二进制数,只要注意逢 2 进位便可。二进制数的运算,也是计算机程序所特有的运算,所以了解二进制的运算是必需要掌握的。
首先咱们来介绍移位
运算,移位运算是指将二进制的数值的各个位置上的元素坐左移和右移操做,见下图
刚才咱们没有介绍右移的状况,是由于右移以后空出来的高位数值,有 0 和 1 两种形式。要想区分何时补0何时补1,首先就须要掌握二进制数表示负数
的方法。
二进制数中表示负数值时,通常会把最高位做为符号来使用,所以咱们把这个最高位看成符号位。 符号位是 0 时表示正数
,是 1 时表示 负数
。那么 -1 用二进制数该如何表示呢?可能不少人会这么认为: 由于 1 的二进制数是 0000 0001
,最高位是符号位,因此正确的表示 -1 应该是 1000 0001
,可是这个答案真的对吗?
计算机世界中是没有减法的,计算机在作减法的时候其实就是在作加法,也就是用加法来实现的减法运算。好比 100 - 50 ,其实计算机来看的时候应该是 100 + (-50),为此,在表示负数的时候就要用到二进制补数
,补数就是用正数来表示的负数。
为了得到补数
,咱们须要将二进制的各数位的数值所有取反,而后再将结果 + 1 便可,先记住这个结论,下面咱们来演示一下。
具体来讲,就是须要先获取某个数值的二进制数,而后对二进制数的每一位作取反操做(0 ---> 1 , 1 ---> 0),最后再对取反后的数 +1 ,这样就完成了补数的获取。
补数的获取,虽然直观上不易理解,可是逻辑上却很是严谨,好比咱们来看一下 1 - 1 的这个过程,咱们先用上面的这个 1000 0001
(它是1的补数,不知道的请看上文,正确性先无论,只是用来作一下计算)来表示一下
奇怪,1 - 1 会变成 130 ,而不是0,因此能够得出结论 1000 0001
表示 -1 是彻底错误的。
那么正确的该如何表示呢?其实咱们上面已经给出结果了,那就是 1111 1111
,来论证一下它的正确性
咱们能够看到 1 - 1 其实实际上就是 1 + (-1),对 -1 进行上面的取反 + 1 后变为 1111 1111
, 而后与 1 进行加法运算,获得的结果是九位的 1 0000 0000
,结果发生了溢出
,计算机会直接忽略掉溢出位,也就是直接抛掉 最高位 1 ,变为 0000 0000
。也就是 0,结果正确,因此 1111 1111
表示的就是 -1 。
因此负数的二进制表示就是先求其补数,补数的求解过程就是对原始数值的二进制数各位取反,而后将结果 + 1。
在了解完补数后,咱们从新考虑一下右移这个议题,右移在移位后空出来的最高位有两种状况 0 和 1
。
将二进制数做为带符号的数值进行右移运算时,移位后须要在最高位填充移位前符号位的值( 0 或 1)。这就被称为算数右移
。若是数值使用补数表示的负数值,那么右移后在空出来的最高位补 1,就能够正确的表示 1/2,1/4,1/8
等的数值运算。若是是正数,那么直接在空出来的位置补 0 便可。
下面来看一个右移的例子。将 -4 右移两位,来各自看一下移位示意图
如上图所示,在逻辑右移的状况下, -4 右移两位会变成 63
, 显然不是它的 1/4,因此不能使用逻辑右移,那么算数右移的状况下,右移两位会变为 -1
,显然是它的 1/4,故而采用算数右移。
那么咱们能够得出来一个结论:左移时,不管是图形仍是数值,移位后,只须要将低位补 0 便可;右移时,须要根据状况判断是逻辑右移仍是算数右移。
下面介绍一下符号扩展:将数据进行符号扩展是为了产生一个位数加倍、但数值大小不变的结果,以知足有些指令对操做数位数的要求,例如倍长于除数的被除数,再如将数据位数加长以减小计算过程当中的偏差。
以8位二进制为例,符号扩展就是指在保持值不变的前提下将其转换成为16位和32位的二进制数。将0111 1111
这个正的 8位二进制数转换成为 16位二进制数时,很容易就可以得出0000 0000 0111 1111
这个正确的结果,可是像 1111 1111
这样的补数来表示的数值,该如何处理?直接将其表示成为1111 1111 1111 1111
就能够了。也就是说,无论正数仍是补数表示的负数,只须要将 0 和 1 填充高位便可。
咱们你们知道,计算机的五大基础部件是 存储器
、控制器
、运算器
、输入和输出设备
,其中从存储功能的角度来看,能够把存储器分为内存
和 磁盘
,咱们上面介绍过内存,下面就来介绍一下磁盘以及磁盘和内存的关系
计算机最主要的存储部件是内存和磁盘。磁盘中存储的程序必须加载到内存中才能运行,在磁盘中保存的程序是没法直接运行的,这是由于负责解析和运行程序内容的 CPU 是须要经过程序计数器来指定内存地址从而读出程序指令的。
咱们上面提到,磁盘每每和内存是互利共生的关系,相互协做,彼此持有良好的合做关系。每次内存都须要从磁盘中读取数据,必然会读到相同的内容,因此必定会有一个角色负责存储咱们常常须要读到的内容。 咱们你们作软件的时候常常会用到缓存技术
,那么硬件层面也不例外,磁盘也有缓存,磁盘的缓存叫作磁盘缓存
。
磁盘缓存指的是把从磁盘中读出的数据存储到内存的方式,这样一来,当接下来须要读取相同的内容时,就不会再经过实际的磁盘,而是经过磁盘缓存来读取。某一种技术或者框架的出现势必要解决某种问题的,那么磁盘缓存就大大改善了磁盘访问的速度。
虚拟内存
是内存和磁盘交互的第二个媒介。虚拟内存是指把磁盘的一部分做为假想内存
来使用。这与磁盘缓存是假想的磁盘(其实是内存)相对,虚拟内存是假想的内存(其实是磁盘)。
虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续可用
的内存(一个完整的地址空间),可是实际上,它一般被分割成多个物理碎片,还有部分存储在外部磁盘管理器上,必要时进行数据交换。
经过借助虚拟内存,在内存不足时仍然能够运行程序。例如,在只剩 5MB 内存空间的状况下仍然能够运行 10MB 的程序。因为 CPU 只能执行加载到内存中的程序,所以,虚拟内存的空间就须要和内存中的空间进行置换(swap)
,而后运行程序。
虚拟内存的方法有分页式
和 分段式
两种。Windows 采用的是分页式。该方式是指在不考虑程序构造的状况下,把运行的程序按照必定大小的页进行分割,并以页
为单位进行置换。在分页式中,咱们把磁盘的内容读到内存中称为 Page In
,把内存的内容写入磁盘称为 Page Out
。Windows 计算机的页大小为 4KB ,也就是说,须要把应用程序按照 4KB 的页来进行切分,以页(page)为单位放到磁盘中,而后进行置换。
为了实现内存功能,Windows 在磁盘上提供了虚拟内存使用的文件(page file,页文件)。该文件由 Windows 生成和管理,文件的大小和虚拟内存大小相同,一般大小是内存的 1 - 2 倍。
以前咱们介绍了CPU、内存的物理结构,如今咱们来介绍一下磁盘的物理结构。磁盘的物理结构指的是磁盘存储数据的形式。
磁盘是经过其物理表面划分红多个空间来使用的。划分的方式有两种:可变长方式
和 扇区方式
。前者是将物理结构划分红长度可变的空间,后者是将磁盘结构划分为固定长度的空间。通常 Windows 所使用的硬盘和软盘都是使用扇区这种方式。扇区中,把磁盘表面分红若干个同心圆的空间就是 磁道
,把磁道按照固定大小的存储空间划分而成的就是 扇区
扇区
是对磁盘进行物理读写的最小单位。Windows 中使用的磁盘,通常是一个扇区 512 个字节。不过,Windows 在逻辑方面对磁盘进行读写的单位是扇区整数倍簇。根据磁盘容量不一样功能,1簇能够是 512 字节(1 簇 = 1扇区)、1KB(1簇 = 2扇区)、2KB、4KB、8KB、16KB、32KB( 1 簇 = 64 扇区)。簇和扇区的大小是相等的。
咱们想必都有过压缩
和 解压缩
文件的经历,当文件太大时,咱们会使用文件压缩来下降文件的占用空间。好比微信上传文件的限制是100 MB,我这里有个文件夹没法上传,可是我解压完成后的文件必定会小于 100 MB,那么个人文件就能够上传了。
此外,咱们把相机拍完的照片保存到计算机上的时候,也会使用压缩算法进行文件压缩,文件压缩的格式通常是JPEG
。
那么什么是压缩算法呢?压缩算法又是怎么定义的呢?在认识算法以前咱们须要先了解一下文件是如何存储的
文件是将数据存储在磁盘等存储媒介的一种形式。程序文件中最基本的存储数据单位是字节
。文件的大小无论是 xxxKB、xxxMB等来表示,就是由于文件是以字节 B = Byte
为单位来存储的。
文件就是字节数据的集合。用 1 字节(8 位)表示的字节数据有 256 种,用二进制表示的话就是 0000 0000 - 1111 1111 。若是文件中存储的数据是文字,那么该文件就是文本文件。若是是图形,那么该文件就是图像文件。在任何状况下,文件中的字节数都是连续存储
的。
上面介绍了文件的集合体其实就是一堆字节数据的集合,那么咱们就能够来给压缩算法下一个定义。
压缩算法(compaction algorithm)
指的就是数据压缩的算法,主要包括压缩和还原(解压缩)的两个步骤。
其实就是在不改变原有文件属性的前提下,下降文件字节空间和占用空间的一种算法。
根据压缩算法的定义,咱们可将其分红不一样的类型:
有损和无损
无损压缩:可以无失真地
从压缩后的数据重构,准确地还原原始数据。可用于对数据的准确性要求严格的场合,如可执行文件和普通文件的压缩、磁盘的压缩,也可用于多媒体数据的压缩。该方法的压缩比较小。如差分编码、RLE、Huffman编码、LZW编码、算术编码。
有损压缩:有失真,不能彻底准确地
恢复原始数据,重构的数据只是原始数据的一个近似。可用于对数据的准确性要求不高的场合,如多媒体数据的压缩。该方法的压缩比较大。例如预测编码、音感编码、分形压缩、小波压缩、JPEG/MPEG。
对称性
若是编解码算法的复杂性和所需时间差很少,则为对称的编码方法,多数压缩算法都是对称的。但也有不对称的,通常是编码难而解码容易,如 Huffman 编码和分形编码。但用于密码学的编码方法则相反,是编码容易,而解码则很是难。
帧间与帧内
在视频编码中会同时用到帧内与帧间的编码方法,帧内编码是指在一帧图像内独立完成的编码方法,同静态图像的编码,如 JPEG;而帧间编码则须要参照先后帧才能进行编解码,并在编码过程当中考虑对帧之间的时间冗余的压缩,如 MPEG。
实时性
在有些多媒体的应用场合,须要实时处理或传输数据(如现场的数字录音和录影、播放MP3/RM/VCD/DVD、视频/音频点播、网络现场直播、可视电话、视频会议),编解码通常要求延时 ≤50 ms。这就须要简单/快速/高效的算法和高速/复杂的CPU/DSP芯片。
分级处理
有些压缩算法能够同时处理不一样分辨率、不一样传输速率、不一样质量水平的多媒体数据,如JPEG2000、MPEG-2/4。
这些概念有些抽象,主要是为了让你们了解一下压缩算法的分类,下面咱们就对具体的几种经常使用的压缩算法来分析一下它的特色和优劣
接下来就让咱们正式看一下文件的压缩机制。首先让咱们来尝试对 AAAAAABBCDDEEEEEF
这 17 个半角字符的文件(文本文件)进行压缩。虽然这些文字没有什么实际意义,可是很适合用来描述 RLE
的压缩机制。
因为半角字符(其实就是英文字符)是做为 1 个字节保存在文件中的,因此上述的文件的大小就是 17 字节。如图
那么,如何才能压缩该文件呢?你们不妨也考虑一下,只要是可以使文件小于 17 字节,咱们可使用任何压缩算法。
最显而易见的一种压缩方式我以为你已经想到了,就是把相同的字符去重化
,也就是 字符 * 重复次数
的方式进行压缩。因此上面文件压缩后就会变成下面这样
从图中咱们能够看出,AAAAAABBCDDEEEEEF 的17个字符成功被压缩成了 A6B2C1D2E5F1 的12个字符,也就是 12 / 17 = 70%,压缩比为 70%,压缩成功了。
像这样,把文件内容用 数据 * 重复次数
的形式来表示的压缩方法成为 RLE(Run Length Encoding, 行程长度编码)
算法。RLE 算法是一种很好的压缩方法,常常用于压缩传真的图像等。由于图像文件的本质也是字节数据的集合体,因此能够用 RLE 算法进行压缩
下面咱们来介绍另一种压缩算法,即哈夫曼算法。在了解哈夫曼算法以前,你必须舍弃半角英文数字的1个字符是1个字节(8位)的数据
。下面咱们就来认识一下哈夫曼算法的基本思想。
文本文件是由不一样类型的字符组合而成的,并且不一样字符出现的次数也是不同的。例如,在某个文本文件中,A 出现了 100次左右,Q仅仅用到了 3 次,相似这样的状况很常见。哈夫曼算法的关键就在于 屡次出现的数据用小于 8 位的字节数表示,不经常使用的数据则可使用超过 8 位的字节数表示。A 和 Q 都用 8 位来表示时,原文件的大小就是 100次 * 8 位 + 3次 * 8 位 = 824位,假设 A 用 2 位,Q 用 10 位来表示就是 2 * 100 + 3 * 10 = 230 位。
不过要注意一点,最终磁盘的存储都是以8位为一个字节来保存文件的。
哈夫曼算法比较复杂,在深刻了解以前咱们先吃点甜品
,了解一下 莫尔斯编码
,你必定看过美剧或者战争片的电影,在战争中的通讯常常采用莫尔斯编码来传递信息,例以下面
接下来咱们来说解一下莫尔斯编码,下面是莫尔斯编码的示例
,你们把 1 看做是短点(嘀),把 11 看做是长点(嗒)便可。
莫尔斯编码通常把文本中出现最高频率的字符用短编码
来表示。如表所示,假如表示短点的位是 1,表示长点的位是 11 的话,那么 E(嘀)这一数据的字符就能够用 1 来表示,C(滴答滴答)就能够用 9 位的 110101101
来表示。在实际的莫尔斯编码中,若是短点的长度是 1 ,长点的长度就是 3,短点和长点的间隔就是1。这里的长度指的就是声音的长度。好比咱们想用上面的 AAAAAABBCDDEEEEEF 例子来用莫尔斯编码重写,在莫尔斯曼编码中,各个字符之间须要加入表示时间间隔的符号。这里咱们用 00 加以区分。
因此,AAAAAABBCDDEEEEEF 这个文本就变为了 A * 6 次 + B * 2次 + C * 1次 + D * 2次 + E * 5次 + F * 1次 + 字符间隔 * 16 = 4 位 * 6次 + 8 位 * 2次 + 9 位 * 1 次 + 6位 * 2次 + 1位 * 5次 + 8 位 * 1次 + 2位 * 16次 = 106位 = 14字节。
因此使用莫尔斯电码的压缩比为 14 / 17 = 82%。效率并不太突出。
刚才已经提到,莫尔斯编码是根据平常文本中各字符的出现频率来决定表示各字符的编码数据长度的。不过,在该编码体系中,对 AAAAAABBCDDEEEEEF 这种文原本说并非效率最高的。
下面咱们来看一下哈夫曼算法。哈夫曼算法是指,为各压缩对象文件分别构造最佳的编码体系,并以该编码体系为基础来进行压缩。所以,用什么样的编码(哈夫曼编码)对数据进行分割,就要由各个文件而定。用哈夫曼算法压缩过的文件中,存储着哈夫曼编码信息和压缩过的数据。
接下来,咱们在对 AAAAAABBCDDEEEEEF 中的 A - F 这些字符,按照出现频率高的字符用尽可能少的位数编码来表示
这一原则进行整理。按照出现频率从高到低的顺序整理后,结果以下,同时也列出了编码方案。
字符 | 出现频率 | 编码(方案) | 位数 |
---|---|---|---|
A | 6 | 0 | 1 |
E | 5 | 1 | 1 |
B | 2 | 10 | 2 |
D | 2 | 11 | 2 |
C | 1 | 100 | 3 |
F | 1 | 101 | 3 |
在上表的编码方案中,随着出现频率的下降,字符编码信息的数据位数也在逐渐增长,从最开始的 1位、2位依次增长到3位。不过这个编码体系是存在问题的,你不知道100这个3位的编码,它的意思是用 一、0、0这三个编码来表示 E、A、A 呢?仍是用十、0来表示 B、A 呢?仍是用100来表示 C 呢。
而在哈夫曼算法中,经过借助哈夫曼树的构造编码体系,即便在不使用字符区分符号的状况下,也能够构建可以明确进行区分的编码体系。不过哈夫曼树的算法要比较复杂,下面是一个哈夫曼树的构造过程。
天然界树的从根开始生叶的,而哈夫曼树则是叶生枝
使用哈夫曼树以后,出现频率越高的数据所占用的位数越少,这也是哈夫曼树的核心思想。经过上图的步骤二能够看出,枝条链接数据时,咱们是从出现频率较低的数据开始的。这就意味着出现频率低的数据到达根部的枝条也越多。而枝条越多则意味着编码的位数随之增长。
接下来咱们来看一下哈夫曼树的压缩比率,用上图获得的数据表示 AAAAAABBCDDEEEEEF 为 000000000000 100100 110 101101 0101010101 111,40位 = 5 字节。压缩前的数据是 17 字节,压缩后的数据居然达到了惊人的5 字节,也就是压缩比率 = 5 / 17 = 29% 如此高的压缩率,简直是太惊艳了。
你们能够参考一下,不管哪一种类型的数据,均可以用哈夫曼树做为压缩算法
文件类型 | 压缩前 | 压缩后 | 压缩比率 |
---|---|---|---|
文本文件 | 14862字节 | 4119字节 | 28% |
图像文件 | 96062字节 | 9456字节 | 10% |
EXE文件 | 24576字节 | 4652字节 | 19% |
最后,咱们来看一下图像文件的数据形式。图像文件的使用目的一般是把图像数据输出到显示器、打印机等设备上。经常使用的图像格式有 : BMP
、JPEG
、TIFF
、GIF
格式等。
图像文件可使用前面介绍的 RLE 算法和哈夫曼算法,由于图像文件在多数状况下并不要求数据须要还原到和压缩以前一摸同样的状态,容许丢失一部分数据。咱们把能还原到压缩前状态的压缩称为 可逆压缩
,没法还原到压缩前状态的压缩称为非可逆压缩
。
通常来讲,JPEG格式的文件是非可逆压缩,所以还原后有部分图像信息比较模糊。GIF 是可逆压缩
程序中包含着运行环境
这一内容,能够说 运行环境 = 操做系统 + 硬件 ,操做系统又能够被称为软件,它是由一系列的指令组成的。咱们不介绍操做系统,咱们主要来介绍一下硬件的识别。
咱们确定都玩儿过游戏,你玩儿游戏前须要干什么?是否是须要先看一下本身的笔记本或者电脑是否是能肝的起游戏?下面是一个游戏的配置(怀念一下 wow)
图中的主要配置以下
操做系统版本:说的就是应用程序运行在何种系统环境,如今市面上主要有三种操做系统环境,Windows 、Linux 和 Unix ,通常咱们玩儿的大型游戏几乎都是在 Windows 上运行,能够说 Windows 是游戏的天堂。Windows 操做系统也会有区分,分为32位操做系统和64位操做系统,互不兼容。
处理器:处理器指的就是 CPU,你的电脑的计算能力,通俗来说就是每秒钟能处理的指令数,若是你的电脑以为卡带不起来的话,极可能就是 CPU 的计算能力不足致使的。想要加深理解,请阅读博主的另外一篇文章:程序员须要了解的硬核知识之CPU
显卡:显卡承担图形的输出任务,所以又被称为图形处理器(Graphic Processing Unit,GPU),显卡也很是重要,好比我以前玩儿的剑灵
开五档(其实就是图像变得更清晰)会卡,其实就是显卡显示不出来的缘由。
内存:内存即主存,就是你的应用程序在运行时可以动态分析指令的这部分存储空间,它的大小也能决定你电脑的运行速度,想要加深理解,请阅读博主的另外一篇文章 程序员须要了解的硬核知识以内存
存储空间:存储空间指的就是应用程序安装所占用的磁盘空间,由图中可知,此游戏的最低存储空间必需要大于 5GB,其实咱们都会遗留很大一部分用来安装游戏。
从程序的运行环境这一角度来考量的话,CPU 的种类是特别重要的参数,为了使程序可以正常运行,必须知足 CPU 所需的最低配置。
CPU 只能解释其自身固有的语言。不一样的 CPU 能解释的机器语言的种类也是不一样的。机器语言的程序称为 本地代码(native code)
,程序员用 C 等高级语言编写的程序,仅仅是文本文件。文本文件(排除文字编码的问题)
在任何环境下都能显示和编辑。咱们称之为源代码
。经过对源代码进行编译,就能够获得本地代码
。下图反映了这个过程。
uploading-image-703074.png
计算机的硬件并不只仅是由 CPU 组成的,还包括用于存储程序指令的数据和内存,以及经过 I/O 链接的键盘、显示器、硬盘、打印机等外围设备。
在 WIndows 软件中,键盘输入、显示器输出等并非直接向硬件发送指令。而是经过向 Windows 发送指令实现的。所以,程序员就不用注意内存和 I/O 地址的不一样构成了。Windows 操做的是硬件而不是软件,软件经过操做 Windows 系统能够达到控制硬件的目的。
接下来咱们看一下操做系统的种类。一样机型的计算机,可安装的操做系统类型也会有多种选择。例如:AT 兼容机除了能够安装 Windows 以外,还能够采用 Unix 系列的 Linux 以及 FreeBSD (也是一种Unix操做系统)等多个操做系统。固然,应用软件则必须根据不一样的操做系统类型来专门开发。CPU 的类型不一样,所对应机器的语言也不一样,一样的道理,操做系统的类型不一样,应用程序向操做系统传递指令的途径也不一样。
应用程序向系统传递指令的途径称为 API(Application Programming Interface)
。Windows 以及 Linux 操做系统的 API,提供了任何应用程序均可以利用的函数组合。由于不一样操做系统的 API 是有差别的。因此,如何要将一样的应用程序移植到另外的操做系统,就必需要覆盖应用所用到的 API 部分。
键盘输入、鼠标输入、显示器输出、文件输入和输出等同外围设备进行交互的功能,都是经过 API 提供的。
这也就是为何 Windows 应用程序不能直接移植到 Linux 操做系统上的缘由,API 差别太大了。
在同类型的操做系统下,不论硬件如何,API 几乎相同。可是,因为不一样种类 CPU 的机器语言不一样,所以本地代码也不尽相同。
操做系统
其实也是一种软件,任何新事物的出现确定都有它的历史背景,那么操做系统也不是凭空出现的,确定有它的历史背景。
在计算机尚不存在操做系统的年代,彻底没有任何程序,人们经过各类按钮
来控制计算机,这一过程很是麻烦。因而,有人开发出了仅具备加载和运行功能的监控程序
,这就是操做系统的原型。经过事先启动监控程序,程序员能够根据须要将各类程序加载到内存中运行。虽然仍旧比较麻烦,但比起在没有任何程序的状态下进行开发,工做量获得了很大的缓解。
随着时代的发展,人们在利用监控程序编写程序的过程当中发现不少程序都有公共的部分。例如,经过键盘进行文字输入,显示器进行数据展现等,若是每编写一个新的应用程序都须要相同的处理的话,那真是太浪费时间了。所以,基本的输入输出部分的程序就被追加到了监控程序中。初期的操做系统就是这样诞生了。
相似的想法能够共用,人们又发现有更多的应用程序能够追加到监控程序中,好比硬件控制程序
,编程语言处理器(汇编、编译、解析)
以及各类应用程序等,结果就造成了和如今差别不大的操做系统,也就是说,其实操做系统是多个程序的集合体。
Windows 操做系统是世界上用户数量最庞大的群体,做为 Windows 操做系统的资深
用户,你都知道 Windows 操做系统有哪些特征吗?下面列举了一些 Windows 操做系统的特性
API
函数集成来提供系统调用WYSIWYG
实现打印输出,WYSIWYG 其实就是 What You See Is What You Get ,值得是显示器上显示的图形和文本都是能够原样输出到打印机打印的。这些是对程序员来说比较有意义的一些特征,下面针对这些特征来进行分别的介绍
这里表示的32位操做系统表示的是处理效率最高的数据大小。Windows 处理数据的基本单位是 32 位。这与最一开始在 MS-DOS
等16位操做系统不一样,由于在16位操做系统中处理32位数据须要两次,而32位操做系统只须要一次就可以处理32位的数据,因此通常在 windows 上的应用,它们的最高可以处理的数据都是 32 位的。
好比,用 C 语言来处理整数数据时,有8位的 char
类型,16位的short
类型,以及32位的long
类型三个选项,使用位数较大的 long 类型进行处理的话,增长的只是内存以及磁盘的开销,对性能影响不大。
如今市面上大部分都是64位操做系统了,64位操做系统也是如此。
Windows 是经过名为 API
的函数集来提供系统调用的。API是联系应用程序和操做系统之间的接口,全称叫作 Application Programming Interface
,应用程序接口。
当前主流的32位版 Windows API 也称为 Win32 API
,之因此这样命名,是须要和不一样的操做系统进行区分,好比最一开始的 16 位版的 Win16 API
,和后来流行的 Win64 API
。
API 经过多个 DLL 文件来提供,各个 API 的实体都是用 C 语言编写的函数。因此,在 C 语言环境下,使用 API 更加容易,好比 API 所用到的 MessageBox()
函数,就被保存在了 Windows 提供的 user32.dll 这个 DLL 文件中。
GUI(Graphical User Interface)
指得就是图形用户界面,经过点击显示器中的窗口以及图标等可视化的用户界面,举个例子:Linux 操做系统就有两个版本,一种是简洁版,直接经过命令行控制硬件,还有一种是可视化版,经过光标点击图形界面来控制硬件。
WYSIWYG 指的是显示器上输出的内容能够直接经过打印机打印输出。在 Windows 中,显示器和打印机被认做同等的图形输出设备处理的,该功能也为 WYSIWYG 提供了条件。
借助 WYSIWYG 功能,程序员能够轻松很多。最初,为了是如今显示器中显示和在打印机中打印,就必须分别编写各自的程序,而在 Windows 中,能够借助 WYSIWYG 基本上在一个程序中就能够作到显示和打印这两个功能了。
多任务指的就是同时可以运行多个应用程序的功能,Windows 是经过时钟分割
技术来实现多任务功能的。时钟分割指的是短期间隔内,多个程序切换运行的方式。在用户看来,就好像是多个程序在同时运行,其底层是 CPU 时间切片
,这也是多线程多任务的核心。
Windows 中,网络功能是做为标准功能提供的。数据库(数据库服务器)功能有时也会在后面追加。网络功能和数据库功能虽然并非操做系统不可或缺的,但由于它们和操做系统很接近,因此被统称为中间件
而不是应用。意思是处于操做系统和应用的中间层,操做系统和中间件组合在一块儿,称为系统软件
。应用不只能够利用操做系统,也能够利用中间件的功能。
相对于操做系统一旦安装就不能轻易更换,中间件能够根据须要进行更换,不过,对于大部分应用来讲,更换中间件的话,会形成应用也随之更换,从这个角度来讲,更å换中间件也不是那么容易。
即插即用(Plug-and-Play)
指的是新的设备链接(plug) 后就能够直接使用的机制,新设备链接计算机后,计算机就会自动安装和设定用来控制该设备的驱动程序
设备驱动是操做系统的一部分,提供了同硬件进行基本的输入输出的功能。键盘、鼠标、显示器、磁盘装置等,这些计算机中必备的硬件的设备驱动,通常都是随操做系统一块儿安装的。
有时 DLL 文件也会同设备驱动文件一块儿安装。这些 DLL 文件中存储着用来利用该新追加的硬件API,经过 API ,能够制做出运行该硬件的心应用。
咱们在以前的文章中探讨过,计算机 CPU 只能运行本地代码(机器语言)程序,用 C 语言等高级语言编写的代码,须要通过编译器编译后,转换为本地代码才可以被 CPU 解释执行。
可是本地代码的可读性很是差,因此须要使用一种可以直接读懂的语言来替换本地代码,那就是在各本地代码中,附带上表示其功能的英文缩写,好比在加法运算的本地代码加上add(addition)
的缩写、在比较运算符的本地代码中加上cmp(compare)
的缩写等,这些经过缩写来表示具体本地代码指令的标志称为 助记符
,使用助记符的语言称为汇编语言
。这样,经过阅读汇编语言,也可以了解本地代码的含义了。
不过,即便是使用汇编语言编写的源代码,最终也必需要转换为本地代码才可以运行,负责作这项工做的程序称为编译器
,转换的这个过程称为汇编
。在将源代码转换为本地代码这个功能方面,汇编器和编译器是一样的。
用汇编语言编写的源代码和本地代码是一一对应的。于是,本地代码也能够反过来转换成汇编语言编写的代码。把本地代码转换为汇编代码的这一过程称为反汇编
,执行反汇编的程序称为反汇编程序
。
哪怕是 C 语言编写的源代码,编译后也会转换成特定 CPU 用的本地代码。而将其反汇编的话,就能够获得汇编语言的源代码,并对其内容进行调查。不过,本地代码变成 C 语言源代码的反编译,要比本地代码转换成汇编代码的反汇编要困难,这是由于,C 语言代码和本地代码不是一一对应的关系。
咱们上面提到本地代码能够通过反汇编转换成为汇编代码,可是只有这一种转换方式吗?显然不是,C 语言编写的源代码也可以经过编译器编译称为汇编代码,下面就来尝试一下。
首先须要先作一些准备,须要先下载 Borland C++ 5.5
编译器,为了方便,我这边直接下载好了读者直接从个人百度网盘提取便可 (连接:pan.baidu.com/s/19LqVICpn… 密码:hz1u)
下载完毕,须要进行配置,下面是配置说明 (wenku.baidu.com/view/22e2f4…
首先用 Windows 记事本等文本编辑器编写以下代码
// 返回两个参数值之和的函数
int AddNum(int a,int b){
return a + b;
}
// 调用 AddNum 函数的函数
void MyFunc(){
int c;
c = AddNum(123,456);
}
复制代码
编写完成后将其文件名保存为 Sample4.c ,C 语言源文件的扩展名,一般用.c
来表示,上面程序是提供两个输入参数并返回它们之和。
在 Windows 操做系统下打开 命令提示符
,切换到保存 Sample4.c 的文件夹下,而后在命令提示符中输入
bcc32 -c -S Sample4.c
复制代码
bcc32 是启动 Borland C++ 的命令,-c
的选项是指仅进行编译而不进行连接,-S
选项被用来指定生成汇编语言的源代码
做为编译的结果,当前目录下会生成一个名为Sample4.asm
的汇编语言源代码。汇编语言源文件的扩展名,一般用.asm
来表示,下面就让咱们用编辑器打开看一下 Sample4.asm 中的内容
.386p
ifdef ??version
if ??version GT 500H
.mmx
endif
endif
model flat
ifndef ??version
?debug macro
endm
endif
?debug S "Sample4.c"
?debug T "Sample4.c"
_TEXT segment dword public use32 'CODE'
_TEXT ends
_DATA segment dword public use32 'DATA'
_DATA ends
_BSS segment dword public use32 'BSS'
_BSS ends
DGROUP group _BSS,_DATA
_TEXT segment dword public use32 'CODE'
_AddNum proc near
?live1@0:
;
; int AddNum(int a,int b){
;
push ebp
mov ebp,esp
;
;
; return a + b;
;
@1:
mov eax,dword ptr [ebp+8]
add eax,dword ptr [ebp+12]
;
; }
;
@3:
@2:
pop ebp
ret
_AddNum endp
_MyFunc proc near
?live1@48:
;
; void MyFunc(){
;
push ebp
mov ebp,esp
;
; int c;
; c = AddNum(123,456);
;
@4:
push 456
push 123
call _AddNum
add esp,8
;
; }
;
@5:
pop ebp
ret
_MyFunc endp
_TEXT ends
public _AddNum
public _MyFunc
?debug D "Sample4.c" 20343 45835
end
复制代码
这样,编译器就成功的把 C 语言转换成为了汇编代码了。
第一次看到汇编代码的读者可能感受起来比较难,不过实际上其实比较简单,并且可能比 C 语言还要简单,为了便于阅读汇编代码的源代码,须要注意几个要点
汇编语言的源代码,是由转换成本地代码的指令(后面讲述的操做码)和针对汇编器的伪指令构成的。伪指令负责把程序的构造以及汇编的方法指示给汇编器(转换程序)。不过伪指令是没法汇编转换成为本地代码的。下面是上面程序截取的伪指令
_TEXT segment dword public use32 'CODE'
_TEXT ends
_DATA segment dword public use32 'DATA'
_DATA ends
_BSS segment dword public use32 'BSS'
_BSS ends
DGROUP group _BSS,_DATA
_AddNum proc near
_AddNum endp
_MyFunc proc near
_MyFunc endp
_TEXT ends
end
复制代码
由伪指令 segment
和 ends
围起来的部分,是给构成程序的命令和数据的集合体上加一个名字而获得的,称为段定义
。段定义的英文表达具备区域
的意思,在这个程序中,段定义指的是命令和数据等程序的集合体的意思,一个程序由多个段定义构成。
上面代码的开始位置,定义了3个名称分别为 _TEXT、_DATA、_BSS
的段定义,_TEXT
是指定的段定义,_DATA
是被初始化(有初始值)的数据的段定义,_BSS
是还没有初始化的数据的段定义。这种定义的名称是由 Borland C++ 定义的,是由 Borland C++ 编译器自动分配的,因此程序段定义的顺序就成为了 _TEXT、_DATA、_BSS
,这样也确保了内存的连续性
_TEXT segment dword public use32 'CODE'
_TEXT ends
_DATA segment dword public use32 'DATA'
_DATA ends
_BSS segment dword public use32 'BSS'
_BSS ends
复制代码
段定义( segment ) 是用来区分或者划分范围区域的意思。汇编语言的 segment 伪指令表示段定义的起始,ends 伪指令表示段定义的结束。段定义是一段连续的内存空间
而group
这个伪指令表示的是将 _BSS和_DATA
这两个段定义汇总名为 DGROUP 的组
DGROUP group _BSS,_DATA
复制代码
围起 _AddNum
和 _MyFun
的 _TEXT
segment 和 _TEXT
ends ,表示_AddNum
和 _MyFun
是属于 _TEXT
这一段定义的。
_TEXT segment dword public use32 'CODE'
_TEXT ends
复制代码
所以,即便在源代码中指令和数据是混杂编写的,通过编译和汇编后,也会转换成为规整的本地代码。
_AddNum proc
和 _AddNum endp
围起来的部分,以及_MyFunc proc
和 _MyFunc endp
围起来的部分,分别表示 AddNum 函数和 MyFunc 函数的范围。
_AddNum proc near
_AddNum endp
_MyFunc proc near
_MyFunc endp
复制代码
编译后在函数名前附带上下划线_
,是 Borland C++ 的规定。在 C 语言中编写的 AddNum 函数,在内部是以 _AddNum 这个名称处理的。伪指令 proc 和 endp 围起来的部分,表示的是 过程(procedure)
的范围。在汇编语言中,这种至关于 C 语言的函数的形式称为过程。
末尾的 end
伪指令,表示的是源代码的结束。
在汇编语言中,一行表示一对 CPU 的一个指令。汇编语言指令的语法结构是 操做码 + 操做数,也存在只有操做码没有操做数的指令。
操做码表示的是指令动做,操做数表示的是指令对象。操做码和操做数一块儿使用就是一个英文指令。好比从英语语法来分析的话,操做码是动词,操做数是宾语。好比这个句子 Give me money
这个英文指令的话,Give 就是操做码,me 和 money 就是操做数。汇编语言中存在多个操做数的状况,要用逗号把它们分割,就像是 Give me,money 这样。
可以使用何种形式的操做码,是由 CPU 的种类决定的,下面对操做码的功能进行了整理。
本地代码须要加载到内存后才能运行,内存中存储着构成本地代码的指令和数据。程序运行时,CPU会从内存中把数据和指令读出来,而后放在 CPU 内部的寄存器中进行处理。
若是 CPU 和内存的关系你还不是很了解的话,请阅读做者的另外一篇文章 程序员须要了解的硬核知识之CPU 详细了解。
寄存器是 CPU 中的存储区域,寄存器除了具备临时存储和计算的功能以外,还具备运算功能,x86 系列的主要种类和角色以下图所示
下面就对 CPU 中的指令进行分析
最经常使用的 mov 指令
指令中最常使用的是对寄存器和内存进行数据存储的 mov
指令,mov 指令的两个操做数,分别用来指定数据的存储地和读出源。操做数中能够指定寄存器、常数、标签(附加在地址前),以及用方括号([])
围起来的这些内容。若是指定了没有用([])
方括号围起来的内容,就表示对该值进行处理;若是指定了用方括号围起来的内容,方括号的值则会被解释为内存地址,而后就会对该内存地址对应的值进行读写操做。让咱们对上面的代码片断进行说明
mov ebp,esp
mov eax,dword ptr [ebp+8]
复制代码
mov ebp,esp 中,esp 寄存器中的值被直接存储在了 ebp 中,也就是说,若是 esp 寄存器的值是100的话那么 ebp 寄存器的值也是 100。
而在 mov eax,dword ptr [ebp+8]
这条指令中,ebp 寄存器的值 + 8 后会被解析称为内存地址。若是 ebp
寄存器的值是100的话,那么 eax 寄存器的值就是 100 + 8 的地址的值。dword ptr
也叫作 double word pointer
简单解释一下就是从指定的内存地址中读出4字节的数据
对栈进行 push 和 pop
程序运行时,会在内存上申请分配一个称为栈的数据空间。栈(stack)的特性是后入先出,数据在存储时是从内存的下层(大的地址编号)逐渐往上层(小的地址编号)累积,读出时则是按照从上往下进行读取的。
栈是存储临时数据的区域,它的特色是经过 push 指令和 pop 指令进行数据的存储和读出。向栈中存储数据称为 入栈
,从栈中读出数据称为 出栈
,32位 x86 系列的 CPU 中,进行1次 push 或者 pop,便可处理 32 位(4字节)的数据。
下面咱们一块儿来分析一下函数的调用机制,咱们以上面的 C 语言编写的代码为例。首先,让咱们从MyFunc
函数调用AddNum
函数的汇编语言部分开始,来对函数的调用机制进行说明。栈在函数的调用中发挥了巨大的做用,下面是通过处理后的 MyFunc 函数的汇编处理内容
_MyFunc proc near
push ebp ; 将 ebp 寄存器的值存入栈中 (1)
mov ebp,esp ; 将 esp 寄存器的值存入 ebp 寄存器中 (2)
push 456 ; 将 456 入栈 (3)
push 123 ; 将 123 入栈 (4)
call _AddNum ; 调用 AddNum 函数 (5)
add esp,8 ; esp 寄存器的值 + 8 (6)
pop ebp ; 读出栈中的数值存入 esp 寄存器中 (7)
ret ; 结束 MyFunc 函数,返回到调用源 (8)
_MyFunc endp
复制代码
代码解释中的(1)、(2)、(7)、(8)的处理适用于 C 语言中的全部函数,咱们会在后面展现 AddNum
函数处理内容时进行说明。这里但愿你们先关注(3) - (6) 这一部分,这对了解函数调用机制相当重要。
(3) 和 (4) 表示的是将传递给 AddNum 函数的参数经过 push 入栈。在 C 语言源代码中,虽然记述为函数 AddNum(123,456),但入栈时则会先按照 456,123 这样的顺序。也就是位于后面的数值先入栈。这是 C 语言的规定。(5) 表示的 call 指令,会把程序流程跳转到 AddNum 函数指令的地址处。在汇编语言中,函数名
表示的就是函数所在的内存地址。AddNum 函数处理完毕后,程序流程必需要返回到编号(6) 这一行。call 指令运行后,call 指令的下一行(也就指的是 (6) 这一行)的内存地址(调用函数完毕后要返回的内存地址)会自动的 push 入栈。该值会在 AddNum 函数处理的最后经过 ret
指令 pop 出栈,而后程序会返回到 (6) 这一行。
(6) 部分会把栈中存储的两个参数 (456 和 123) 进行销毁处理。虽然经过两次的 pop 指令也能够实现,不过采用 esp 寄存器 + 8 的方式会更有效率(处理 1 次便可)。对栈进行数值的输入和输出时,数值的单位是4字节。所以,经过在负责栈地址管理的 esp 寄存器中加上4的2倍8,就能够达到和运行两次 pop 命令一样的效果。虽然内存中的数据实际上还残留着,但只要把 esp 寄存器的值更新为数据存储地址前面的数据位置,该数据也就至关于销毁了。
我在编译 Sample4.c
文件时,出现了下图的这条消息
图中的意思是指 c 的值在 MyFunc 定义了可是一直未被使用,这实际上是一项编译器优化的功能,因为存储着 AddNum 函数返回值的变量 c 在后面没有被用到,所以编译器就认为 该变量没有意义,进而也就没有生成与之对应的汇编语言代码。
下图是调用 AddNum 这一函数先后栈内存的变化
上面咱们用汇编代码分析了一下 Sample4.c 整个过程的代码,如今咱们着重分析一下 AddNum 函数的源代码部分,分析一下参数的接收、返回值和返回等机制
_AddNum proc near
push ebp -----------(1)
mov ebp,esp -----------(2)
mov eax,dword ptr[ebp+8] -----------(3)
add eax,dword ptr[ebp+12] -----------(4)
pop ebp -----------(5)
ret ----------------------------------(6)
_AddNum endp
复制代码
ebp 寄存器的值在(1)中入栈,在(5)中出栈,这主要是为了把函数中用到的 ebp 寄存器的内容,恢复到函数调用前的状态。
(2) 中把负责管理栈地址的 esp 寄存器的值赋值到了 ebp 寄存器中。这是由于,在 mov 指令中方括号内的参数,是不容许指定 esp 寄存器的。所以,这里就采用了不直接经过 esp,而是用 ebp 寄存器来读写栈内容的方法。
(3) 使用[ebp + 8] 指定栈中存储的第1个参数123,并将其读出到 eax 寄存器中。像这样,不使用 pop 指令,也能够参照栈的内容。而之因此从多个寄存器中选择了 eax 寄存器,是由于 eax 是负责运算的累加寄存器。
经过(4) 的 add 指令,把当前 eax 寄存器的值同第2个参数相加后的结果存储在 eax 寄存器中。[ebp + 12] 是用来指定第2个参数456的。在 C 语言中,函数的返回值必须经过 eax 寄存器返回,这也是规定。也就是 函数的参数是经过栈来传递,返回值是经过寄存器返回的。
(6) 中 ret 指令运行后,函数返回目的地内存地址会自动出栈
,据此,程序流程就会跳转返回到(6) (Call _AddNum)
的下一行。这时,AddNum 函数入口和出口处栈的状态变化,就以下图所示
在熟悉了汇编语言后,接下来咱们来了解一下全局变量和局部变量,在函数外部定义的变量称为全局变量
,在函数内部定义的变量称为局部变量
,全局变量能够在任意函数中使用,局部变量只能在函数定义局部变量的内部使用。下面,咱们就经过汇编语言来看一下全局变量和局部变量的不一样之处。
下面定义的 C 语言代码分别定义了局部变量和全局变量,而且给各变量进行了赋值,咱们先看一下源代码部分
// 定义被初始化的全局变量
int a1 = 1;
int a2 = 2;
int a3 = 3;
int a4 = 4;
int a5 = 5;
// 定义没有初始化的全局变量
int b1,b2,b3,b4,b5;
// 定义函数
void MyFunc(){
// 定义局部变量
int c1,c2,c3,c4,c5,c6,c7,c8,c9,c10;
// 给局部变量赋值
c1 = 1;
c2 = 2;
c3 = 3;
c4 = 4;
c5 = 5;
c6 = 6;
c7 = 7;
c8 = 8;
c9 = 9;
c10 = 10;
// 把局部变量赋值给全局变量
a1 = c1;
a2 = c2;
a3 = c3;
a4 = c4;
a5 = c5;
b1 = c6;
b2 = c7;
b3 = c8;
b4 = c9;
b5 = c10;
}
复制代码
上面的代码挺暴力的,不过不要紧,可以便于咱们分析其汇编源码就好,咱们用 Borland C++ 编译后的汇编代码以下,编译完成后的源码比较长,这里咱们只拿出来一部分做为分析使用(咱们改变了一下段定义顺序,删除了部分注释)
_DATA segment dword public use32 'DATA'
align 4
_a1 label dword
dd 1
align 4
_a2 label dword
dd 2
align 4
_a3 label dword
dd 3
align 4
_a4 label dword
dd 4
align 4
_a5 label dword
dd 5
_DATA ends
_BSS segment dword public use32 'BSS'
align 4
_b1 label dword
db 4 dup(?)
align 4
_b2 label dword
db 4 dup(?)
align 4
_b3 label dword
db 4 dup(?)
align 4
_b4 label dword
db 4 dup(?)
align 4
_b5 label dword
db 4 dup(?)
_BSS ends
_TEXT segment dword public use32 'CODE'
_MyFunc proc near
push ebp
mov ebp,esp
add esp,-20
push ebx
push esi
mov eax,1
mov edx,2
mov ecx,3
mov ebx,4
mov esi,5
mov dword ptr [ebp-4],6
mov dword ptr [ebp-8],7
mov dword ptr [ebp-12],8
mov dword ptr [ebp-16],9
mov dword ptr [ebp-20],10
mov dword ptr [_a1],eax
mov dword ptr [_a2],edx
mov dword ptr [_a3],ecx
mov dword ptr [_a4],ebx
mov dword ptr [_a5],esi
mov eax,dword ptr [ebp-4]
mov dword ptr [_b1],eax
mov edx,dword ptr [ebp-8]
mov dword ptr [_b2],edx
mov ecx,dword ptr [ebp-12]
mov dword ptr [_b3],ecx
mov eax,dword ptr [ebp-16]
mov dword ptr [_b4],eax
mov edx,dword ptr [ebp-20]
mov dword ptr [_b5],edx
pop esi
pop ebx
mov esp,ebp
pop ebp
ret
_MyFunc endp
_TEXT ends
复制代码
编译后的程序,会被归类到名为段定义的组。
_DATA segment dword public use32 'DATA'
...
_DATA ends
复制代码
_BSS segment dword public use32 'BSS'
...
_BSS ends
复制代码
_TEXT segment dword public use32 'CODE'
_MyFunc proc near
...
_MyFunc endp
_TEXT ends
复制代码
咱们在分析上面汇编代码以前,先来认识一下更多的汇编指令,此表是对上面部分操做码及其功能的接续
操做码 | 操做数 | 功能 |
---|---|---|
add | A,B | 把A和B的值相加,并把结果赋值给A |
call | A | 调用函数A |
cmp | A,B | 对A和B进行比较,比较结果会自动存入标志寄存器中 |
inc | A | 对A的值 + 1 |
ige | 标签名 | 和 cmp 命令组合使用。跳转到标签行 |
jl | 标签名 | 和 cmp 命令组合使用。跳转到标签行 |
jle | 标签名 | 和 cmp 命令组合使用。跳转到标签行 |
jmp | 标签名 | 和 cmp 命令组合使用。跳转到标签行 |
mov | A,B | 把 B 的值赋给 A |
pop | A | 从栈中读取数值并存入A |
push | A | 把A的值存入栈中 |
ret | 无 | 将处理返回到调用源 |
xor | A,B | A和B的位进行亦或比较,并将结果存入A中 |
咱们首先来看一下 _DATA
段定义的内容。_a1 label dword
定义了 _a1
这个标签。标签表示的是相对于段定义起始位置的位置。因为_a1
在 _DATA 段
定义的开头位置,因此相对位置是0。 _a1
就至关因而全局变量a1。编译后的函数名和变量名前面会加一个(_)
,这也是 Borland C++ 的规定。dd 1
指的是,申请分配了4字节的内存空间,存储着1这个初始值。 dd指的是 define double word
表示有两个长度为2的字节领域(word),也就是4字节的意思。
Borland C++ 中,因为int
类型的长度是4字节,所以汇编器就把 int a1 = 1 变换成了 _a1 label dword 和 dd 1
。一样,这里也定义了至关于全局变量的 a2 - a5 的标签 _a2 - _a5
,它们各自的初始值 2 - 5 也被存储在各自的4字节中。
接下来,咱们来讲一说 _BSS
段定义的内容。这里定义了至关于全局变量 b1 - b5 的标签 _b1 - _b5
。其中的db 4dup(?)
表示的是申请分配了4字节的领域,但值还没有肯定(这里用 ? 来表示)的意思。db(define byte)
表示有1个长度是1字节的内存空间。于是,db 4 dup(?) 的状况下,就是4字节的内存空间。
注意:db 4 dup(?) 不要和 dd 4 混淆了,前者表示的是4个长度是1字节的内存空间。而 db 4 表示的则是双字节( = 4 字节) 的内存空间中存储的值是 4
咱们知道,局部变量是临时保存在寄存器和栈中的。函数内部利用栈进行局部变量的存储,函数调用完成后,局部变量值被销毁,可是寄存器可能用于其余目的。因此,局部变量只是函数在处理期间临时存储在寄存器和栈中的。
回想一下上述代码是否是定义了10个局部变量?这是为了表示存储局部变量的不只仅是栈,还有寄存器。为了确保 c1 - c10 所需的域,寄存器空闲的时候就会使用寄存器,寄存器空间不足的时候就会使用栈。
让咱们继续来分析上面代码的内容。_TEXT
段定义表示的是 MyFunc
函数的范围。在 MyFunc 函数中定义的局部变量所须要的内存领域。会被尽量的分配在寄存器中。你们可能认为使用高性能的寄存器来替代普通的内存是一种资源浪费,可是编译器不这么认为,只要寄存器有空间,编译器就会使用它。因为寄存器的访问速度远高于内存,因此直接访问寄存器可以高效的处理。局部变量使用寄存器,是 Borland C++ 编译器最优化的运行结果。
代码清单中的以下内容表示的是向寄存器中分配局部变量的部分
mov eax,1
mov edx,2
mov ecx,3
mov ebx,4
mov esi,5
复制代码
仅仅对局部变量进行定义是不够的,只有在给局部变量赋值时,才会被分配到寄存器的内存区域。上述代码至关于就是给5个局部变量 c1 - c5 分别赋值为 1 - 5。eax、edx、ecx、ebx、esi 是 x86 系列32位 CPU 寄存器的名称。至于使用哪一个寄存器,是由编译器
来决定的 。
x86 系列 CPU 拥有的寄存器中,程序能够操做的是十几,其中空闲的最多会有几个。于是,局部变量超过寄存器数量的时候,可分配的寄存器就不够用了,这种状况下,编译器就会把栈派上用场,用来存储剩余的局部变量。
在上述代码这一部分,给局部变量c1 - c5 分配完寄存器后,可用的寄存器数量就不足了。因而,剩下的5个局部变量c6 - c10 就被分配给了栈的内存空间。以下面代码所示
mov dword ptr [ebp-4],6
mov dword ptr [ebp-8],7
mov dword ptr [ebp-12],8
mov dword ptr [ebp-16],9
mov dword ptr [ebp-20],10
复制代码
函数入口 add esp,-20
指的是,对栈数据存储位置的 esp 寄存器(栈指针)的值作减20的处理。为了确保内存变量 c6 - c10 在栈中,就须要保留5个 int 类型的局部变量(4字节 * 5 = 20 字节)所需的空间。mov ebp,esp
这行指令表示的意思是将 esp 寄存器的值赋值到 ebp 寄存器。之因此须要这么处理,是为了经过在函数出口处 mov esp ebp
这一处理,把 esp 寄存器的值还原到原始状态,从而对申请分配的栈空间进行释放,这时栈中用到的局部变量就消失了。这也是栈的清理处理。在使用寄存器的状况下,局部变量则会在寄存器被用于其余用途时自动消失,以下图所示。
mov dword ptr [ebp-4],6
mov dword ptr [ebp-8],7
mov dword ptr [ebp-12],8
mov dword ptr [ebp-16],9
mov dword ptr [ebp-20],10
复制代码
这五行代码是往栈空间代入数值的部分,因为在向栈申请内存空间前,借助了 mov ebp, esp
这个处理,esp 寄存器的值被保存到了 esp 寄存器中,所以,经过使用[ebp - 4]、[ebp - 8]、[ebp - 12]、[ebp - 16]、[ebp - 20] 这样的形式,就能够申请分配20字节的栈内存空间切分红5个长度为4字节的空间来使用。例如,mov dword ptr [ebp-4],6
表示的就是,从申请分配的内存空间的下端(ebp寄存器指示的位置)开始向前4字节的地址([ebp - 4]) 中,存储着6这一4字节数据。
上面说的都是顺序流程,那么如今就让咱们分析一下循环流程的处理,看一下 for 循环
以及 if 条件分支
等 c 语言程序的 流程控制
是如何实现的,咱们仍是以代码以及编译后的结果为例,看一下程序控制流程的处理过程。
// 定义MySub 函数
void MySub(){
// 不作任何处理
}
// 定义MyFunc 函数
void Myfunc(){
int i;
for(int i = 0;i < 10;i++){
// 重复调用MySub十次
MySub();
}
}
复制代码
上述代码将局部变量 i 做为循环条件,循环调用十次MySub
函数,下面是它主要的汇编代码
xor ebx, ebx ; 将寄存器清0
@4 call _MySub ; 调用MySub函数
inc ebx ; ebx寄存器的值 + 1
cmp ebx,10 ; 将ebx寄存器的值和10进行比较
jl short @4 ; 若是小于10就跳转到 @4
复制代码
C 语言中的 for 语句是经过在括号中指定循环计数器的初始值(i = 0)、循环的继续条件(i < 10)、循环计数器的更新(i++) 这三种形式来进行循环处理的。与此相对的汇编代码就是经过比较指令(cmp)
和 跳转指令(jl)
来实现的。
下面咱们来对上述代码进行说明
MyFunc
函数中用到的局部变量只有 i ,变量 i 申请分配了 ebx 寄存器的内存空间。for 语句括号中的 i = 0 被转换为 xor ebx,ebx
这一处理,xor 指令会对左起第一个操做数和右起第二个操做数进行 XOR 运算,而后把结果存储在第一个操做数中。因为这里把第一个操做数和第二个操做数都指定为了 ebx,所以就变成了对相同数值的 XOR 运算。也就是说无论当前寄存器的值是什么,最终的结果都是0。相似的,咱们使用 mov ebx,0
也能获得相同的结果,可是 xor 指令的处理速度更快,并且编译器也会启动最优化功能。
XOR 指的就是异或操做,它的运算规则是 若是a、b两个值不相同,则异或结果为1。若是a、b两个值相同,异或结果为0。
相同数值进行 XOR 运算,运算结果为0。XOR 的运算规则是,值不一样时结果为1,值相同时结果为0。例如 01010101 和 01010101 进行运算,就会分别对各个数字位进行 XOR 运算。由于每一个数字位都相同,因此运算结果为0。
ebx 寄存器的值初始化后,会经过 call 指定调用 _MySub 函数,从 _MySub 函数返回后,会执行inc ebx
指令,对 ebx 的值进行 + 1 操做,这个操做就至关于 i++ 的意思,++ 表示的就是当前数值 + 1。
这里须要知道 i++ 和 ++i 的区别
i++ 是先赋值,复制完成后再对 i执行 + 1 操做
++i 是先进行 +1 操做,完成后再进行赋值
inc
下一行的 cmp
是用来对第一个操做数和第二个操做数的数值进行比较的指令。 cmp ebx,10
就至关于 C 语言中的 i < 10 这一处理,意思是把 ebx 寄存器的值与10进行比较。汇编语言中比较指令的结果,会存储在 CPU 的标志寄存器中。不过,标志寄存器的值,程序是没法直接参考的。那如何判断比较结果呢?
汇编语言中有多个跳转指令
,这些跳转指令会根据标志寄存器的值来判断是否进行跳转操做,例如最后一行的 jl,它会根据 cmp ebx,10 指令所存储在标志寄存器中的值来判断是否跳转,jl
这条指令表示的就是 jump on less than(小于的话就跳转)
。发现若是 i 比 10 小,就会跳转到 @4 所在的指令处继续执行。
那么汇编代码的意思也能够用 C 语言来改写一下,加深理解
i ^= i;
L4: MySub();
i++;
if(i < 10) goto L4;
复制代码
代码第一行 i ^= i 指的就是 i 和 i 进行异或运算,也就是 XOR 运算,MySub() 函数用 L4 标签来替代,而后进行 i 自增操做,若是i 的值小于 10 的话,就会一直循环 MySub() 函数。
条件分支的处理方式和循环的处理方式很类似,使用的也是 cmp 指令和跳转指令。下面是用 C 语言编写的条件分支的代码
// 定义MySub1 函数
void MySub1(){
// 不作任何处理
}
// 定义MySub2 函数
void MySub2(){
// 不作任何处理
}
// 定义MySub3 函数
void MySub3(){
// 不作任何处理
}
// 定义MyFunc 函数
void MyFunc(){
int a = 123;
// 根据条件调用不一样的函数
if(a > 100){
MySub1();
}
else if(a < 50){
MySub2();
}
else
{
MySub3();
}
}
复制代码
很简单的一个实现了条件判断的 C 语言代码,那么咱们把它用 Borland C++ 编译以后的结果以下
_MyFunc proc near
push ebp
mov ebp,esp
mov eax,123 ; 把123存入 eax 寄存器中
cmp eax,100 ; 把 eax 寄存器的值同100进行比较
jle short @8 ; 比100小时,跳转到@8标签
call _MySub1 ; 调用MySub1函数
jmp short @11 ; 跳转到@11标签
@8:
cmp eax,50 ; 把 eax 寄存器的值同50进行比较
jge short @10 ; 比50大时,跳转到@10标签
call _MySub2 ; 调用MySub2函数
jmp short @11 ; 跳转到@11标签
@10:
call _MySub3 ; 调用MySub3函数
@11:
pop ebp
ret
_MyFunc endp
复制代码
上面代码用到了三种跳转指令,分别是jle(jump on less or equal)
比较结果小时跳转,jge(jump on greater or equal)
比较结果大时跳转,还有无论结果怎样都会进行跳转的jmp
,在这些跳转指令以前还有用来比较的指令 cmp
,构成了上述汇编代码的主要逻辑形式。
经过对上述汇编代码和 C 语言源代码进行比较,想必你们对程序的运行方式有了新的理解,并且,从汇编源代码中获取的知识,也有助于了解 Java 等高级语言的特性,好比 Java 中就有 native 关键字修饰的变量,那么这个变量的底层就是使用 C 语言编写的,还有一些 Java 中的语法糖只有经过汇编代码才能知道其运行逻辑。在某些状况下,对于查找 bug 的缘由也是有帮助的。
上面咱们了解到的编程方式都是串行处理的,那么串行处理有什么特色呢?
串行处理最大的一个特色就是专心只作一件事情
,一件事情作完以后才会去作另一件事情。
计算机是支持多线程的,多线程的核心就是 CPU切换,以下图所示
咱们仍是举个实际的例子,让咱们来看一段代码
// 定义全局变量
int counter = 100;
// 定义MyFunc1()
void MyFunc(){
counter *= 2;
}
// 定义MyFunc2()
void MyFunc2(){
counter *= 2;
}
复制代码
上述代码是更新 counter 的值的 C 语言程序,MyFunc1() 和 MyFunc2() 的处理内容都是把 counter 的值扩大至原来的二倍,而后再把 counter 的值赋值给 counter 。这里,咱们假设使用多线程处理
,同时调用了一次MyFunc1 和 MyFunc2 函数,这时,全局变量 counter 的值,理应编程 100 * 2 * 2 = 400。若是你开启了多个线程的话,你会发现 counter 的数值有时也是 200,对于为何出现这种状况,若是你不了解程序的运行方式,是很难找到缘由的。
咱们将上面的代码转换成汇编语言的代码以下
mov eax,dword ptr[_counter] ; 将 counter 的值读入 eax 寄存器
add eax,eax ; 将 eax 寄存器的值扩大2倍。
mov dword ptr[_counter],eax ; 将 eax 寄存器的值存入 counter 中。
复制代码
在多线程程序中,用汇编语言表示的代码每运行一行,处理都有可能切换到其余线程中。于是,假设 MyFun1 函数在读出 counter 数值100后,还将来得及将它的二倍值200写入 counter 时,正巧 MyFun2 函数读出了 counter 的值100,那么结果就将变为 200 。
为了不该bug,咱们能够采用以函数或 C 语言代码的行为单位来禁止线程切换的锁定
方法,或者使用某种线程安全的方式来避免该问题的出现。
如今基本上没有人用汇编语言来编写程序了,由于 C、Java等高级语言的效率要比汇编语言快不少。不过,汇编语言的经验仍是很重要的,经过借助汇编语言,咱们能够更好的了解计算机运行机制。
文章参考
www.computerhope.com/jargon/m/me…
《程序是怎样跑起来的》
磁盘
磁盘缓存
虚拟内存
www.digitaltrends.com/computing/w…