cpu是什么
CPU 是英文 Central Processing Unit( 中央处理器)的缩写,相当于计算机的大脑,它的内部由数百万至数亿个晶体管构成这些都是大家所熟知的。
CPU 的内部结构解析
CPU和内存是由许多晶体管组成的电子部件,通常称为IC( Integrated Circuit,集成电路)。从功能方面来看,CPU的内部由寄存器、控制器、运算器和时钟四个部分构成,各部分之间由电流信号相互连通。
寄存器可用来暂存指令、数据等处理对象,可以将其看作是内存的一种。根据种类的不同,一个 CPU 内部会有20~100个寄存器。
控制器负责把内存上的指令、数据等读人寄存器并根据指令的执行结果来控制整个计算机。
运算器负责运算从内存读入寄存器的数据。
时钟负责发出 CPU 开始计时的时钟信号“。不过,也有些计算机的时钟位于CPU的外部。
通常所说的内存指的是计算机的主存储器( main memory,简称主存。主存通过控制芯片等与CPU相连主要负责存储指令和数据。主存由可读写的元素构成,每个字节(1字节 =8位)都带有一个地址编号。CPU 可以通过该地址读取主存中的指令和数据,当然也可以写入数据。主存中存储的指令和数据会随着计算机的关机而自动清除。
程序启动后,根据时钟信号,控制器会从内存中读取指令和数据。通过对这些指令加以解释和运行,运算器就会对数据进行运算,控制器根据该运算结果来控制计算机。所谓的控制就是指数据运算以外的处理(主要是数据输入输出的时机控制 )。比如内存和磁盘等媒介的输入输出、键盘和鼠标的输人、显示器和打印机的输出等,这些都是控制的内容。
CPU 是寄存器的集合体
程序是把寄存器作为对象来描述的。
1 | mov eax, dword ptr [ebp-8] …把数值从内存复制到eax |
汇编语言是80386以上的CPU所使用的语言。eax和ebp是CPU内部的寄存器的名称。内存的存储场所通过地址编号来区分,而寄存器的种类则通过名字来区分。使用高级语言编写的程序会在编译后转化成机器语言,然后再通过CPU内部的寄存器来处理。
寄存器中存储的内容既可以是指令也可以是数据。其中,数据分为“用于运算的数值”和“表示内存地址的数值”两种。数据种类不同,存储该数值的寄存器也不同。CPU中每个寄存器的功能都是不同的。用于运算的数值放在累加寄存器中存储,表示内存地址的数值则放在基址寄存器和变址寄存器中存储。
CPU是具有各种功能的寄存器的集合体。其中,程序计数器、累加寄存器、标志寄存器、指令寄存器和栈寄存器都只有一个,其他的寄存器一般有多个。
程序计数器
地址0100是程序运行的开始位置。Windows等操作系统把程序从硬盘复制到内存后,会将程序计数器(CPU寄存器的一种)设定为0100,然后程序便开始运行。CPU每执行一个指令,程序计数器的值就会自动加1。例如,CPU执行0100地址的指令后,程序计数器的值就变成了0101(当执行的指令占据多个内存地址时,增加与指令长度相应的数值)。然后,CPU的控制器就会参照程序计数器的数值,从内存中读取命令并执行。也就是说,程序计数器决定着程序的流程。
条件分支和循环机制
CPU在进行运算时,标志寄存器的数值会根据运算结果自动设定。条件分支在跳转指令前会进行比较运算。至于是否执行跳转指令,则由CPU在参考标志寄存器的数值后进行判断。运算结果的正、零、负三种状态由标志寄存器的三个位表示。图1-6是32位CPU(寄存器的长度是32位)的标志寄存器的示例。标志寄存器的第一个字节位、第二个字节位和第三个字节位的值为1时,表示运算结果分别为正数、零和负数。
假设要比较累加寄存器中存储的XXX值和通用寄存器中存储的YYY值,执行比较的指令后,CPU的运算装置就会在内部(暗中)进行XXX-YYY的减法运算。而无论减法运算的结果是正数、零还是负数,都会保存到标志寄存器中。结果为正表示XXX比YYY大,零表示XXX和YYY相等,负表示XXX比YYY小。程序中的比较指令,就是在CPU内部做减法运算。
函数的调用机制
哪怕是高级语言编写的程序,函数调用处理也是通过把程序计数器的值设定成函数的存储地址来实现的。
函数的调用需要在完成函数内部的处理后,处理流程再返回到函数调用点(函数调用指令的下一个地址)。因此,如果只是跳转到函数的入口地址,处理流程就不知道应该返回至哪里了。
给变量a和b分别代入123和456后,将其赋值给参数(parameter)来调用MyFunc函数的C语言程序。图中的地址是将C语言编译成机器语言后运行时的地址。由于1行C语言程序在编译后通常会变成多行的机器语言,所以图中的地址是离散的。
此外,通过跳转指令把程序计数器的值设定成0260也可实现调用MyFunc函数。函数的调用原点(0132地址)和被调用函数(0260地址)之间的数据传递,可以通过内存或寄存器来实现。不过,当函数处理进行到最后的0354地址时,我们知道应该将程序计数器的值设定成函数调用后要执行的0154地址,但实际上这一操作根本无法实现。
机器语言的call指令和return指令能够解决这个问题。建议大家把二者结合起来来记忆。函数调用使用的是call指令,而不是跳转指令。在将函数的入口地址设定到程序计数器之前,call指令会把调用函数后要执行的指令地址存储在名为栈的主存内。函数处理完毕后,再通过函数的出口来执行return命令。return命令的功能是把保存在栈中的地址设定到程序计数器中。
通过地址和索引实现数组
基址寄存器和变址寄存器。通过这两个寄存器,我们可以对主内存上特定的内存区域进行划分,从而实现类似于数组的操作。
首先,我们用十六进制数将计算机内存上00000000~FFFFFFFF的地址划分出来。那么,凡是该范围的内存区域,只要有一个32位的寄存器,即可查看全部的内存地址。但如果想要像数组那样分割特定的内存区域以达到连续查看的目的,使用两个寄存器会更方便些。例如,查看10000000地址~1000FFFF地址时,可以将10000000存入基址寄存器,并使变址寄存器的值在00000000~0000FFFF变化。CPU则会把基址寄存器+变址寄存器的值解释为实际查看的内存地址。变址寄存器的值就相当于高级编程语言程序中数组的索引功能。
CPU的处理
数据
计算机内部是由IC这种电子部件构成的。
CPU(微处理器)和内存也是IC的一种。IC有几种不同的形状,有的像一条黑色蜈蚣,在其两侧有数个乃至数百个引脚;有的则像插花用的针盘,引脚在IC内部并排排列着。IC的所有引脚,只有直流电压0V或5V两个状态。也就是说,IC的一个引脚,只能表示两个状态。
IC的这个特性,决定了计算机的信息数据只能用二进制数来处理。由于1位(一个引脚)只能表示两个状态,所以二进制的计数方式就变成了0、1、10、11、100…这种形式。
内存和磁盘都使用字节单位来存储和读写数据,使用位单位则无法读写数据。因此,字节是信息的基本单位。奔腾等32位微处理器,具有32个引脚以用于信息的输入和输出。也就是说,奔腾一次可以处理32位(32位=4字节)的二进制数信息。
二进制数
十进制数39用8位的二进制表示是00100111,左移两位后是10011100,再转换成十进制数就是156。
补数
二进制数中表示负数值时,一般会把最高位作为符号来使用,因此我们把这个最高位称为符号位。符号位是0时表示正数,符号位是1时表示负数。那么-1用8位二进制数来表示的话是什么样的呢?
正确答案是11111111。
计算机在做减法运算时,实际上内部是在做加法运算。补数就是用正数来表示负数。
为了获得补数,我们需要将二进制数的各数位的数值全部取反,然后再将结果加1。例如,用8位二进制数表示- 1时,只需求得1,也就是00000001的补数即可。具体来说,就是将各数位的0取反成1, 1取反成0,然后再将取反的结果加1,最后就转化成了11111111。
00000001+11111111确实为0(=00000000)。这个运算中出现了最高位溢出的情况,不过,正如之前所介绍的那样,对于溢出的位,计算机会直接忽略掉。在8位的范围内进行计算时,100000000这个9位二进制数就会被认为是00000000这一8位二进制数。
当运算结果为负数时,计算结果的值也是以补数的形式来表示的。
比如3- 5这个运算,用8位二进制数表示3时为00000011,而5=00000101的补数为“取反+1”,也就是11111011。因此3- 5其实就是00000011+ 11111011的运算。00000011 + 11111011的运算结果为11111110,最高位变成了1。
C语言的数据类型中,既有不能处理负数的unsigned short类型,也有能处理负数的short类型。这两种类型,都是2字节(=16位)的变量,都能表示2的16次幂=65536种值,这一点是相同的。不过,值的范围有所不同,short类型是- 32768~32767,unsigned short类型是0~65535。
逻辑右移和算术右移
右移有移位后在最高位补0和补1两种情况。
当二进制数的值表示图形模式而非数值时,移位后需要在最高位补0。
将二进制数作为带符号的数值进行运算时,移位后要在最高位填充移位前符号位的值(0或1)。这就称为算术右移。如果数值是用补数表示的负数值,那么右移后在空出来的最高位补1,就可以正确地实现1/2、1/4、1/8等的数值运算。如果是正数,只需在最高位补0即可。
现在我们来看一个右移的例子。将- 4(=11111100)右移两位。这时,逻辑右移的情况下结果就会变成00111111,也就是十进制数63,显然不是- 4的1/4。而算术右移的情况下,结果就会变成11111111,用补数表示就是- 1,即- 4的1/4。
符号扩充
符号扩充就是指在保持值不变的前提下将其转换成16位和32位的二进制数。
将01111111这个正的8位二进制数转换成16位二进制数时,很容易就能得出0000000001111111这个正确结果。
11111111这样用补数来表示的数值,该如何处理比较好呢?
实际上处理方法非常简单,将其表示成1111111111111111就可以了。也就是说,不管是正数还是用补数表示的负数,都只需用符号位的值(0或者1)填充高位即可。
逻辑运算
计算机能处理的运算,大体可分为算术运算和逻辑运算。算术运算是指加减乘除四则运算。逻辑运算是指对二进制数各数字位的0和1分别进行处理的运算,包括逻辑非(NOT运算)、逻辑与(AND运算)、逻辑或(OR运算)和逻辑异或(XOR运算)四种。
计算机进行小数运算时出错的原因
将0.1累加100次也得不到10
1 |
|
10.000002
用二进制数表示小数
由于计算机内部所有的信息都是以二进制数的形式来处理的,因此在这一点上,整数和小数并无差别。不过,使用二进制数来表示整数和小数的方法却有很大的不同。
把1011.0011这个有小数点的二进制数转换成十进制数。
计算机运算出错的原因
计算机之所以会出现运算错误,是因为“有一些十进制数的小数无法转换成二进制数”。
小数点后4位用二进制数表示时的数值范围为0.0000~0.1111。因此,这里只能表示0.5、0.25、0.125、0.0625这四个二进制数小数点后面的位权组合而成(相加总和)的小数。
因为无法正确表示的数值,最后都变成了近似值。计算机这个功能有限的机器设备,是无法处理无限循环的小数的。因此,在遇到循环小数时,计算机就会根据变量数据类型所对应的长度将数值从中间截断或者四舍五入。
浮点数
双精度浮点数类型用64位、单精度浮点数类型用32位来表示全体小数。
内存
内存的物理机制
内存实际上是一种名为内存IC的电子元件。虽然内存IC包括DRAM、SRAM、ROM等多种形式,但从外部来看,其基本机制都是一样的。内存IC中有电源、地址信号、数据信号、控制信号等用于输入输出的大量引脚(IC的引脚),通过为其指定地址(address),来进行数据的读写。
VCC和GND是电源,A0~A9是地址信号的引脚,D0~D7是数据信号的引脚,RD和WR是控制信号的引脚。将电源连接到VCC和GND后,就可以给其他引脚传递比如0或者1这样的信号。大多数情况下,+ 5V的直流电压表示1,0V表示0。
首先,我们假设要往该内存IC中写入1字节的数据。为了实现该目的,可以给VCC接入+5V,给GND接入0V的电源,并使用A0~A9的地址信号来指定数据的存储场所,然后再把数据的值输入给D0~D7的数据信号,并把WR(write=写入的简写)信号设定成1。执行完这些操作,就可以在内存IC内部写入数据(图4-2 (a))了。
读出数据时,只需通过A0~A9的地址信号指定数据的存储场所,然后再将RD(read=读出的简写)信号设成1即可。
当WR和RD同时为0时,写入和读出的操作都无法进行。
内存的逻辑模型
指针
指针也是一种变量,它所表示的不是数据的值,而是存储着数据的内存的地址。通过使用指针,就可以对任意指定地址的数据进行读写。
Windows计算机上使用的程序通常都是32位(4字节)的内存地址。这种情况下,指针变量的长度也是32位。
数组
数组是指多个同样数据类型的数据在内存中连续排列的形式。作为数组元素的各个数据会通过连续的编号被区分开来,这个编号称为索引(index)。指定索引后,就可以对该索引所对应地址的内存进行读写操作。而索引和内存地址的变换工作则是由编译器自动实现的。
栈、队列以及环形缓冲区
栈和队列,都可以不通过指定地址和索引来对数组的元素进行读写。需要临时保存计算过程中的数据、连接在计算机上的设备或者输入输出的数据时,都可以通过这些方法来使用内存。如果每次保存临时数据都需指定地址和索引,程序就会变得比较麻烦,因此要加以改进。
队列一般是以环状缓冲区(ring buffer)的方式来实现的.
内存和磁盘
不读入内存就无法运行
计算机中主要的存储部件是内存和磁盘。磁盘中存储的程序,必须要加载到内存后才能运行。在磁盘中保存的原始程序是无法直接运行的。
这是因为,负责解析和运行程序内容的CPU,需要通过内部程序计数器来指定内存地址,然后才能读出程序。即使CPU可以直接读出并运行磁盘中保存的程序,由于磁盘读取速度慢,程序的运行速度还是会降低。
磁盘缓存加快了磁盘访问速度
磁盘缓存指的是把从磁盘中读出的数据存储到内存空间中的方式。使用磁盘缓存可以大大改善磁盘数据的访问速度。
把低速设备的数据保存在高速设备中,需要时可以直接将其从高速设备中读出,这种缓存的方式在其他情况下也会用到。
其中的一个实例就是在Web浏览器中的使用。由于Web浏览器是通过网络来获取远程Web服务器的数据并将其显示出来的。因此,在显示较大的图片等文件时,会花费不少时间。于是,Web浏览器就可以把获取的数据暂时保存在磁盘中,然后在需要时再显示磁盘中的数据。也就是说,把低速的网络数据保存到相对高速的磁盘中。
虚拟内存把磁盘作为部分内存来使用
虚拟内存是指把磁盘的一部分作为假想的内存来使用。这与磁盘缓存是假想的磁盘(实际上是内存)相对,虚拟内存是假想的内存(实际上是磁盘)。
通过借助虚拟内存,在内存不足时也可以运行程序。
为了实现虚拟内存,就必须把实际内存(也可称为物理内存)的内容,和磁盘上的虚拟内存的内容进行部分置换(swap),并同时运行程序。
节约内存的编程方法
由于使用虚拟内存时发生的Page In和Page Out往往伴随着低速的磁盘访问,因此在这个过程中应用的运行会变得迟钝起来。
也就是说,虚拟内存无法彻底解决内存不足的问题。
为了从根本上解决内存不足的问题,需要增加内存的容量,或者尽量把运行的应用文件变小。
(1)通过DLL文件实现函数共有
DLL(Dynamic Link Library)文件,顾名思义,是在程序运行时可以动态加载Library(函数和数据的集合)的文件。多个应用可以共有同一个DLL文件。而通过共有同一个DLL文件则可以达到节约内存的效果。
例如,假设我们编写了一个具有某些处理功能的函数MyFunc()。应用A和应用B都会使用这个函数。在各个应用的运行文件中内置函数MyFunc()(这个称为Static Link,静态链接)后同时运行这两个应用,内存中就存在了具有同一函数的两个程序。但这会导致内存的利用效率降低。所以,有两个同样的函数,还是有点浪费。
此内存中存在的函数MyFunc()的程序就只有1个。这样一来,内存的利用效率也就提高了。
Windows的操作系统本身也是多个DLL文件的集合体。有时在安装新应用时,DLL文件也会被追加。应用则会通过利用这些DLL文件的功能来运行。像这样,之所以要利用多个DLL文件,其中一个原因就是可以节约内存。而且DLL文件还有一个优点就是,在不变更EXE文件的情况下,只通过升级DLL文件就可以更新。
(2)通过调用_stdcall来减小程序文件的大小
C语言中,在调用函数后,需要执行栈清理处理指令。栈清理处理是指,把不需要的数据从接收和传递函数的参数时使用的内存上的栈区域中清理出去。该命令不是程序记述的,而是在程序编译时由编译器自动附加到程序中的。编译器默认将该处理附加在函数调用方。
磁盘的物理结构
磁盘是通过把其物理表面划分成多个空间来使用的。划分的方式有扇区方式和可变长方式两种,前者是指将磁盘划分为固定长度的空间,后者则是指把磁盘划分为长度可变的空间。一般的Windows计算机所使用的硬盘和软盘,采用的都是扇区方式。扇区方式中,把磁盘表面分成若干个同心圆的空间就是磁道,把磁道按照固定大小(能存储的数据长度相同)划分而成的空间就是扇区。
扇区是对磁盘进行物理读写的最小单位。Windows中使用的磁盘,一般1个扇区是512字节。不过,Windows在逻辑方面(软件方面)对磁盘进行读写的单位是扇区整数倍簇。根据磁盘容量的不同,1簇可以是512字节(1簇=1扇区)、1KB(1簇=2扇区)、2KB、4KB、8KB、16KB、32KB(1簇=64扇区)。磁盘的容量越大,簇的容量也越大。不过,在软盘中,1簇=512字节=1扇区,簇和扇区的大小是相等的。
不管是多么小的文件,都会占用1簇的空间。这样一来,所有的文件都会占用1簇的整数倍的磁盘空间。如果减少簇的容量,磁盘访问次数就会增加,就会导致读写文件的时间变长。
压缩数据
文件以字节为单位保存
文件是将数据存储在磁盘等存储媒介中的一种形式。
RLE算法的机制
AAAAAABBCDDEEEEEF就可以用A6B2C1D2E5F1来表示。
把文件内容用“数据×重复次数”的形式来表示的压缩方法称为RLE(Run Length Encoding,行程长度编码)算法(图6-2)。RLE算法是一种很好的压缩方法,经常被用于压缩传真的图像等。因为图像文件本质上也是字节数据的集合体,所以可以用RLE算法来压缩。
RLE算法的缺点
然而,在实际的文本文件中,同样字符多次重复出现的情况并不多见。虽然针对相同数据经常连续出现的图像、文件等,RLE算法可以发挥不错的效果,但它并不适合文本文件的压缩。不过,因为该压缩机制非常简单,因此使用RLE算法的程序也相对更容易编写。
哈夫曼编码
哈夫曼算法的关键就在于“多次出现的数据用小于8位的字节数来表示,不常用的数据则可以用超过8位的字节数来表示”。
不过有一点需要注意,不管是不满8位的数据,还是超过8位的数据,最终都要以8位为单位保存到文件中。这是因为磁盘是以字节(8位)为单位来保存数据的。为了实现这一处理,压缩程序的内容会复杂很多,不过作为回报,最终得到的压缩率也是相当高的。
可逆压缩和非可逆压缩
图像文件的使用目的通常是把图像数据输出到显示器、打印机等设备上。Windows的标准图像数据形式为BMP,是完全未压缩的。
我们把能还原到压缩前状态的压缩称为可逆压缩,无法还原到压缩前状态的压缩称为非可逆压缩。
程序是在何种环境中运行的
运行环境=操作系统 + 硬件
CPU只能解释其自身固有的机器语言。不同的CPU能解释的机器语言的种类也是不同的。例如,CPU有x86、MIPS、SPARC、PowerPC等几种类型,它们各自的机器语言是完全不同的。
机器语言的程序称为本地代码(native code)。程序员用C语言等编写的程序,在编写阶段仅仅是文本文件。文本文件(排除文字编码的问题)在任何环境下都能显示和编辑。我们称之为源代码。通过对源代码进行编译,就可以得到本地代码。
Windows克服了CPU以外的硬件差异
计算机的硬件并不仅仅是由CPU构成的,还包括用于存储程序指令和数据的内存,以及通过I/O连接的键盘、显示器、硬盘、打印机等外围设备。
在Windows的应用软件中,键盘输入、显示器输出等并不是直接向硬件发送指令,而是通过向Windows发送指令来间接实现的。因此,程序员就不用注意内存和I/O地址的不同构成了。因为Windows操作的是硬件而非应用软件,而且针对不同的机型,这些硬件的构成也是有差异的。
MS-DOS应用大多都是不经过操作系统而直接控制硬件的,而Windows应用则基本上都由Windows来完成对硬件的控制
不同操作系统的API不同
同样机型的计算机,可安装的操作系统类型也会有多种选择。
当然,应用软件则必须根据不同的操作系统类型来专门开发。CPU的类型不同,所对应的机器语言也不同,同样的道理,操作系统的类型不同,应用程序向操作系统传递指令的途径也是不同的。
应用程序向操作系统传递指令的途径称为API(Application Programming Interface)。Windows及Unix系列操作系统的API,提供了任何应用程序都可以利用的函数组合。因为不同操作系统的API是有差异的,因此,将同样的应用程序移植到其他操作系统时,就必须要重写应用中利用到API的部分。像键盘输入、鼠标输入、显示器输出、文件输入输出等同外围设备进行输入输出操作的功能,都是通过API提供的。
在同类型操作系统下,不管硬件如何,API基本上没有差别。因而,针对某特定操作系统的API所编写的程序,在任何硬件上都可以运行。当然,由于CPU种类不同,机器语言也不相同,因此本地代码当然也是不同的。这种情况下,就需要利用能够生成各CPU专用的本地代码的编译器,来对源代码进行重新编译了。
FreeBSD Port
既然CPU类型不同会导致同样的本地代码无法重复利用,那么为何不直接把源代码分发给程序呢?
Unix系列操作系统FreeBSD中,存在一种名为Ports的机制。该机制能够结合当前运行的硬件环境来编译应用的源代码,进而得到可以运行的本地代码系统。如果目标应用的源代码没有在硬件上的话,Ports就会自动使用FTP连接到相关站点来下载代码。
FreeBSD上应用的源代码,大部分都是用C语言来记述的。FreeBSD等Unix系列操作系统中,都带有标准的C编译器。C编译器可以结合FreeBSD的运行环境生成合适的本地代码。
提供相同运行环境的Java虚拟机
有一种方法能够提供不依赖于特定硬件及操作系统的程序运行环境,那就是Java。
同其他编程语言相同,Java也是将Java语法记述的源代码编译后运行。不过,编译后生成的并不是特定CPU使用的本地代码,而是名为字节代码的程序。字节代码的运行环境就称为Java虚拟机(JavaVM, Java Virtual Machine)。Java虚拟机是一边把Java字节代码逐一转换成本地代码一边运行的。而Java虚拟机(java.exe)则会把字节代码变换成x86系列CPU适用的本地代码,然后由x86系列CPU负责实际的处理。
Java虚拟机每次运行时都要把字节代码变换成本机代码,这一机制是造成运行速度慢的原因。
BIOS和引导
BIOS存储在ROM中,是预先内置在计算机主机内部的程序。BIOS除了键盘、磁盘、显卡等基本控制程序外,还有启动“引导程序”的功能。引导程序是存储在启动驱动器起始区域的小程序。操作系统的启动驱动器一般是硬盘,不过有时也可以是CD-ROM或软盘。
开机后,BIOS会确认硬件是否正常运行,没有问题的话就会启动引导程序。引导程序的功能是把在硬盘等记录的OS加载到内存中运行。虽然启动应用是OS的功能,但OS并不能自己启动自己,而是通过引导程序来启动。
从源文件到可执行文件
计算机只能运行本地代码
CPU能直接解析并运行的不是源代码而是本地代码的程序。作为计算机大脑的Pentium等CPU,也只能解释已经转换成本地代码的程序内容。
编译器负责转换源代码
能够把C语言等高级编程语言编写的源代码转换成本地代码的程序称为编译器。每个编写源代码的编程语言都需要其专用的编译器。
编译器首先读入代码的内容,然后再把源代码转换成本地代码。编译器中就好像有一个源代码同本地代码的对应表。但实际上,仅仅靠对应表是无法生成本地代码的。读入的源代码还要经过语法解析、句法解析、语义解析等,才能生成本地代码。
根据CPU类型的不同,本地代码的类型也不同。因而,编译器不仅和编程语言的种类有关,和CPU的类型也是相关的。
还有一种交叉编译器,它生成的是和运行环境中的CPU不同的CPU所使用的本地代码。
仅靠编译是无法得到可执行文件的
编译器转换源代码后,就会生成本地文件。不过,本地文件是无法直接运行的。为了得到可以运行的EXE文件,编译之后还需要进行“链接”处理。
编译后生成的不是EXE文件,而是扩展名为“.obj”的目标文件。
把多个目标文件结合,生成1个EXE文件的处理就是链接,运行连接的程序就称为链接器(linkage editor或连结器)。
DLL文件及导入库
Windows以函数的形式为应用提供了各种功能。这些形式的函数称为API(Application Programming Interface,应用程序接口)。
Windows中,API的目标文件,并不是存储在通常的库文件中,而是存储在名为DLL(Dynamic Link Library)文件的特殊库文件中。就如Dynamic这一名称所表示的那样,DLL文件是程序运行时动态结合的文件。
与此相反,存储着目标文件的实体,并直接和EXE文件结合的库文件形式称为静态链接库。
系统调用和高级编程语言的移植性
操作系统的硬件控制功能,通常是通过一些小的函数集合体的形式来提供的。这些函数及调用函数的行为统称为系统调用(system call),也就是应用对操作系统(system)的功能进行调用(call)的意思。
高级编程语言的机制就是,使用独自的函数名,然后再在编译时将其转换成相应操作系统的系统调用(也有可能是多个系统调用的组合)。也就是说,用高级编程语言编写的应用在编译后,就转换成了利用系统调用的本地代码(图9-6)。
操作系统和高级编程语言使硬件抽象化
通过使用高级编程语言,有时甚至也无需考虑系统调用的存在。这是因为操作系统和高级编程语言能够使硬件抽象化。
当前主流的32位版Windows API也称为 Win32 API。之所以这样命名,是为了便于和以前的16位版的Win16 API,以及更先进的64位版的Win64 API区分开来。Win32 API中,各函数的参数及返回值的数据大小,基本上都是32位。
通过汇编语言了解程序的实际构成
汇编语言和本地代码是一一对应的
通过调查本地代码的内容,可以了解程序最终是以何种形式来运行的。但是,如果直接打开本地代码来看的话,只能看到数值的罗列。如果直接使用这些数值来编写程序的话,还真是不太容易理解。因而就产生了这样一种想法,那就是在各本地代码中,附带上表示其功能的英语单词缩写。例如,在加法运算的本地代码中加上add(addition的缩写)、在比较运算的本地代码中加上cmp(compare的缩写)等。
这些缩写称为助记符,使用助记符的编程语言称为汇编语言。
哪怕是用C语言编写的源代码,编译后也会转换成特定CPU用的本地代码。而将其反汇编的话,就可以得到汇编语言的源代码,并对其内容进行调查。不过,本地代码变换成C语言源代码的反编译,则要比反汇编困难。这是因为,C语言的源代码同本地代码不是一一对应的,因此完全还原到原始的源代码是不太可能的
通过编译器输出汇编语言的源代码
汇编语言源文件的扩展名,通常用“.asm”来表示。
不会转换成本地代码的伪指令
汇编语言的源代码,是由转换成本地代码的指令(后面讲述的操作码)和针对汇编器的伪指令构成的。伪指令负责把程序的构造及汇编的方法指示给汇编器(转换程序)。不过伪指令本身是无法汇编转换成本地代码的。
伪指令proc和endp围起来的部分,表示的是过程(procedure)的范围。在汇编语言中,这种相当于C语言的函数的形式称为过程。
汇编语言的语法是“操作码+操作数”
在汇编语言中,1行表示对CPU的一个指令。汇编语言指令的语法结构是操作码+操作数(也存在只有操作码没有操作数的指令)。
能够使用何种形式的操作码,是由CPU的种类决定的。
本地代码加载到内存后才能运行。内存中存储着构成本地代码的指令和数据。程序运行时,CPU会从内存中把指令和数据读出,然后再将其存储在CPU内部的寄存器中进行处理。
寄存器是CPU中的存储区域。不过,寄存器并不仅仅具有存储指令和数据的功能,也有运算功能。寄存器的名称会通过汇编语言的源代码指定给操作数。内存中的存储区域是用地址编号来区分的。CPU内的寄存器是用eax及ebx这些名称来区分的。此外,CPU内部也有程序员无法直接操作的寄存器。例如,表示运算结果正负及溢出状态的标志寄存器及操作系统专用的寄存器等,都无法通过程序员编写的程序直接进行操作。
x86系列32位CPU的寄存器名称中,开头都带了一个字母e,例如eax、ebx、ecx、edx等。这是因为16位CPU的寄存器名称是ax、bx、cx、dx等。32位CPU寄存器的名称中的e,有扩展(extended)的意思。我们也可以仅利用32位寄存器的低16位,此时只需把要指定的寄存器名开头的字母e去掉即可。
mov指令
mov指令的两个操作数,分别用来指定数据的存储地和读出源。操作数中可以指定寄存器、常数、标签(附加在地址前),以及用方括号([])围起来的这些内容。如果指定了没有用方括号围起来的内容,就表示对该值进行处理;如果指定了用方括号围起来的内容,方括号中的值则会被解释为内存地址,然后就会对该内存地址对应的值进行读写操作。
1 | mov ebp, esp |
mov ebp, esp中,esp寄存器中的值被直接存储在了ebp寄存器中。esp寄存器的值是100时ebp寄存器的值也是100。而在mov eax, dword ptr [ebp+8]的情况下,ebp寄存器的值加8后得到的值会被解释为内存地址。如果ebp寄存器的值是100的话,那么eax寄存器中存储的就是100+ 8=108地址的数据。dword ptr(double word pointer)表示的是从指定内存地址读出4字节的数据。
16位cpu一个字是两个字节,这里是32位所以是双字,4字节
push和pop
栈是存储临时数据的区域,它的特点是通过push指令和pop指令进行数据的存储和读出。往栈中存储数据称为“入栈”,从栈中读出数据称为“出栈”。32位x86系列的CPU中,进行1次push或pop,即可处理32位(4字节)的数据。
push指令和pop指令中只有一个操作数。该操作数表示的是“push的是什么及pop的是什么”,而不需要指定“对哪一个地址编号的内存进行push或pop”。这是因为,对栈进行读写的内存地址是由esp寄存器(栈指针)进行管理的。push指令和pop指令运行后,esp寄存器的值会自动进行更新(push指令是-4, pop命令是+4),因而程序员就没有必要指定内存地址了。
函数调用机制
函数的参数是通过栈来传递,返回值是通过寄存器来返回的
硬件控制方法
应用和硬件无关?
Windows提供了通过应用来间接控制硬件的方法。利用操作系统提供的系统调用功能就可以实现对硬件的控制。在Windows中,系统调用称为API(图11-1)。各API就是应用调用的函数。这些函数的实体被存储在DLL文件中。
例如,假设要在窗口中显示字符串,就可以使用Windows API中的TextOut函数。
支撑硬件输入输出的IN指令和OUT指令
这是Pentium等x86系列CPU用的IN指令和OUT指令的语法。IN指令通过指定端口号的端口输入数据,并将其存储在CPU内部的寄存器中。OUT指令则是把CPU寄存器中存储的数据,输出到指定端口号的端口。
计算机主机中,附带了用来连接显示器及键盘等外围设备的连接器。而各连接器的内部,都连接有用来交换计算机主机同外围设备之间电流特性的IC。这些IC,统称为I/O 控制器。由于电压不同,数字信号及模拟信号的电流特性也不同,计算机主机和外围设备是无法直接连接的。为了解决这个问题,I/O控制器就很有必要了。
I/O是Input/Output的缩写。显示器、键盘等外围设备都有各自专用的I/O控制器。I/O控制器中有用于临时保存输入输出数据的内存。这个内存就是端口。端口(port)的字面意思是“港口”。由于端口就像是在计算机主机和外围设备之间进行货物(数据)装卸的港口,所以因此得名。I/O控制器内部的内存,也称为寄存器。虽然都是寄存器,但它和CPU内部的寄存器在功能上是不同的。CPU内部的寄存器是用来进行数据运算处理的,而I/O寄存器则主要是用来临时存储数据的。
DMA可以实现短时间内传送大量数据
DMA是指在不通过CPU的情况下,外围设备直接和主内存进行数据传送。磁盘等都用到了这个DMA机制。通过利用DMA,大量数据就可以在短时间内转送到主内存。之所以这么快速,是因为CPU作为中介的时间被节省了。
文字及图片的显示机制
用一句话来简单地概括该机制,那就是显示器中显示的信息一直存储在某内存中。该内存称为VRAM(Video RAM)。在程序中,只要往VRAM中写入数据,该数据就会在显示器中显示出来。实现该功能的程序,是由操作系统或BIOS提供,并借助中断来进行处理的。
在现在的计算机中,显卡等专用硬件中一般都配置有与主内存相独立的VRAM和GPU(Graphics Processing Unit,图形处理器,也称为图形芯片)。这是因为,对经常需要描绘图形的Windows来说,数百兆的VRAM是必需的。而为了提升图形的描绘速度,有时还需要专用的图形处理器。但不管怎样,内存VRAM中存储的数据就是显示器上显示的信息,这一机制是不变的。