一,处理器体系结构1.1. 处理器简要结构

我们都知道CPU的根本任务就是执行指令,对计算机来说最终都是一串由“0”和“1”组成的序列。CPU从逻辑上可以划分成3个模块,分别是控制单元、运算单元和存储单元,这三部分由CPU内部总线连接起来。如下所示:

1. 控制单元:控制单元是整个CPU的指挥控制中心,由指令寄存器IR(Instruction Register)、指令译码器ID(Instruction Decoder)和操作控制器OC(Operation Controller)等,对协调整个电脑有序工作极为重要。它根据用户预先编好的程序,依次从存储器中取出各条指令,放在指令寄存器IR中,通过指令译码(分析)确定应该进行什么操作,然后通过操作控制器OC,按确定的时序,向相应的部件发出微操作控制信号。操作控制器OC中主要包括节拍脉冲发生器、控制矩阵、时钟脉冲发生器、复位电路和启停电路等控制逻辑。

2. 运算单元:是运算器的核心。可以执行算术运算(包括加减乘数等基本运算及其附加运算)和逻辑运算(包括移位、逻辑测试或两个值比较)。相对控制单元而言,运算器接受控制单元的命令而进行动作,即运算单元所进行的全部操作都是由控制单元发出的控制信号来指挥的,所以它是执行部件。

3. 存储单元:包括CPU片内缓存和寄存器组,是CPU中暂时存放数据的地方,里面保存着那些等待处理的数据,或已经处理过的数据,CPU访问寄存器所用的时间要比访问内存的时间短。采用寄存器,可以减少CPU访问内存的次数,从而提高了CPU的工作速度。但因为受到芯片面积和集成度所限,寄存器组的容量不可能很大。寄存器组可分为专用寄存器和通用寄存器。专用寄存器的作用是固定的,分别寄存相应的数据。而通用寄存器用途广泛并可由程序员规定其用途,通用寄存器的数目因微处理器而异。这个是我们以后要介绍的重点,这里先提一下。

我们将上图细化一下,可以得出CPU的工作原理概括如下:

计算机处理器性能参数(从零开始学计算机)(1)

总的来说,CPU从内存中一条一条地取出指令和相应的数据,按指令操作码的规定,对数据进行运算处理,直到程序执行完毕为止。

1.2. 寄存器简要结构

计算机处理器性能参数(从零开始学计算机)(2)

计算机处理器性能参数(从零开始学计算机)(3)

以上所列出的一些通用寄存器(注:其中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中新增的段寄存器,以允许程序访问附加的存储器段。可以将其视为“通用的寄存器”,通过将段的基地址存入这两个寄存器中可以实现自定义的寻址操作,从而增加了编程的灵活性。

更多Linux内核视频教程文档资料免费领取后台私信【内核】自行获取。

计算机处理器性能参数(从零开始学计算机)(4)

内核学习网站:

Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈-学习视频教程-腾讯课堂

每一个寄存器都有一个”可见”部分和一个”隐藏”部分。(这个隐藏部分有时也指一个”描述符缓存”(descRIPtor cache)或者”阴影寄存器”(shadow register))。当一个段选择器被加载到段寄存器的可见部分,处理器也会自动把基址,段界限,和段描述符中的访问控制信息加载到段寄存器的隐藏部分。把信息缓存在段寄存器(可见和隐藏部分)允许处理器不经过额外的总线循环(bus cycles)去段描述符总读取基址和界限来转换地址。当描述符表发生了更改,软件有义务重新加载段寄存器。如果不这样做,段寄存器中使用的老段描述符还是会继续使用的。

计算机处理器性能参数(从零开始学计算机)(5)

如上图所示,在Pentium4及更高型号处理器中增加了R8~R15这8个64位通用寄存器,这些新增的64位寄存器仍支持按字节、字、双字或四字方式寻址,而不同之处在于只有最右边的数据位可以用来作为单独的一个字节/字等。注意在使用这些新增寄存器的其中一个部分时需要在寄存器末尾添加控制字,例如:

计算机处理器性能参数(从零开始学计算机)(6)

RIP寻址代码段中当前执行指令的下一条指令,当处理器工作在实模式下时使用16位的IP寄存器,当工作于保护模式时则使用32位的EIP。指令指针可由转移指令或调用指令修改。需要注意的是,在64位模式中由于处理器包含40位地址总线,所以总共可以寻址240=1TB的内存

计算机处理器性能参数(从零开始学计算机)(7)

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) 用于控制操作系统或是执行操作,它们不允许被应用程序所修改。

二,处理器工作及寻址模式

对于一根实际的、实实在在的、物理的、可看得见、摸得着的内存条而言,处理器把它当做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内存空间进行寻址了。如下:

计算机处理器性能参数(从零开始学计算机)(8)

很明显,这种方式可以寻址的最高地址为0xFFFF:0xFFFF,其地址空间为0x00000~0x10FFEF,因为8086的地址总线是20位,最大只能访问到1MB的物理地址空间,即物理地址空间是0x00000~0xFFFFF。当程序访问0x100000~0x10FFEF这一段地址时,因为其逻辑上是正常的,CPU并不会认为其访问越界而产生异常,但这段地址确实没有实际的物理地址与其对应,怎么办?此时CPU采取的策略是,对于这部分超出1M地址空间的部分,自动将其从物理0地址处开始映射。也就是说,系统计算实际物理地址时是按照对1M求模运算的方式进行的,在有些技术文献里你会看到这种技术被称之为wrap-around。还是通过一幅图来描述一下吧:

计算机处理器性能参数(从零开始学计算机)(9)

根据前面的讲解我们可以发现段基址有个特征,其第4位全为0,也就是说每个段的起始地址一定是16的整数倍,这是分段的一个基本原则。这样每个段的最小长度是16字节,而最大长度只能是64KB。这里我们可以计算一下,1MB的物理地址空间能划分成多少个段。

如果每个段的长度为16字节,这样1MB物理地址空间最多可以划分成64K个段;

如果每个段的长度为64KB,那么1MB的物理地址空间最多能划分成16个段。

8086这种分段基址虽然实现了寻址空间的提升,但是也带来一些问题:

同一个物理地址可以有多种表示方法。例如0x01C0:0x0000和0x0000:0x1C00所表示的物理地址都是0x01C00。

地址空间缺乏保护机制。对于每一个由段寄存器的内容确定的“基地址”,一个进程总是能够访问从此开始64KB的连续地址空间,而无法加以限制。另一方面,可以用来改变段寄存器内容的指令也不是什么“特权指令”,也就是说,通过改变段寄存器的内容,一个进程可以随心所欲地访问内存中的任何一个单元,而丝毫不受限制。不能对一个进程的内存访问加以限制,也就谈不上对其他进程以及系统本身的保护。与此相应,一个CPU如果缺乏对内存访问的限制,或者说保护,就谈不上什么内存管理,也就谈不上是现代意义上的中央处理器。

8086和后来的80186,这种只能访问1MB地址空间的工作模式,我们将其称之为“实模式”。我的理解就是“实际地址模式”,因为通过段基址和段偏移算出来的地址,经过模1MB之后得出来的地址都是实际内存的物理地址。

虽然现在CPU已经发展到了64位的酷睿6代,但是仍然保持着实模式这个工作模式。CPU的实模式是为了与8086处理器兼容而设置的。在实模式下,CPU处理器就相当于一个快速的8086处理器。CPU处理器被复位或加电的时候以实模式启动。这时候处理器中的各寄存器以实时模式的初始化值工作。CPU处理器在实模式下的存储器寻址方式和8086基本一致,由段寄存器的内容乘以16作为基地址,加上段内的偏移地址形成最终的物理地址,这时候它的32位地址线只使用了低20位,即可访问1MB的物理地址空间。在实模式下,CPU处理器不能对内存进行分页机制的管理,所以指令寻址的地址就是内存中实际的物理地址。在现实模式下,所有的段都是可以读、写和执行的。实模式下CPU不支持优先级,所有的指令相当于工作在特权级(即优先级0),所以它可以执行所有特权指令,包括读写控制寄存器CR0等。这实际上使得在现实模式下不太可能设计一个有保护能力的操作系统。实模式下不支持硬件上的多任务切换。实模式下的中断处理方式和8086处理器相同,也用中断向量表来定位中断服务程序地址。中断向量表的结构也和8086处理器一样,每4个字节组成一个中断向量,其中包括两个字节的段地址和两个字节的偏移地址。应用程序可以任意修改中断向量表的内容,使得计算机系统容易受到病毒、木马等的攻击,整个计算机系统的安全性无法得到保证。

2.2. 保护模式(IA-32模式)

由于8086的上述问题,1982年,Intel在80286的CPU里,首次引入的地址保护的概念。也就是说80286的CPU能够对内存及一些其他外围设备做硬件级的保护设置(实质上就是屏蔽一些地址的访问)。自从最初的x86微处理器规格以后,它对程序开发完全向下兼容,80286芯片被制作成启动时继承了以前版本芯片的特性,工作在实模式下,在这种模式下实际上是关闭了新的保护功能特性,因此能使以往的软件继续工作在新的芯片下。后续的x86处理器都是在计算机加电启动时都是工作在实模式下。

也就是说,在保护模式下,程序不能再随意的访问物理内存了,有些内存地址CPU做了明确的保护限制。在这些要求下,286时代的“根据段寄存器确定段基址”方法已经行不通了,我们需要的不仅仅是基址,还需要访问权限等额外的信息,而且我们不想把具体的基址暴露给用户。

2.2.1. 段描述符

为了解决这些问题,intel引入一个中间结构体,段描述符。并增设了两个寄存器:GDTR (global descriptor talbe register)指向全局段描述符数组(表);LDTR (localdescriptor table register)执行局部段描述符数组(表)。而6个段寄存器,CS/DS/SS/ES包括后来的FS/GS,其内容不在用作基址,而是用作索引去段描述符数组中查找对应的段描述符。段描述符占8个字节,其定义以及其中各个标志位的定义如下:

计算机处理器性能参数(从零开始学计算机)(10)

计算机处理器性能参数(从零开始学计算机)(11)

段限制字段(segment limit field) 确定段的大小。处理器将两个限制字段放在一起形成一个20-bit的值。根据G(粒度(granularity))标记位设置的不同,处理器有两种方式解析段限制。

如果G标记位被清除了,段大小范围从1 byte到1 MByte,步长为一个字节。

如果G字段设置了,段大小范围从4 KBytes到4GBytes,步长为4-KByte。

处理器有两种方式使用段限制,取决于段是向上扩展段(expand-upsegment)还是向下扩展段(expand-downsegment)。对于向上扩展段,逻辑地址的偏移量范围从0到段大小限制。大于段限制的偏移量会产生一个通用保护异常(GP,对于除了SS之外的段)或者栈错误异常(stack-faultexception)(SS,对于SS段)。对于向下扩展段,段限制有一个反向函数;偏移量范围从段限制加上1到加上FFFFFFFFH或者FFFFH,取决于B标记位的设置。小于或者等于段限制的偏移量会产生一个通用保护异常或者系统错误异常。在向下扩展段中申请新的内存的时候,会减少段限制字段的值,并且新申请的空间在段地址空间的底部而不是顶部。IA-32架构栈总是向下增长的,这个机制对于扩展段来说非常便利。

基址字段(Base address fields) 在4-GByte线性地址空间中定义了段基址byte0的位置。处理器将三个基址字段加在一起形成一个32-bit的值。段基址需要在16-byte的边界对齐。对于16-byte对齐并不是必须的,但是在16-byte边界对齐的代码和数据的程序有最佳表现。

类型字段(Type field) 指定了段和门的类型并且指定了段的访问方式以及段数据增长方向。对这个字段的解释取决于描述符类型是应用(代码和数据)描述符或是系统描述符。类型字段的编码在代码,数据,和系统描述符中是不同的。

S(描述符类型(descriptor type))标识 决定了这个段描述符是一个系统段(S标记位清0)或是一个代码或者数据段(S标记位设置了)。

DPL(描述符优先级(descriptor privilege level))字段 决定了段位的特权级别。特权级别范围从0到3,其中0是最大特权级别。DPL用来控制对段的访问的。

P(segment-present) flag 决定了是否这个段现在是在内存(set)还是不在(clear)。如果标记位为clear,如果有段选择器指向段描述符加载到段寄存器的时候,处理器将会产生一个段不在异常(segment-not-present exception)(NP)。内存管理软件使用这个标记位来控制当前时间哪些段真正地加载到物理内存。它在分页虚拟内存之外提供了一个额外的控制。

下图展示了当segment-present是clear状态时,段描述符的格式。当这个标记位是clear,操作系统或者执行指令可以直接使用标记为”可用(Avilable)”的位置来存储自己的数据,比如缺失段下落的信息。

D/B(操作的默认大小/栈指针大小和/或上界)标记位 该段描述符是一个可执行代码段,一个向下扩展段,或是栈段不同情况下,表现出不同的方法。(在32-bit代码和数据段中,应该总是被设置成1,而在16-bit代码和数据段中总是0);

可执行代码段(Executable code segment)

此时标记位也称为D标记,它决定了段中有效地址和指针运算符的默认大小。如果标记位是set,认定是一个32-bit地址和32-bit或者8-bit的运算符;如果是clear,认定是一个16-bit的地址和16-bit或者8-bit运算符。

栈段(Stack segment)(通过SS寄存器指向数据段)

此时标记也称为B(big)标记,它决定了在隐式栈操作使用中栈指针的大小(比如pushes,pops,和calls)。如果标记为set,会使用一个存储在32-bit

ESP寄存器的32-bit栈指针;如果标记是clear,会是欧诺个一个存储在16-bit的SP寄存器的16-bit栈指针。如果栈段被设置成向下扩展的数据段,B标记也指定了栈段的上界。

向下扩展数据段(Expand-down data segment)

此时标记也称为B标记,它指定了段的上界。如果标记是set,上界就是FFFFFFFFH(4

GBytes),否则(clear)就是FFFFH(64 KBytes)。

计算机处理器性能参数(从零开始学计算机)(12)

G(粒度(grandularity))标记位 决定段限制的缩放比例。如果粒度标记是clear,段限制是以字节为单元的;如果是set,段限制是以4-KByte为单元的。(这个标记并不影响基址的粒度。)当粒度标记位是set的,检查偏移量有没有超过段限制时,不会测试一个偏移量中最不重要12个位(the twelve least significant bits of an offset are not tested when checking the offset against the segment limit. )。比如,当粒度标记是set的,段限制为0意味着有效偏移量是0到4095。

L(64-bit 代码段)标记 在IA-32e模式,段描述符第二个双字(doubleword)中的bit21决定了一个代码段是否包含了原生64-bit代码。值为1决定了代码段中的指令按照64-bit模式执行。值为0决定了代码段中的执行按照兼容模式执行。如果L-bit是set的,D-bit必须是clear的。当不在IA-32e模式或者非代码段,bit21是保留的,而且必须是0。

可用与保留位(Available and reserved bits) 段描述符的第二个双字的bit20对系统软件是可用的。

通过描述符,我们能够得到如下信息:

段的基址,由B31-B24/B23-B16/B15-B0构成,一共32位,基址可以是4GB空间中任意地址;

段的长度,由L19-L16/L15-L0构成,一共20位。如果G位为0,表示段的长度单位为字节,则段的最大长度是1M,如果G位为1,表示段的长度单位为4kb,则段的最大长度为1M*4K=4G。假定我们把段的基地址设置为0,而将段的长度设置为4G,这样便构成了一个从0地址开始,覆盖整个4G空间的段。访存指令中给出的“逻辑地址”,就是放到地址总线上的“物理地址”,这有别于“段基址加偏移”构成的“层次式”地址(其实应该算作“层次式”地址的特例),所以intel称其为flat地址即平面地址。

段的类型,代码段还是数据段,可读还是可写

描述符表存储在由操作系统维护着的特殊数据结构中,并且由处理器的内存管理硬件来引用。这些特殊结构应该保存在仅由操作系统软件访问的受保护的内存区域中,以防止应用程序修改其中的地址转换信息。同时,为了避免每次访问内存时都通过段寄存器去查表、去读和解码一个段描述符,每次更改段寄存器的内容时,CPU将段寄存器指向的段描述符中的段基址、长度以及访问控制信息等加载到CPU中的“影子结构”中缓存起来。后续对该段的访问控制都通过“影子结构体”来进行。

但是如果可以修改GDTR和LDTR的内容呢?我们不就可以随便指定GDTR到我们自己伪造的段描述数组从而掌控程序吗?为了解决这个问题,intel将访问这两个寄存器的专门指令设为特权指令(LGDT/LLDT,SGDT/SLDT),这些指令只有当CPU处于系统状态(即在操作系统内核中)才能使用,用户空间无法访问寄存器的内容。这样一来,工作1-2天就完成了。

2.2.1.1. 段描述符实例

以下是一个典型的代码段描述符:

计算机处理器性能参数(从零开始学计算机)(13)

基地址域的数据位宽度为16 8 8=32,该域指示存储器段的起始位置。

20位的界限域指示段的最大偏移量,通常与描述符中的特征位(G位,也称为粒度位)一起使用,当G置位时,将在20位的界限域的尾部添加FFFH形成一个32位的值。

AV位指示段是否有效,当AV=1时指示当前存储器段有效,反之则无效,该位由操作系统使用,但Linux系统通常将其所省略。

偏移量的数据位宽度为32时D位被置位,为16时该位被清零。

下图详细解释了访问权限域的各个位置:

计算机处理器性能参数(从零开始学计算机)(14)

,