对不起,学会这些计算机基础知识后我飘了
The following article comes from Java建设者 Author cxuan
CPU
Central Processing Unit
,它是你的电脑中最硬核
的组件,这种说法一点不为过。CPU 是能够让你的计算机叫计算机
的核心组件,但是它却不能代表你的电脑,CPU 与计算机的关系就相当于大脑和人的关系。CPU 的核心是从程序或应用程序获取指令并执行计算。此过程可以分为三个关键阶段:提取,解码和执行。CPU从系统的主存中提取指令,然后解码该指令的实际内容,然后再由 CPU 的相关部分执行该指令。CPU 内部处理过程
控制单元
和 算术逻辑单元(ALU)
控制单元:从内存中提取指令并解码执行 算数逻辑单元(ALU):处理算数和逻辑运算
寄存器
是中央处理器内的组成部分。它们可以用来暂存指令、数据和地址。可以将其看作是内存的一种。根据种类的不同,一个 CPU 内部会有 20 - 100个寄存器。控制器
负责把内存上的指令、数据读入寄存器,并根据指令的结果控制计算机运算器
负责运算从内存中读入寄存器的数据时钟
负责发出 CPU 开始计时的时钟信号
CPU 是一系列寄存器的集合体
寄存器
就可以了,其余三个不用过多关注,为什么这么说?因为程序是把寄存器作为对象来描述的。程序计数器
程序计数器(Program Counter)
是用来存储下一条指令所在单元的地址。控制器
首先按程序计数器所指出的指令地址从内存中取出一条指令,然后分析和执行该指令,同时将PC的值加1指向下一条要执行的指令。0100
是程序运行的起始位置。Windows 等操作系统把程序从硬盘复制到内存后,会将程序计数器作为设定为起始位置 0100,然后执行程序,每执行一条指令后,程序计数器的数值会增加1(或者直接指向下一条指令的地址),然后,CPU 就会根据程序计数器的数值,从内存中读取命令并执行,也就是说,程序计数器控制着程序的流程。条件分支和循环机制
顺序执行、条件分支、循环判断
三种,顺序执行是按照地址的内容顺序的执行指令。条件分支是根据条件执行任意地址的指令。循环是重复执行同一地址的指令。顺序执行的情况比较简单,每执行一条指令程序计数器的值就是 + 1。 条件和循环分支会使程序计数器的值指向任意的地址,这样一来,程序便可以返回到上一个地址来重复执行同一个指令,或者跳转到任意指令。
if()
判断是一样的,在不满足条件的情况下,指令会直接跳过。所以 PC 的执行过程也就没有直接+1,而是下一条指令的地址。标志寄存器
jump(跳转指令)
,会根据当前的指令来判断是否跳转,上面我们提到了标志寄存器
,无论当前累加寄存器的运算结果是正数、负数还是零,标志寄存器都会将其保存减法
运算。函数调用机制
call
和 return
指令,再将函数的入口地址设定到程序计数器之前,call 指令会把调用函数后要执行的指令地址存储在名为栈的主存内。函数处理完毕后,再通过函数的出口来执行 return 指令。return 指令的功能是把保存在栈中的地址设定到程序计数器。MyFun 函数在被调用之前,0154 地址保存在栈中,MyFun 函数处理完成后,会把 0154 的地址保存在程序计数器中。这个调用过程如下通过地址和索引实现数组
数组
是指同样长度的数据在内存中进行连续排列的数据构造。用数组名表示数组全部的值,通过索引来区分数组的各个数据元素,例如: a[0] - a[4],[]
内的 0 - 4 就是数组的下标。CPU 指令执行过程
取指令
阶段是将内存中的指令读取到 CPU 中寄存器的过程,程序寄存器用于存储下一条指令所在的地址指令译码
阶段,在取指令完成后,立马进入指令译码阶段,在指令译码阶段,指令译码器按照预定的指令格式,对取回的指令进行拆分和解释,识别区分出不同的指令类别以及各种获取操作数的方法。执行指令
阶段,译码完成后,就需要执行这一条指令了,此阶段的任务是完成指令所规定的各种操作,具体实现指令的功能。访问取数
阶段,根据指令的需要,有可能需要从内存中提取数据,此阶段的任务是:根据指令地址码,得到操作数在主存中的地址,并从主存中读取该操作数用于运算。结果写回
阶段,作为最后一个阶段,结果写回(Write Back,WB)阶段把执行指令阶段的运行结果数据“写回”到某种存储形式:结果数据经常被写到CPU的内部寄存器中,以便被后续的指令快速地存取;
内存
什么是内存
主存
,其作用是存放 CPU 中的运算数据,以及与硬盘等外部存储设备交换的数据。只要计算机在运行中,CPU 就会把需要运算的数据调到主存中进行运算,当运算完成后CPU再将结果传送出来,主存的运行也决定了计算机的稳定运行。内存的物理结构
随机存储器(RAM):内存中最重要的一种,表示既可以从中读取数据,也可以写入数据。当机器关闭时,内存中的信息会 丢失
。只读存储器(ROM):ROM 一般只能用于数据的读取,不能写入数据,但是当机器停电时,这些数据不会丢失。 高速缓存(Cache):Cache 也是我们经常见到的,它分为一级缓存(L1 Cache)、二级缓存(L2 Cache)、三级缓存(L3 Cache)这些数据,它位于内存和 CPU 之间,是一个读写速度比内存 更快
的存储器。当 CPU 向内存写入数据时,这些数据也会被写入高速缓存中。当 CPU 需要读取数据时,会直接从高速缓存中直接读取,当然,如需要的数据在Cache中没有,CPU会再去读取内存中的数据。
1024个地址
。每个地址都会存放 1 byte 的数据,因此我们可以得出内存 IC 的容量就是 1 KB。内存的读写过程
首先给 VCC 接通 +5V 的电源,给 GND 接通 0V 的电源,使用 A0 - A9
来指定数据的存储场所,然后再把数据的值输入给D0 - D7
的数据信号,并把WR(write)
的值置为 1,执行完这些操作后,即可以向内存 IC 写入数据读出数据时,只需要通过 A0 - A9 的地址信号指定数据的存储场所,然后再将 RD 的值置为 1 即可。 图中的 RD 和 WR 又被称为控制信号。其中当WR 和 RD 都为 0 时,无法进行写入和读取操作。
内存的现实模型
地址
,下面是内存和楼层整合的模型图数据类型
的概念,从内存上来看,就是占用内存大小(占用楼层数)的意思。即使物理上强制以 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,以此类推。移位运算和乘除的关系
移位
运算,移位运算是指将二进制的数值的各个位置上的元素坐左移和右移操作,见下图补数
负数
的方法。正数
,是 1 时表示 负数
。那么 -1 用二进制数该如何表示呢?可能很多人会这么认为:因为 1 的二进制数是 0000 0001
,最高位是符号位,所以正确的表示 -1 应该是 1000 0001
,但是这个答案真的对吗?二进制补数
,补数就是用正数来表示的负数。补数
,我们需要将二进制的各数位的数值全部取反,然后再将结果 + 1 即可,先记住这个结论,下面我们来演示一下。1000 0001
(它是1的补数,不知道的请看上文,正确性先不管,只是用来做一下计算)来表示一下1000 0001
表示 -1 是完全错误的。1111 1111
,来论证一下它的正确性1111 1111
, 然后与 1 进行加法运算,得到的结果是九位的 1 0000 0000
,结果发生了溢出
,计算机会直接忽略掉溢出位,也就是直接抛掉 最高位 1 ,变为 0000 0000
。也就是 0,结果正确,所以 1111 1111
表示的就是 -1 。算数右移和逻辑右移的区别
0 和 1
。算数右移
。如果数值使用补数表示的负数值,那么右移后在空出来的最高位补 1,就可以正确的表示 1/2,1/4,1/8
等的数值运算。如果是正数,那么直接在空出来的位置补 0 即可。63
, 显然不是它的 1/4,所以不能使用逻辑右移,那么算数右移的情况下,右移两位会变为 -1
,显然是它的 1/4,故而采用算数右移。0111 1111
这个正的 8位二进制数转换成为 16位二进制数时,很容易就能够得出0000 0000 0111 1111
这个正确的结果,但是像 1111 1111
这样的补数来表示的数值,该如何处理?直接将其表示成为1111 1111 1111 1111
就可以了。也就是说,不管正数还是补数表示的负数,只需要将 0 和 1 填充高位即可。内存和磁盘的关系
存储器
、控制器
、运算器
、输入和输出设备
,其中从存储功能的角度来看,可以把存储器分为内存
和 磁盘
,我们上面介绍过内存,下面就来介绍一下磁盘以及磁盘和内存的关系程序不读入内存就无法运行
磁盘构造
磁盘缓存
缓存技术
,那么硬件层面也不例外,磁盘也有缓存,磁盘的缓存叫做磁盘缓存
。虚拟内存
虚拟内存
是内存和磁盘交互的第二个媒介。虚拟内存是指把磁盘的一部分作为假想内存
来使用。这与磁盘缓存是假想的磁盘(实际上是内存)相对,虚拟内存是假想的内存(实际上是磁盘)。连续可用
的内存(一个完整的地址空间),但是实际上,它通常被分割成多个物理碎片,还有部分存储在外部磁盘管理器上,必要时进行数据交换。置换(swap)
,然后运行程序。虚拟内存与内存的交换方式
分页式
和 分段式
两种。Windows 采用的是分页式。该方式是指在不考虑程序构造的情况下,把运行的程序按照一定大小的页进行分割,并以页
为单位进行置换。在分页式中,我们把磁盘的内容读到内存中称为 Page In
,把内存的内容写入磁盘称为 Page Out
。Windows 计算机的页大小为 4KB ,也就是说,需要把应用程序按照 4KB 的页来进行切分,以页(page)为单位放到磁盘中,然后进行置换。磁盘的物理结构
可变长方式
和 扇区方式
。前者是将物理结构划分成长度可变的空间,后者是将磁盘结构划分为固定长度的空间。一般 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
为单位来存储的。连续存储
的。压缩算法的定义
压缩算法(compaction algorithm)
指的就是数据压缩的算法,主要包括压缩和还原(解压缩)的两个步骤。无失真地
从压缩后的数据重构,准确地还原原始数据。可用于对数据的准确性要求严格的场合,如可执行文件和普通文件的压缩、磁盘的压缩,也可用于多媒体数据的压缩。该方法的压缩比较小。如差分编码、RLE、Huffman编码、LZW编码、算术编码。不能完全准确地
恢复原始数据,重构的数据只是原始数据的一个近似。可用于对数据的准确性要求不高的场合,如多媒体数据的压缩。该方法的压缩比较大。例如预测编码、音感编码、分形压缩、小波压缩、JPEG/MPEG。几种常用压缩算法的理解
RLE 算法的机制
AAAAAABBCDDEEEEEF
这 17 个半角字符的文件(文本文件)进行压缩。虽然这些文字没有什么实际意义,但是很适合用来描述 RLE
的压缩机制。去重化
,也就是 字符 * 重复次数
的方式进行压缩。所以上面文件压缩后就会变成下面这样数据 * 重复次数
的形式来表示的压缩方法成为 RLE(Run Length Encoding, 行程长度编码)
算法。RLE 算法是一种很好的压缩方法,经常用于压缩传真的图像等。因为图像文件的本质也是字节数据的集合体,所以可以用 RLE 算法进行压缩哈夫曼算法和莫尔斯编码
半角英文数字的1个字符是1个字节(8位)的数据
。下面我们就来认识一下哈夫曼算法的基本思想。不过要注意一点,最终磁盘的存储都是以8位为一个字节来保存文件的。
甜品
,了解一下 莫尔斯编码
,你一定看过美剧或者战争片的电影,在战争中的通信经常采用莫尔斯编码来传递信息,例如下面示例
,大家把 1 看作是短点(嘀),把 11 看作是长点(嗒)即可。短编码
来表示。如表所示,假如表示短点的位是 1,表示长点的位是 11 的话,那么 E(嘀)这一数据的字符就可以用 1 来表示,C(滴答滴答)就可以用 9 位的 110101101
来表示。在实际的莫尔斯编码中,如果短点的长度是 1 ,长点的长度就是 3,短点和长点的间隔就是1。这里的长度指的就是声音的长度。比如我们想用上面的 AAAAAABBCDDEEEEEF 例子来用莫尔斯编码重写,在莫尔斯曼编码中,各个字符之间需要加入表示时间间隔的符号。这里我们用 00 加以区分。用二叉树实现哈夫曼算法
出现频率高的字符用尽量少的位数编码来表示
这一原则进行整理。按照出现频率从高到低的顺序整理后,结果如下,同时也列出了编码方案。哈夫曼树能够提升压缩比率
可逆压缩和非可逆压缩
BMP
、JPEG
、TIFF
、GIF
格式等。BMP :是使用 Windows 自带的画笔来做成的一种图像形式 JPEG:是数码相机等常用的一种图像数据形式 TIFF: 是一种通过在文件中包含"标签"就能够快速显示出数据性质的图像形式 GIF:是由美国开发的一种数据形式,要求色数不超过 256个
可逆压缩
,无法还原到压缩前状态的压缩称为非可逆压缩
。操作系统
操作系统环境
运行环境
这一内容,可以说 运行环境 = 操作系统 + 硬件 ,操作系统又可以被称为软件,它是由一系列的指令组成的。我们不介绍操作系统,我们主要来介绍一下硬件的识别。操作系统版本:说的就是应用程序运行在何种系统环境,现在市面上主要有三种操作系统环境,Windows 、Linux 和 Unix ,一般我们玩儿的大型游戏几乎都是在 Windows 上运行,可以说 Windows 是游戏的天堂。Windows 操作系统也会有区分,分为32位操作系统和64位操作系统,互不兼容。
处理器:处理器指的就是 CPU,你的电脑的计算能力,通俗来讲就是每秒钟能处理的指令数,如果你的电脑觉得卡带不起来的话,很可能就是 CPU 的计算能力不足导致的。想要加深理解,请阅读博主的另一篇文章:程序员需要了解的硬核知识之CPU
显卡:显卡承担图形的输出任务,因此又被称为图形处理器(Graphic Processing Unit,GPU),显卡也非常重要,比如我之前玩儿的
剑灵
开五档(其实就是图像变得更清晰)会卡,其实就是显卡显示不出来的原因。内存:内存即主存,就是你的应用程序在运行时能够动态分析指令的这部分存储空间,它的大小也能决定你电脑的运行速度,想要加深理解,请阅读博主的另一篇文章 程序员需要了解的硬核知识之内存
存储空间:存储空间指的就是应用程序安装所占用的磁盘空间,由图中可知,此游戏的最低存储空间必须要大于 5GB,其实我们都会遗留很大一部分用来安装游戏。
本地代码(native code)
,程序员用 C 等高级语言编写的程序,仅仅是文本文件。文本文件(排除文字编码的问题)
在任何环境下都能显示和编辑。我们称之为源代码
。通过对源代码进行编译,就可以得到本地代码
。下图反映了这个过程。Windows 操作系统克服了CPU以外的硬件差异
不同操作系统的 API 差异性
API(Application Programming Interface)
。Windows 以及 Linux 操作系统的 API,提供了任何应用程序都可以利用的函数组合。因为不同操作系统的 API 是有差异的。所以,如何要将同样的应用程序移植到另外的操作系统,就必须要覆盖应用所用到的 API 部分。操作系统功能的历史
操作系统
其实也是一种软件,任何新事物的出现肯定都有它的历史背景,那么操作系统也不是凭空出现的,肯定有它的历史背景。按钮
来控制计算机,这一过程非常麻烦。于是,有人开发出了仅具有加载和运行功能的监控程序
,这就是操作系统的原型。通过事先启动监控程序,程序员可以根据需要将各种程序加载到内存中运行。虽然仍旧比较麻烦,但比起在没有任何程序的状态下进行开发,工作量得到了很大的缓解。硬件控制程序
,编程语言处理器(汇编、编译、解析)
以及各种应用程序等,结果就形成了和现在差异不大的操作系统,也就是说,其实操作系统是多个程序的集合体。Windows 操作系统的特征
资深
用户,你都知道 Windows 操作系统有哪些特征吗?下面列举了一些 Windows 操作系统的特性Windows 操作系统有两个版本:32位和64位 通过 API
函数集成来提供系统调用提供了采用图形用户界面的用户界面 通过 WYSIWYG
实现打印输出,WYSIWYG 其实就是 What You See Is What You Get ,值得是显示器上显示的图形和文本都是可以原样输出到打印机打印的。提供多任务功能,即能够同时开启多个任务 提供网络功能和数据库功能 通过即插即用实现设备驱动的自设定
32位操作系统
MS-DOS
等16位操作系统不同,因为在16位操作系统中处理32位数据需要两次,而32位操作系统只需要一次就能够处理32位的数据,所以一般在 windows 上的应用,它们的最高能够处理的数据都是 32 位的。char
类型,16位的short
类型,以及32位的long
类型三个选项,使用位数较大的 long 类型进行处理的话,增加的只是内存以及磁盘的开销,对性能影响不大。通过 API 函数集来提供系统调用
API
的函数集来提供系统调用的。API是联系应用程序和操作系统之间的接口,全称叫做 Application Programming Interface
,应用程序接口。Win32 API
,之所以这样命名,是需要和不同的操作系统进行区分,比如最一开始的 16 位版的 Win16 API
,和后来流行的 Win64 API
。MessageBox()
函数,就被保存在了 Windows 提供的 user32.dll 这个 DLL 文件中。提供采用了 GUI 的用户界面
GUI(Graphical User Interface)
指得就是图形用户界面,通过点击显示器中的窗口以及图标等可视化的用户界面,举个例子:Linux 操作系统就有两个版本,一种是简洁版,直接通过命令行控制硬件,还有一种是可视化版,通过光标点击图形界面来控制硬件。通过 WYSIWYG 实现打印输出
提供多任务功能
时钟分割
技术来实现多任务功能的。时钟分割指的是短时间间隔内,多个程序切换运行的方式。在用户看来,就好像是多个程序在同时运行,其底层是 CPU 时间切片
,这也是多线程多任务的核心。提供网络功能和数据库功能
中间件
而不是应用。意思是处于操作系统和应用的中间层,操作系统和中间件组合在一起,称为系统软件
。应用不仅可以利用操作系统,也可以利用中间件的功能。通过即插即用实现设备驱动的自动设定
即插即用(Plug-and-Play)
指的是新的设备连接(plug) 后就可以直接使用的机制,新设备连接计算机后,计算机就会自动安装和设定用来控制该设备的驱动程序
汇编语言和本地代码
add(addition)
的缩写、在比较运算符的本地代码中加上cmp(compare)
的缩写等,这些通过缩写来表示具体本地代码指令的标志称为 助记符
,使用助记符的语言称为汇编语言
。这样,通过阅读汇编语言,也能够了解本地代码的含义了。编译器
,转换的这个过程称为汇编
。在将源代码转换为本地代码这个功能方面,汇编器和编译器是同样的。反汇编
,执行反汇编的程序称为反汇编程序
。通过编译器输出汇编语言的源代码
Borland C++ 5.5
编译器,为了方便,我这边直接下载好了读者直接从我的百度网盘提取即可 (链接:https://pan.baidu.com/s/19LqVICpn5GcV88thD2AnlA 密码:hz1u)// 返回两个参数值之和的函数
int AddNum(int a,int b){
return a + b;
}
// 调用 AddNum 函数的函数
void MyFunc(){
int c;
c = AddNum(123,456);
}
.c
来表示,上面程序是提供两个输入参数并返回它们之和。命令提示符
,切换到保存 Sample4.c 的文件夹下,然后在命令提示符中输入bcc32 -c -S Sample4.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
不会转换成本地代码的伪指令
_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
围起来的部分,是给构成程序的命令和数据的集合体上加一个名字而得到的,称为段定义
。段定义的英文表达具有区域
的意思,在这个程序中,段定义指的是命令和数据等程序的集合体的意思,一个程序由多个段定义构成。_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
伪指令,表示的是源代码的结束。汇编语言的语法是 操作码 + 操作数
Give me money
这个英文指令的话,Give 就是操作码,me 和 money 就是操作数。汇编语言中存在多个操作数的情况,要用逗号把它们分割,就像是 Give me,money 这样。指令解析
mov
指令,mov 指令的两个操作数,分别用来指定数据的存储地和读出源。操作数中可以指定寄存器、常数、标签(附加在地址前),以及用方括号([])
围起来的这些内容。如果指定了没有用([])
方括号围起来的内容,就表示对该值进行处理;如果指定了用方括号围起来的内容,方括号的值则会被解释为内存地址,然后就会对该内存地址对应的值进行读写操作。让我们对上面的代码片段进行说明 mov ebp,esp
mov eax,dword ptr [ebp+8]
mov eax,dword ptr [ebp+8]
这条指令中,ebp 寄存器的值 + 8 后会被解析称为内存地址。如果 ebpdword ptr
也叫做 double word pointer
简单解释一下就是从指定的内存地址中读出4字节的数据入栈
,从栈中读出数据称为 出栈
,32位 x86 系列的 CPU 中,进行1次 push 或者 pop,即可处理 32 位(4字节)的数据。函数的调用机制
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
AddNum
函数处理内容时进行说明。这里希望大家先关注(3) - (6) 这一部分,这对了解函数调用机制至关重要。函数名
表示的就是函数所在的内存地址。AddNum 函数处理完毕后,程序流程必须要返回到编号(6) 这一行。call 指令运行后,call 指令的下一行(也就指的是 (6) 这一行)的内存地址(调用函数完毕后要返回的内存地址)会自动的 push 入栈。该值会在 AddNum 函数处理的最后通过 ret
指令 pop 出栈,然后程序会返回到 (6) 这一行。Sample4.c
文件时,出现了下图的这条消息函数的内部处理
_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
自动出栈
,据此,程序流程就会跳转返回到(6) (Call _AddNum)
的下一行。这时,AddNum 函数入口和出口处栈的状态变化,就如下图所示全局变量和局部变量
全局变量
,在函数内部定义的变量称为局部变量
,全局变量可以在任意函数中使用,局部变量只能在函数定义局部变量的内部使用。下面,我们就通过汇编语言来看一下全局变量和局部变量的不同之处。// 定义被初始化的全局变量
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;
}
_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 的段定义中
_DATA segment dword public use32 'DATA'
...
_DATA ends
没有初始化的全局变量,会汇总到名为 _BSS 的段定义中
_BSS segment dword public use32 'BSS'
...
_BSS ends
被段定义 _TEXT 围起来的汇编代码则是 Borland C++ 的定义
_TEXT segment dword public use32 'CODE'
_MyFunc proc near
...
_MyFunc endp
_TEXT ends
_DATA
段定义的内容。_a1 label dword
定义了 _a1
这个标签。标签表示的是相对于段定义起始位置的位置。由于_a1
在 _DATA 段
定义的开头位置,所以相对位置是0。_a1
就相当于是全局变量a1。编译后的函数名和变量名前面会加一个(_)
,这也是 Borland C++ 的规定。dd 1
指的是,申请分配了4字节的内存空间,存储着1这个初始值。dd指的是 define double word
表示有两个长度为2的字节领域(word),也就是4字节的意思。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
临时确保局部变量使用的内存空间
_TEXT
段定义表示的是 MyFunc
函数的范围。在 MyFunc 函数中定义的局部变量所需要的内存领域。会被尽可能的分配在寄存器中。大家可能认为使用高性能的寄存器来替代普通的内存是一种资源浪费,但是编译器不这么认为,只要寄存器有空间,编译器就会使用它。由于寄存器的访问速度远高于内存,所以直接访问寄存器能够高效的处理。局部变量使用寄存器,是 Borland C++ 编译器最优化的运行结果。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
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();
}
}
MySub
函数,下面是它主要的汇编代码 xor ebx, ebx ; 将寄存器清0
@4 call _MySub ; 调用MySub函数
inc ebx ; ebx寄存器的值 + 1
cmp ebx,10 ; 将ebx寄存器的值和10进行比较
jl short @4 ; 如果小于10就跳转到 @4
比较指令(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。
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 所在的指令处继续执行。 i ^= i;
L4: MySub();
i++;
if(i < 10) goto L4;
条件分支的处理方法
// 定义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();
}
}
_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
,构成了上述汇编代码的主要逻辑形式。了解程序运行逻辑的必要性
专心只做一件事情
,一件事情做完之后才会去做另外一件事情。// 定义全局变量
int counter = 100;
// 定义MyFunc1()
void MyFunc(){
counter *= 2;
}
// 定义MyFunc2()
void MyFunc2(){
counter *= 2;
}
多线程处理
,同时调用了一次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 中。
锁定
方法,或者使用某种线程安全的方式来避免该问题的出现。