由8086/8088、x86、Pentium发展到core系列短短40多年间,处理器的时钟频率几乎已接近极限,尽管如此,自从86年Intel推出386至今除了增加一些有关流媒体的指令如mmx/sse之外,其他新增的大多数指令都可以从最初的指令集中组合实现同样的功能,整个编程模型维持了约有20多年。
1. 处理器体系结构
1.1. 处理器简要结构
我们都知道CPU的根本任务就是执行指令,对计算机来说最终都是一串由“0”和“1”组成的序列。CPU从逻辑上可以划分成3个模块,分别是控制单元、运算单元和存储单元,这三部分由CPU内部总线连接起来。如下所示:
![](http://i.imgur.com/szDKt6v.jpg)
1. 控制单元:控制单元是整个CPU的指挥控制中心,由指令寄存器IR(Instruction Register)、指令译码器ID(Instruction Decoder)和操作控制器OC(Operation Controller)等,对协调整个电脑有序工作极为重要。它根据用户预先编好的程序,依次从存储器中取出各条指令,放在指令寄存器IR中,通过指令译码(分析)确定应该进行什么操作,然后通过操作控制器OC,按确定的时序,向相应的部件发出微操作控制信号。操作控制器OC中主要包括节拍脉冲发生器、控制矩阵、时钟脉冲发生器、复位电路和启停电路等控制逻辑。
2. 运算单元:是运算器的核心。可以执行算术运算(包括加减乘数等基本运算及其附加运算)和逻辑运算(包括移位、逻辑测试或两个值比较)。相对控制单元而言,运算器接受控制单元的命令而进行动作,即运算单元所进行的全部操作都是由控制单元发出的控制信号来指挥的,所以它是执行部件。
3. 存储单元:包括CPU片内缓存和寄存器组,是CPU中暂时存放数据的地方,里面保存着那些等待处理的数据,或已经处理过的数据,CPU访问寄存器所用的时间要比访问内存的时间短。采用寄存器,可以减少CPU访问内存的次数,从而提高了CPU的工作速度。但因为受到芯片面积和集成度所限,寄存器组的容量不可能很大。寄存器组可分为专用寄存器和通用寄存器。专用寄存器的作用是固定的,分别寄存相应的数据。而通用寄存器用途广泛并可由程序员规定其用途,通用寄存器的数目因微处理器而异。这个是我们以后要介绍这个重点,这里先提一下。
我们将上图细化一下,可以得出CPU的工作原理概括如下:
总的来说,CPU从内存中一条一条地取出指令和相应的数据,按指令操作码的规定,对数据进行运算处理,直到程序执行完毕为止。
1.2. 寄存器简要结构
以上所列出的一些通用寄存器(注:其中RSP为专用寄存器,之所以把它放在通用寄存器组中只是为了方便记忆整个模型),除了数据位宽度不同之外,并无多大差别:
- RAX(累加器):RAX如果是8/16/32位寻址,则只改变该寄存器的一部分。累加器用于乘法、除法及一些调整指令,同时也可以保存存储单元的偏移地址。
- RBX(基址):用于保存存储单元的偏移地址,同时也能寻址存储器数据,作为偏移地址访问数据时默认使用数据段基址DS作为段前缀。
- RCX(计数):可保存访问存储单元的偏移地址,或在串指令(REP/REPE/REPNE)以及移位、循环和LOOP/LOOPD指令中用作计数器。
- RDX(数据):可使用RDX/EDX/DX/DH/DL寻址,同时作为通用寄存器也用于保存乘法形成的部分结果或者除法之前的部分被除数,也可用于寻址存储单元。
- RBP(基指针):可用RBP/EBP/BP寻址,同时作为偏移地址访问存储单元时默认使用堆栈段基址SS作为段前缀。
- RDI(目的变址):可用RDI/EDI/DI寻址,常用于在串指令中寻址目的数据串。
- RSI(源变址):如RDI一样,RSI也可作为通用寄存器使用,通常为串指令寻址源数据串。
段寄存器CS、DS、ES、SS、FS、GS以及RSP为专用寄存器,以下是这些寄存器的概要描述:
- RSP(堆栈指针):RSP寻址称为堆栈的存储区,通过该指针存取堆栈数据。用作16位寄存器时使用SP,如果是32位则为ESP。
- CS(代码段):代码段寄存器存放程序所使用的代码在存储器中的基地址。 • DS(数据段):存放数据段的基地址。
- ES(附加段):该段寄存器通常在串指令(LODS/STOS/MOVS/INS/OUTS)中使用,主要用于在存储器中将数据进行成块转移。
- SS(堆栈段):为堆栈定义一个存储区域。主要用来存放过程调用所需参数、本地局部变量以及处理器状态等。
- FS与GS:这两个段寄存器是386~Core2中新增的段寄存器,以允许程序访问附加的存储器段。可以将其视为“通用的段寄存器”,通过将段的基地址存入这两个寄存器中可以实现自定义的寻址操作,从而增加了编程的灵活性。
每一个寄存器都有一个”可见”部分和一个”隐藏”部分。(这个隐藏部分有时也指一个”描述符缓存”(descriptor cache)或者”阴影寄存器”(shadow register))。当一个段选择器被加载到段寄存器的可见部分,处理器也会自动把基址,段界限,和段描述符中的访问控制信息加载到段寄存器的隐藏部分。把信息缓存在段寄存器(可见和隐藏部分)允许处理器不经过额外的总线循环(bus cycles)去段描述符总读取基址和界限来转换地址。当描述符表发生了更改,软件有义务重新加载段寄存器。如果不这样做,段寄存器中使用的老段描述符还是会继续使用。
如上图所示,在Pentium4及更高型号处理器中增加了R8~R15这8个64位通用寄存器,这些新增的64位寄存器仍支持按字节、字、双字或四字方式寻址,而不同之处在于只有最右边的数据位可以用来作为单独的一个字节/字等。注意在使用这些新增寄存器的其中一个部分时需要在寄存器末尾添加控制字,例如:
- mov R11D, R8D ;其中字母D用于表示双字访问
- ;也可以将D改为B或者W,B表示字节访问,W表示字访问
- ;如果不加任何控制字则使用整个寄存器
RIP寻址代码段中当前执行指令的下一条指令,当处理器工作在实模式下时使用16位的IP寄存器,当工作于保护模式时则使用32位的EIP。指令指针可由转移指令或调用指令修改。需要注意的是,在64位模式中由于处理器包含40位地址总线,所以总共可以寻址240=1TB的内存。
EFLAGS(program status and control) register主要用于提供程序的状态及进行相应的控制,在64-bit模式下,EFLGAS寄存器被扩展为64位的RFLGAS寄存器,高32位被保留,而低32位则与EFLAGS寄存器相同。
32位的EFLAGS寄存器包含一组状态标志、系统标志以及一个控制标志。在x86处理器初始化之后,EFLAGS寄存器的状态值为0000 0002H。第1、3、5、15以及22到31位均被保留,这个寄存器中的有些标志通过使用特殊的通用指令可以直接被修改,但并没有指令能够检查或者修改整个寄存器。通过使用LAHF/SAHF/PUSHF/POPF/POPFD等指令,可以将EFLAGS寄存器的标志位成组移到程序栈或EAX寄存器,或者从这些设施中将操作后的结果保存到EFLAGS寄存器中。在EFLAGS寄存器的内容被传送到栈或是EAX寄存器后,可以通过位操作指令(BT, BTS, BTR, BTC)检查或修改这些标志位。当调用中断或异常处理程序时,处理器将在程序栈上自动保存EFLAGS的状态值。若在中断或异常处理时发生任务切换,那么EFLAGS寄存器的状态将被保存在TSS中 【the state of the EFLAGS register is saved in the TSS for the task being suspended.】 ,注意是将要被挂起的本次任务的状态。
EFLAGS寄存器的状态标志(0、2、4、6、7以及11位)指示算术指令(如ADD, SUB, MUL以及DIV指令)的结果。位于EFLAGS寄存器的第10位DF标志(DF flag) 控制串指令(MOVS, CMPS, SCAS, LODS以及STOS)。设置DF标志使得串指令自动递减(从高地址向低地址方向处理字符串),清除该标志则使得串指令自动递增。EFLAGS寄存器中的系统标志以及IOPL域(System Flags and IOPL Field) 用于控制操作系统或是执行操作,它们不允许被应用程序所修改。
2. 处理器工作及寻址模式
对于一根实际的、实实在在的、物理的、可看得见、摸得着的内存条而言,处理器把它当做8位一个字节的序列来管理和存取,每一个内存字节都有一个对应的地址,我们叫它物理地址,用地址可以表示的长度叫做寻址空间。而CPU是如何去访问内存单元里的数据的方式就叫做寻址。
2.1. 实模式
8086得CPU在内存寻址方面第一次引入了一个非常重要的概念—-段。在8086之前都是4位机和8位机的天下,那是并没有段的概念。当程序要访问内存时都是要给出内存的实际物理地址,这样在程序源代码中就会出现很多硬编码的物理地址。段寄存器的产生源于Intel 8086 CPU体系结构中数据总线与地址总线的宽度不一致。也就是为了实现16位8086 CPU实现20位地址总线位宽。为了支持分段机制,Intel在8086的CPU里新增了4个寄存器,分别是代码段CS,数据段DS,堆栈段SS和其他ES。这样一来,一个物理地址就由两个部分组成,分别是“段地址”:“段内偏移量”。在实模式中,通常寻址时都是通过段寄存器+通用寄存器,即基址+变址的方式进行寻址。例如,ES=0x1000,DI=0xFFFF,那么这个数据ES:DI在内存里的绝对物理地址就是:
AD(Absolute Address)=(ES)*(0x10)+(DI)=0x1FFFF
就是讲段基地址左移4位然后加上段内偏移量就得到了物理内存里的绝对地址,经过这么一个变换,就可以得到一个20位的地址,8086就可以对20位的1M内存空间进行寻址了。如下:
很明显,这种方式可以寻址的最高地址为0xFFFF:0xFFFF,其地址空间为0x00000~0x10FFEF,因为8086的地址总线是20位,最大只能访问到1MB的物理地址空间,即物理地址空间是0x00000~0xFFFFF。当程序访问0x100000~0x10FFEF这一段地址时,因为其逻辑上是正常的,CPU并不会认为其访问越界而产生异常,但这段地址确实没有实际的物理地址与其对应,怎么办?此时CPU采取的策略是,对于这部分超出1M地址空间的部分,自动将其从物理0地址处开始映射。也就是说,系统计算实际物理地址时是按照对1M求模运算的方式进行的,在有些技术文献里你会看到这种技术被称之为wrap-around。还是通过一幅图来描述一下吧:
根据前面的讲解我们可以发现段基址有个特征,其低4位全为0,也就是说每个段的起始地址一定是16的整数倍,这是分段的一个基本原则。这样每个段的最小长度是16字节,而最大长度只能是64KB。这里我们可以计算一下,1MB的物理地址空间能划分成多少个段。
如果每个段的长度为16字节,这样1MB物理地址空间最多可以划分成64K个段;
如果每个段的长度为64KB,那么1MB的物理地址空间最多能划分成16个段。
8086这种分段基址虽然实现了寻址空间的提升,但是也带来一些问题:
- 同一个物理地址可以有多种表示方法。例如0x01C0:0x0000和0x0000:0x1C00所表示的物理地址都是0x01C00。
- 地址空间缺乏保护机制。对于每一个由段寄存器的内容确定的“基地址”,一个进程总是能够访问从此开始64KB的连续地址空间,而无法加以限制。另一方面,可以用来改变段寄存器内容的指令也不是什么“特权指令”,也就是说,通过改变段寄存器的内容,一个进程可以随心所欲地访问内存中的任何一个单元,而丝毫不受限制。不能对一个进程的内存访问加以限制,也就谈不