注意:以下这黑带为上图的文字内容,复制到写字板就可以看到。完整的文档无法附加到文章,就不上传了。
深入x86的内存寻址 本文旨在全面解决寻址方面的疑问,解决一些教材对寻址问题解说不够全面的问题。包含以下主要内容: 4个数据寄存器: EAX,Extended Accumulator Register 累加寄存器; EBX,Extended Base Register 基址寄存器; ECX,Extended Counter Register 计数寄存器; EDX,Extended Data Register 数据寄存器; 2个变址寄存器: ESI,Extended Source Index Register 源索引寄存器; EDI,Extended Destination Index Register 目标索引寄存器; 2个指针寄存器: ESP,Extended Stack Pointer Register 堆栈指针寄存器; EBP,Extended Stack-frame Base Pointer 堆栈基址指针寄存器; 4个段寄存器+段寄存器与内存模型: CS,Code Segment Register 代码段寄存器; DS,Data Segment Register 数据段寄存器; ES,Extra Segment Register 额外数据段寄存器; SS,Stack Segment Register 堆栈寄存器; 1个指令指针寄存器: EIP,Execution Instruction Pointer Register 1个标志寄存器: EFlags,Executioin Status Flags Register 5种寻址方式+内存寻址的组合: Immediate Addressing 立即数寻址 Register Addressing 寄存器寻址 Direct Addressing 直接寻址 Register Indirect Addressing 寄存器间接寻址 I/O Port Addressing I/O 端口寻址 保护模式+内存模型+特权等级等 一、数据寄存器 数据寄存器主要用来保存操作数和运算结果等信息,从而节省读取操作数所需占用总线和访问 存储器的时间。32位CPU有4个32位的通用寄存器EAX、EBX、ECX和EDX。对低16位数据的存取,不会影响高16位的数据。这些低16位寄存 器分别命名为:AX、BX、CX和DX,它和 8086 的寄存器相一致。 4个16位寄存器又可分割成8个独立的8位寄存器(AX:AH-AL、BX:BH-BL、CX:CH-CL、DX:DH-DL),每个寄存器都有自己的名称,可独立存取。AX和AL通常称为累加器(Accumulator),用累加器进行的操作可能需要更少时间。累加器可用于乘、 除、输入/输出等操作,它们的使用频率很高; 寄存器BX称为基地址寄存器(Base Register),它可作为存储器指针来使用,此时将使用堆栈段来寻址数据; CX称为计数寄存器(Count Register),在循环和字符串操作时,要用它来控制循环次数;在位操作 中,当移多位时,要用CL来指明移位的位数;DX称为数据寄存器(Data Register),在进行乘、除运算时,它可作为默认的操作数参与运算,也 可用于存放I/O的端口地址。在16位CPU中,AX、BX、CX和DX不能作为基址和变址寄存器来存放存储单元的地址,但在32位CPU中,其32位寄 存器EAX、EBX、ECX和EDX不仅可传送数据、暂存数据保存算术逻辑运算结果,而且也可作为指针寄存器,所以,这些32位寄存器更具有通用性。 二、变址寄存器 2个32位通用寄存器ESI和EDI为变址寄存器,又称为索引寄存器。其低16位对应先前CPU中的SI和DI,对低16位数据的存取,不影响高16位的数据。(E)SI、(E)DI 统称为变址寄存器(Index Register),它们主要用于存放存储单元在段内的偏移量,使用(E)SI时表示相对堆栈段的偏移。用它们可实现多种存储器操作数的寻址方式,为以不同的地址形式访问存储单元提供方便。变址寄存器不可分割成8位寄存器。作为通用寄存器,也可存储算术逻辑运算的操作数和运算结果。它们可作一般的存储器指针使用。在字符串操作指令的执行过程中, 对它们有特定的要求,而且还具有特殊的功能。 三、指针寄存器 2个 32-bit 通用指针寄存器 EBP 和 ESP 分别用作基地指针和堆栈指针。其低16位对应 8086 中的BP和SP,对低16位数据的存取,不影响高16位的数据。(E)BP、(E)SP 统称为指针寄存器(Pointer Register),主要用于存放堆栈内存储单元的偏移量,用它们可实现多种存储器操作数的寻址方式,为以不同的地址形式访问存储单元提供方便。指针寄存器不可分割成8位寄存器。作为通用寄存器,也可存储算术逻辑运算的操作数和运算结果。它们主要用于访问堆栈内的存储单元,并且规定: BP为基指针(Base Pointer)寄存器,通过它减去一定的偏移值,来访问栈中的元素; SP为堆栈指针(Stack Pointer)寄存器,它始终指向栈顶。 虽然说始终指向栈顶,但并不是指它始终保持不变。因堆栈的增长方向是从高地址向低地址的,所以 PUSH 进栈时,数据就存入栈顶,然后 SP 按数据长度自减;POP 出栈时,反向操作,先按数据长度自增,再读出数据。 四、段寄存器与寻址 Intel 8086/8088 CPU File:KL Intel D8086.jpg 分段内存模型 1978年,Intel 8086 CPU 刚研制出来,这种16位CPU芯片使用40针的DIP封装。地址线只有20条,设计时认为地址线只要20条就够了,这样可以寻址 220=1MB 的内存。对当时,1MB内存就像现在了1TB的概念一样,也就是这个设计导致后来CUP升级过程中,为了兼容而带来软件开发的各种问题,其中就有 A20 Gate。 这时就引入了内存分段的概念了,因为1个16位的寄存器只能访问到216=64KB 的内存,为此就要使用一个额外的偏移值,这样就引入了16位的 CS、DS、ES、SS 段寄存器组合一个偏移值来寻址1MB内存的概念,偏移值也称为偏移地址 Offset Address。使用 CS 来访问代码段,DS 来访问数据段,ES 留给程序来访问额外数据段,SS 来访问堆栈。内存从第一个字节到最后一个字节都有一个唯一的号码连续的地址,称作内存的物理地址 Physical Address、有效地址 Effective Address。 引入分段概念后,CS、DS、ES、SS 就存储各个段的首地址的高16位,也称作段基址,偏移值则作为低16位值相加。偏移值是16-bit数据,最大值也只有65535,因此也可以认为每个段为64KB。用冒号来表示拼接偏移量, SEGMEMT:OFFSET 这样就表示了一个20位的有效地址,也就是1MB的寻址空间。计算方法,段基址左移4位+16位偏移=20位地址,如下: SEGMENT<<4 + OFFSET 注意,这种方式能够表示的最大内存为: FFFF:FFFF=FFFF0+FFFF=10FFEF=1MB+64KB-16Bytes 8086 时代的内存分区 因此,这种方法不只能寻址1M,还多余出近64KB,这部分被称做高端内存区High Memory Area (HMA),也就有了右边所示的内存分段机制模型 Segmentation Memory Model (SMM)。但8086/8088只有20位地址线,如果访问 100000~10FFEF 这部分内存,则必须有第21根地址线。CPU寻址时,系统并不认为其访问越界而产生异常,而是根据20根地址线来进行寻址,因此系统计算有效地址的时候对1M求模的方式进行的,这种技术被称为折回 Wrap-around。由于开来,对不同的内存区就形成了不同称谓,0~640KB 这部分内存就称为传统内存 Conventional Memory,用一个16-bit的寄存器就可以完成寻址;而640KB~1MB这部分内存就称为上位内存 Upper Memory Area (UMA);而高于HMA的部分就总称为扩展内存 Extended Memory。在较新版本的DOS系统是可以通过软件控制,将HMA当作传统内存使用的,以扩大程序的内存空间。 ntel 80286 CPU 1982年,Intel 80286 CPU被研制出来,共有24条地址线,寻址16MB。这种CPU引入了虚拟内存及内存保护技术,并对内存的分段概念进行了修改,而 8086 的运行方式被称为实地址模式 Real Address Mode。80286 兼容了实模式,同时引入了一种称为保护模式 Protected Mode,全称 Protected Virtual Address Mode。保护模式下,CPU运行情况要复杂得多,它装入段寄存器的不再是段值,而是称为“选择子”Selector 的某个值。我觉得“选择子”叫法挺怪异的,更情愿称之为“选择器”。保护模式下的内存寻址比较复杂,在后面的保护模式进行讲述。保持兼容本是好事,这样8086的程序可以直接在80286上运行。为了兼容 8086 内存分段机制的实模式,80286 可以进入实模式运行。问题是 80286 拥有24根地址线而不是20根,因此 80286 的指针就可以指向 100000 至 10FFEF 处的内存地址,这就是将近 64KB 的高位内存 HMA。也可以认为这是芯片的一个BUG:如果程序访问HMA内存,因为有24条地址线,系统将真实地访问这块内存,而不是像8086一样进行折回访问1MB的区域。 话说当其时,IBM作为全球PC生产厂商的龙头,它的PC市场份额占据了全球50%以上。作为利益的受损方,IBM想到一个解决方法。使用键盘控制器 8042 芯片来处理A20 Gate,利用控制器上剩余的一些输出线来管理第 A20 根地址线,即第21根地址线,这个功能就被称为A20 Gate:开启A20 Gate,A20 根地址线有效,则程序可以访问HMA内存区;关闭A20Gate,则程序访问HMA时,系统遵循8086的折回方式访问1MB寻址空间。IBM PC兼容机默认的 A20 Gate 是被禁止的。在当时似乎只能这样的方法来解决A20 Gate,这就是硬件 bug 的 Hack 行为,毕竟 A20 Gate 和键盘操作是没没有任何关系的。新型PC上引入了 BIOS 芯片,这样 A20 Gate 功能就集成到了 BIOS上面。因此,在保护模式下,尽管系统有24条地址线,但开启 A20 Gate 就意味着损失了一条,就只能访问内存的奇数段(2N+1)MB,寻址空间也只有16MB的一半。在BIOS中,这个功能就是Fast A20中断,INT 15h,AX=2401h。 Intel 80386 CPU 1985年10月,Intel 80386 32-bit CPU正式上市,全功能的386芯片到1986年上市。它更强大了,在硬件上有了一定的虚拟能力,此时兼容 8086 实模式的运行状态就是通过硬件虚拟技术实现的,因此又称为 Virtual 8086 Mode、Virtual real mode, V86-mode 或 VM86 等等。还引进了两个通用段寄存器 FS 和 GS,并没有限定功能,在不同的操作系统上具有功能差异。到了2003年,x86-64 CPU研发出来后,内存分段的概念已经大大削弱了。 再谈堆栈 堆栈是一种数据结构定义,在CPU内部,它就是以硬件实现的数据结构,并在 SS 指定堆栈的起始地址,ESP 指向栈顶地址。使用 PUSH 和 POP 指令来向堆栈存入、取出数据。PUSH 存入数据时,会修改 ESP,使其向内存的低位地址移动,长度为数据需要的字节数。POP 则从栈顶取出数据,并使用 ESP 向内存的高位地址还原。 堆栈还被用来实现函数的上下文保存与还原。在 CALL 指令执行前,通过 PUSH 指令来将数据或特定的寄存器入栈,这样既可以向被调用的代码传入参数,又可以保护寄存器的数据。CALL 指令执行时,它会保存当前代码执行的地址,即 EIP 的数据会被 PUSH 到堆栈,待调用过程通过 RET 返回时,EIP 就会被还原回来。跳转指令和 CALL、RET 指令一样会转移控制到另外一个代码区,只是跳转指令不会主动去处理 EIP。 五、指令指针寄存器 指令指针 EIP 用来存放下次将要执行的指令在代码段的偏移量,即 CS:EIP 指向下一条指令。它也常称为程序计数器 Program Counter (PC),JMP、CALL、RET 指令就是通过修改它来实现转移控制的。在具有预取指令功能的系统中,下次要执行的指令通常已被预取到指令队列中,除非发生转移情况。所以,在理解它们的功能时,不考虑存在指令队列的情况。在实方式下,由于每个段的最大范围为64K,所以,EIP中的高16位肯定都为0,此时,相当于只用其低16位的IP来反映程序中指令的执行次序。32位CPU把指令指针扩展到32位,并记作 EIP,EIP 的低16位与先前 8086 CPU 中的 IP 相同。 六、标志寄存器 运算结果标志位 1、进位标志CF(Carry Flag) 进位标志CF主要用来反映运算是否产生进位或借位。如果运算结果的最高位产生了一个进位或借位,那么,其值为1,否则其值为0。使用该标志位的情况有:多字(字节)数的加减运算,无符号数的大小比较运算,移位操作,字(字节)之间移位,专门改变CF值的指令 Set Carry Flag (STC)、Clear Carry Flag (CLC)。 2、奇偶标志PF(Parity Flag) 奇偶标志PF用于反映运算结果中”1″的个数的奇偶性。如果”1″的个数为偶数,则PF的值为1,否则其值为0。 利用PF可进行奇偶校验检查,或产生奇偶校验位。在数据传送过程中,为了提供传送的可靠性,如果采用奇偶校验的方法,就可使用该标志位。 3、辅助进位标志AF(Auxiliary Carry Flag) 在发生下列情况时,辅助进位标志AF的值被置为1,否则其值为0: (1)、在字操作时,发生低字节向高字节进位或借位时; (2)、在字节操作时,发生低4位向高4位进位或借位时。 对以上6个运算结果标志位,在一般编程情况下,标志位CF、ZF、SF和OF的使用频率较高,而标志位PF和AF的使用频率较低。 4、零标志ZF(Zero Flag) 零标志ZF用来反映运算结果是否为0。如果运算结果为0,则其值为1,否则其值为0。在判断运算结果是否为0时,可使用此标志位。 5、符号标志SF(Sign Flag) 符号标志SF用来反映运算结果的符号位,它与运算结果的最高位相同。在微机系统中,有符号数采用补码表示法,所以,SF也就反映运算结果的正负号。运算结果为正数时,SF的值为0,否则其值为1。 6、溢出标志OF(Overflow Flag) 溢出标志OF用于反映有符号数加减运算所得结果是否溢出。如果运算结果超过当前运算位数所能表示的范围,则称为溢出,OF的值被置为1,否则,OF的值被清为0。溢出和进位是两个不同含义的概念,前者意味结果是不准确的,不要混淆。 通过判断标志位可以确定运算结果,如指令 CMP ax,bx,逻辑含义是比较ax和bx中的值,不影响上目标操作数 ax,在此基础上就相当 SUB ax, bx: ZF=1,说明(ax)=(bx),JZ/JE 指令的判断条件。 ZF=0,说明(ax)≠(bx),JNZ/JNE 指令的判断条件。 CF=1,说明(ax)<(bx),JC/JB/JNAE 指令的判断条件。 CF=0,说明(ax)≥(bx),JNC/JNB/JAE 指令的判断条件。 CF=0 并且 ZF=0,说明(ax)>(bx),JNLE/JG 指令判断条件。 CF=1 或 ZF=1,说明(ax)≤(bx),JBE/JNA 指令判断条件。 SF=OF,说明(ax)≥(bx),JNL/JGE 指令判断条件。 TEST 指令则常用来检测内容而不是运算内容的,如 TEST eax, eax,它不会存储结果,因此 eax 内容不变,在此基础上 TEST 就相当 AND 运算,经常用于测试寄存器 ZF 是否为0,结合 JZ 指令来实现跳转。 以上指令的缩写 J 表示 Jump 跳转指令,E 表示 Equal 相等;B 表示 Below,L 表示 Less,都是小于的意义;A 表示 Above,G 表示 Greater,都是大于的意义;N 表示 Not 否定。这样就很容易还原指令的全称,如 JNLE 表示 Jump if Not Less Or Equal 即不小于或等于时跳转。C、Z 则表示对应的 ZF、CF 标志位。 状态控制标志位 状态控制标志位是用来控制CPU操作的,它们要通过专门的指令才能使之发生改变。 1、追踪标志TF(Trap Flag) 当追踪标志TF被置为1时,CPU进入单步执行方式,即每执行一条指令,产生一个单步中断请求。这种方式主要用于程序的调试。指令系统中没有专门的指令来改变标志位TF的值,但程序员可用其它办法来改变其值。 2、中断允许标志IF(Interrupt-enable Flag) 中断允许标志IF是用来决定CPU是否响应CPU外部的可屏蔽中断发出的中断请求。但不管该标志为何值,CPU都必须响应CPU外部的不可屏蔽中断所发出的中断请求,以及CPU内部产生的中断请求。具体规定如下: (1)、当IF=1时,CPU可以响应CPU外部的可屏蔽中断发出的中断请求; (2)、当IF=0时,CPU不响应CPU外部的可屏蔽中断发出的中断请求。 CPU的指令系统中也有专门的 CLI、STI 指令来改变标志位IF的值。 3、方向标志DF(Direction Flag) 方向标志DF用来决定串操作指令执行时有关指针寄存器指向地址的自增或自减,对应 DF=0、DF=1。指令系统提供了专门的指令来置DF的值为0、1,对应指令为 Clear Direction Flag (CLD) 和 Set Direction Flag (STD)。 32位标志寄存器增加的标志位 1、I/O特权标志IOPL(I/O Privilege Level) I/O特权标志用两位二进制位来表示,也称为I/O特权级字段。该字段指定了要求执行I/O指令的特权级。如果当前的特权级别在数值上小于等于IOPL的值,那么,该I/O指令可执行,否则将发生一个保护异常。 2、嵌套任务标志NT(Nested Task) 嵌套任务标志NT用来控制中断返回指令IRET的执行。具体规定如下: (1)、当NT=0,用堆栈中保存的值恢复EFLAGS、CS和EIP,执行常规的中断返回操作; (2)、当NT=1,通过任务转换实现中断返回。 3、重启动标志RF(Restart Flag) 重启动标志RF用来控制是否接受调试故障。规定:RF=0时,表示”接受”调试故障,否则拒绝之。在成功执行完一条指令后,处理机把RF置为0,当接受到一个非调试故障时,处理机就把它置为1。 4、虚拟8086方式标志VM(Virtual 8086 Mode) 如果该标志的值为1,则表示处理机处于虚拟的8086方式下的工作状态,否则,处理机处于一般保护方式下的工作状态。 七、寻址 寻址是指CPU寻找数据的过程,怎样定位到数据,并读取到数据的过程。要完整理解寻址,还需要从CPU的定义的内存模型着手,典型的内存模型就是 8086 的内存分段模型,它一直沿用到现代的CPU上,如引入分页内存模型的 80386 依然将分段内存作为基础。只在理解不同的内存模型,才能全面的理解和掌握指令执行时的寻址问题。这也就是为什么将保护模式和相关的一些CPU特性组织到本文来讲述,因为掌握技术的最好方法,就是从这种技术的源头出发。只有站在一个广阔视角看问题,才理解得更透彻。关于寻址,先来了解处理器是如何计算有效地址的,下面这个有效地址算式图基本说明了除立即数寻址和寄存器寻址以外的寻址方式: 立即数寻址 这是最简单了一种寻址方式,或者说这是一种不用寻找的寻址方式,因为数据就是指令当中,如: MOV ax, 0 寄存器寻址 数据存放在寄存器时,CPU只需要访问寄存器就可取得数据,这就是寄存器寻址方式: MOV ax, bx 直接寻址 数据存放在内存某处时,CPU就需要访问指定的内存地址来取得数据,这就是直接寻址方式,如下示例 [0] 就指定了数据所在内存地址: MOV ax, [0] 寄存器间接寻址 数据存放在内存某处,而其地址存入寄存器时,CPU只需要访问寄存器取得地址,然后再取得数据,这就是寄存器间接寻址方式,它比寄存器寻址多了取数据地址这个过程: MOV ax, [bx] 内存寻址的组合 通过组合和引入额外的数据,可以重新组合内存的寻址形成特别用途的数据寻址方式。通过使用位移量加基址或变址寄存器(BP、BX、DI或SI)的内容寻址存储器段中的数据,这样的寻址方式称为寄存器相对寻址。示例如下: MOV AX, [DI+2H] MOV ARRAY[SI], BL MOV LIST[SI+2H], CL MOV DI, [EAX+2H] 又如比例变址寻址,这种方式使用一个总数因子用来访问内存的对齐地址,这种方式就像使用高级语言的数组下标来获取数据一样。示例如下,假设 ARRAY 是定义好的变量,其中的数字4就是比例因子: MOV EAX, [EBX+4*ECX] MOV [EAX+4*EDI+2], CX MOV AL, [EBP+4*EDI+2] MOV EAX, ARRAY[4*ECX] 如果使用用一个基址寄存器(BP或BX)和一个变址寄存器(DI或SI)间接寻址存储器,此时数据的起始地址通常保存在基址寄存器,而变址寄存器保存数组元素的相对位置。80386+ 这种寻址方式允许除了ESP以外的任意两个32位扩展寄存器组合使用。注意,如果使用了 EBP 寄存器来寻址,则数据在堆栈段中而不在数据段中。这种差异由 CUP 使用段寄存器的隐含约定引起,指令的操作数默认使用数据段来寻址,但是显式使用 (E)BP 后将引起对其隐含约定的 SS 指定的堆栈段来寻址,后面在保护模式中有讲及。这种寻址方式就称作基址加变址寻址,示例如下,注意第2、4条指令使用的数据、目标来自堆栈段。注意按 Intel 80386 编程手册要求这种寻址方式以 [BX] [DI] 形式出现,如前面展示的有效地址算式图,而不是 [BX+DI] 形式: MOV CX, [BX+DI] // Intel ASM: MOV CX,[BX][DI] MOV CH, [BP+SI] MOV [BX+SI], SP MOV [BP+DI], AH MOV CL, [EDX+EDI] MOV [EAX+EBX], ECX MOV [RSI+RBX], RAX // X64 instructions 在基址加变址寻址方式的基础上添加一个偏移量就可以形成另一种常用来寻址存储器的二维数组的方式,称作相对基址加变址寻址。此时,基址寄存器就相当数组起点地址,变址寄存器就相当数据的第一维度,偏移量则为数组的第二维度。示例如下: MOV DH, [BX+DI+20H] MOV AX, FILE[BX+DI] MOV LIST[BP+DI], CL MOV LIST[BP+SI+4],DH MOV EAX, FILE[EBX+ECX+2] 前面提到,使用 (E)BP 会隐含地使用 SS 作为寻址的段,这样的也就可以名副其实地称为堆栈存储器寻址,按这样的分类思想,PUSH、POP 这样的指令也可以归纳为堆栈存储器寻址的行列,因此它们隐含地使用堆栈段来进行数据寻址。而对执行代码的指令如 CALL、JMP、RET,因其使用的是代码段寻址,又可以称之为程序存储器寻址,它又分直接寻址、相对寻址和间接寻址形式。这类指令和其它操作数,即将要被执行的指令地址存储在一起时,就是直接程序存储器寻址。还有相对程序存储器寻址,这里相对(relative)意味着相对于指令指针(IP)。例如,如果JMP指令跳过后面两个存储器字节,则相对于指令指针的地址是2,将2与指令指针相加,就得到程序下一条指令的地址,这个2就是相对 IP 的偏移量。注意,JMP指令的格式是1字节操作码加1个或2个字节的位移量,位移量将与指令指针相加。1个字节位移量用于短(short)转移;2个字节位移量用于近(near)转移和调用。这两种类型都为段内转移(intrasegment jump),段内转移是转移到当前代码段内的任何位置。在80386及更高型号的微处理器中,位移量还可以是32位数,允许用相对寻址转移到4GB代码段内的任何位置。最后,程序的跳转地址还可以通过其它寄存器来给出,这样就有了间接程序存储器寻址。对于CALL和JMP指令,可以用任何16位寄存器(AX、BX、CX、DX、SP、BP、DI或SI),任何相对寄存器([BP]、[BX]、[DI]或[SI]),或任何带有位移量的相对寄存器。80386和更高型号的微处理器中,可以用扩展寄存器存放相对JMP 或CALL的地址或间接地址。 位寻址是一种常用来访问比特位的寻址,就读写比特位的含意。有单片机的基础,就很容易理解,比特位也可以像寄存器一样被访问,只是它只有一个bit位。例如 8051 通过 P1.0 就可以访问一个输出端的第一个比特位,这时将 P1.0 理解为一个寄存器是正确的思维!而 x86 CPU 中最常见的位寻址就是 JECXZ、JA、JB、JG、JL、JE、JZ、JS、JC、JO、JP 等等系列跳转指令,它位通过访问标志寄存器的比特位来进行有条件跳转。关于位寻址,可以参考芯片级 Datasheet 手册,通过阅读芯片手册,你会形成一个从顶往下的观点。即操作系统就是对芯片的编程,应用程序是对操作系统的编程,而脚本则是对应用程序的编程!因此,掌握一种知识,从它的源头着手一直是我最为认可的方法。 总结一下,这里介绍了这么多的寻址方式,总的来说,基本的还只有那么几个。衍生出和各种寻址方式,只因和特定的寄存器、立即数有关系,才形成了各式各样的特例。 I/O寻址 在计算机内部,通过数据总线等等硬件连接,可以给整个系统添加各种各样的外设。主机与外设的通信或数据的互通就是I/O寻址,80386 允许两种方式: 独立于内存的 I/O 寻址空间 映射到内存 I/O 寻址 通过使用不同的指令形式就可以确立是何种 I/O 寻址方式。独立的 I/O 寻址空间共有216=64KB,如果按 8-bit 、16-bit、 32-bit 划分成端口则对应为 64K、32K、 16K 个端口数量,端口的起始地址就作为端口的编号使用。端口 Port 是 I/O 寻址空间的组织单元,要求地址是连续的。以端口组织 I/O 寻址的原因,是因为 I/O 指令使用 AL、AX 或 EAX 来进行 I/O 操作,因为这几个通用寄存器就是 8-bit 、16-bit、 32-bit 的。端口的 bit 位数决定了端口可以接收和获取的数据 bit 数量。使用 IN、OUT 指令来获取、输出数据,如果是一连串的数据则使用 INS、OUTS 指令来自执行。像后者这些传输多个数据的指令又称为 串 I/O 指令 String I/O Instruction 或者 块 I/O 指令 Block I/O Instruction。给指令指定端口时,可以通过立即数给出,也可以使用 DX 作为间接寄存器的 I/O 寻址。对于块 I/O 指令只能通过 DX 寄存器来指定端口,至于端口的 bit 长度则是通过操作数来确认的。这些块操作指令包括 OUTS、OUTSB、OUTSW、OUTSD、INS、INSB、INSW、INSD,后缀 B 表示 Byte、W 表示 Word、D 表示 Double Word。因为这些指令只能接受 8-bit 的立即数,所以通过立即数来指定端口时,只能访问 8-bit 端口的前 28=256个,或 16-bit 端口的前 28/2=128个,又或者 32-bit 端口的前 28/4=64个。为了使用 I/O 指令能在单个总线周期完成数据的传输需要按端口的 bit 长度来对齐数据在内存的地址,如 32-bit 的端口传输的数据要求对齐 4Byte 的边界,即内存地址的最低两位保持为0。 块操作指令的原理是 CPU 自动按标志寄存器的 Direction Flag (DF) 位的指示自动增减指定数据地址的 ESI 或 EDI 寄存器值,DF=0 自动按操作端口的字节数增值,DF=1 则自动端口的字节数减值。因此可以通过给 INS 或 OUTS 前缀 REP 指令来重复执行指令以处理块数据,或都使用 LOOP 指令来循环执行代码块来处理块数据,这两条指令都通过 (E)CX 来指定重复次数。对于 LOOP 指令则还要指定一个偏移值来执行程序跳转。另外,对于 INS 指令,它只能使用 ES 段作为目标地址,输出类型的块 I/O 指令和大多数指令一样默认使用 DS 段作为数据源。 如果设备拥有自己的内部寄存器,那么系统就可以将其映射到系统内存上。这样就可以使用映射内存的 I/O 寻址,它的便利之处就是通过映射的内存,所有指令对映射区的内存操作都直接反映到外设的端口上,因此使用起来更灵活。外设的端口映射是在计算机启动时完成的,BIOS 在自检时会对所有安装的设置进行配置,端口分配就是其中一项主要的工作。操作系统启动后,如 Windows 可以在设备管理器中打开设备属性页的资源选项卡来检视设备的端口号。例如本系统的蜂呜器 System Speaker 的端口为 0061,System Time 的端口号为 0040 - 0043。这里有个有趣的现象,通常 BIOS 的编程者是来自生成 PC 的厂商,所以作为内部人员,就知道如何访问外设,因此会直接给集成的外设固定的端口。久而久之,形成了一种习惯,一系列常见的设备就按约定给它分配端口了。但是实际使用中,这种固定分配端口的做是很不方便的,当系统更改硬件时就会出现外设的配置问题,因此旧有的设备就有很多跳线开关 Jumper 用来更改配置。后来,Intel 联合几家公司推出了即插即用 Plug-and-Play (PnP) 的概念,PnP 的作用是自动配置低层计算机中的板卡和其他设备,然后告诉对应设备都做了什么。PnP的任务是配对物理设备和软件设备驱动程序,建立通信信道。简而言之,就是给设备配置 I/O地址、IRQ、DMA通道和映射内存地址。PnP 就是一种硬件规范,符合这一规范的设备就可以实现自动配置。。 最后,来讲一下 80386 的保护模式对 I/O 寻址的影响部分。在标志寄存器中有部分为 I/O privilege level (IOPL),这是特权指令约束标记,只有满足条件的指令才许可执行: CPL ≤ IOPL CPL 可以从段描述符中得到,它指示了当前进程所拥有的特权。只有当前进程特权不比约束特权小时,以下 I/O 指令才会被执行: IN ── Input INS ── Input String OUT ── Output OUTS ── Output String CLI ── Clear Interrupt-Enable Flag STI ── Set Interrupt-Enable 每个进程都会带有自身的一套标志寄存器副本,所以不是所有进程都可以使用这些特权指令的。越权执行指令,系统将发出通用的保护异常。一般来讲,设备驱动是在 Ring 0 运行的,拥有这些特权指令的使用权。 IBM PC/XT系统中8253的计数器是一种 Programmable Interval Timer (PIT) 芯片,改进型号为8254。它使用5v电源,有3个16位的独立计数器,有6种工作方式,已经作为PC标准的外设集成在系统中,即称为系统定时器 System Timer。它支持二进制或十进制 BCD 计数,计数频率为2MHz,改进型可高达10MHz,所有引脚电平和晶体管-晶体管集成电路 Transistor-Transistor Logic (TTL) 兼容。 以下是扬声器接口电路的驱动发声程序,42H为扬声器端口地址,43H为控制寄存器地址,阅读程序,回答以下问题 DEEP PROC MOV AL,0B6H ① OUT 61H,AL ⑩ OUT 43H,AL ② SUB CX,CX MOV AX,0533H③ GO:LOOP GO OUT 42H,AL ④ DEC BL MOV AL,AH ⑤ JNE GO OUT 42H,AL ⑥ MOV AL,AH ⑾ IN AL,61H ⑦ OUT 61H,AL⑿ MOV AH,AL ⑧ RET OR AL,03H ⑨ BEEF ENDP 1,指令①--②的作用是什么 2,指令③--⑥的作用是什么 3,指令⑦-- ⑩的作用是什么 4,指令 ⑾--⑿的作用是什么 参考答案:一,选择题:CDDCCBADBDDC 二,简答:DCDAA 三,综合题:1,指令①--②的作用是要8253/54工作在方式3,计数器2输出方波 ,控制字 为0B6H,写到43H端口 2,指令③--⑥的作用是将发声频率送入端口42h,计数器2 3,指令⑦-- ⑩的作用是读61h端口,然后将第0、1位置1,写回61h,使PB0、PB1 为高,使能扬声器 4,指令 ⑾--⑿的作用是恢复61h初值,停止发声 八、保护模式与寻址 80286保护模式 正如前面提到,保护模式全称就是保护虚拟寻址模式。CPU一通电工作就处于实模式下,通过设置几个描述符表,就可以通过控制寄存器的PE Protection Enable 位 RC0 来激活保护模式。80286 的保护模式主要实现了内存分段的保护机制,防止进程间互访问对方的内存带来冲突,这种机制在 80286+ 的CPU都有效。它给CPU带来了24-bit的内存寻址,可以访问16MB内存空间。但这个空间不是通过移位产生的,而是通过增强的分段内存管理机制来实现的。此时,16位的段寄存器不再直接存放基址,而是存放一个段描述符表的索引值 ,段描述符又是一种在CPU内部实现的数据结构,它包含了一个24-bit的段基址。这样,另外再加一个16-bit的偏移值就实现了16MB的内存寻址。2086 的保护模式也是最不受欢迎的保护模式,并没有得到广泛应用。有几个原因,首先它不支持不重置CPU的情况下从保护模式切换回到实模式,这使用 BIOS 和 DOS 调用无法正常使用;另外,让人不可接受的是它通过段寄存器访问的段只允许16-bit长度的段,这问意味着每个段最大才为64KB,同时只能访问到 4*216=256KB内存。这是因为在保护模式下修改段寄存器会引起CPU从内存某处加载一个6-Byte的段描述符数据,这会消耗几十个时钟周期,这使用得 80286 比 8086 运行还要慢!因此,运算一个超过128KB、处于相邻段的数据结构这样的策略也变得不切实际。因此,我们需要的是 80386+ 的保护模式! 80386+保护模式 80386+,即80386或以上的CPU,这种CPU的保护模式带来了32-bit寻址空间、32-bit段地址偏移、RM-PM两种模式自由切换、强劲的内存分页机制、虚拟内存、虚拟8086模式及多任务特性,因此又称为 IA32 处理器架构,成为 x86 处理器的一支强大的分支,与 x86-64 即 AMD64 相呼应。AMD64 与 x86 属同系,可以兼容 x86 架构,而 Intel 的 IA 64 架构和 AMD64 架构不是兼容架构。80386 还引入了两个通用的数据段寄存器 FS 和 GS,有人把它们称作 File Segment Register 和 Graphics Segment Register, 只是这两个寄存器并没有指定用途,在不同的系统可以用于不同的目的。 段描述符概念与符表 通过在CPU的地址译码电路添加一层分页单元,使得可访问的内存空间可以超过4GB。尽管如此,80386+ 继续保持了 80286的内存分段机制。地址偏移也使用32-bit而不是16-bit值,段描述符中的段基址也使用 32-bit 而不是 80286 实模式使用的 24-bit 段基址,而段寄存器则完全可以不理它。这里正式引入了段描述符 Segment Descriptor 的概念,它就是用来解析内存段的一个数据结构。试想一下,在 8086 的内存分段机制,每个段基址只含有内存物理地址的高16-bit,通过16-bit的偏移形成一个物理地址,这个约束使得每个段最大也只能是64KB。内存分段模型提供了一个基础,这里的一系列数据其实就是一个隐含的段描述符! 保护模式引入的一个重要特性就是内存的保护机制,又为段模式提供了保护机制。这要求段描述符规定对自身的访问权限。因而段描述符的数据结构将不可避免地包含 Base Address, Limit, Access 三个方面的内容,它们组成了一个 64-bit 的数据结构。进而通过段描述符来寻址一个段,就要求使用 64-bit 的段寄存器来装入这个段描述符。但Intel为了保持向后兼容,将段寄存器仍然规定为16-bit。尽管事实上,每个段寄存器有隐藏不可见的部分,有足够长的 64-bit,但对于程序员来说,段寄存器就是16-bit的,隐藏部分只 CPU 才能从内部访问。 如何使用 64-bit 的段描述符就存在问题了。从寄存器的宽度来讲,32-bit 的段寄存器放不下 64-bit 的段描述符数据结构。解决方法就是使用数组,用数据来管理段描述符,这个全局的数组就是全局描述符表 Global Descriptor Table (GDT),每个元素就是 64-bit 的段描述符。前面提到的,在段寄存器中的索引值就是选择器 Segment Selector 就是用来找到 GDT 中对应的段描述符的。选择器 Selector 是一个 16-bit 的数据,它高 13-bit 是一个索引值,用于定位 GDT 或 LDT 上的段描述符,也有教材怪怪地称之为“选择子”。因为段选择器是指向 GDT 中的某个段描述符,因此得名,如果一个选择器指向的是 GDT 中不同类型的描述符如 LDT 的描述符,那么此选择器就称为 LDT 选择器了。和 GDT 一样,局部描述符表 Local Descriptor Table (LDT) 就是用来管理局部段描述符的数组。不同的是LDT 属于进程的,只有当前引用它的进程可以使用。同时,作为进程级的段描述符表,它自然而然就可以在系统中存在多个不同的副本,在每个进程最多只能有一个 LDT。它是可选的,使用它需要在程序复杂度及便利性之间平衡。 正如前面所述,GDT 不仅存放段描述符而已,还有用于多任务的 Task State Segment (TSS) 的描述符、用于进程的 LDT 的描述符,即描述局部符表的描述符,有点绕,其实它就是用来定位系统存在的 LDT 的。也就是说,段描述符之所以称作段描述符,是因为它对内存的段起到描述的作用。还有 Call Gate 的描述符。最后一个,Call Gates 表示调用门,在 x86 的特权级间转移控制的重要调用,尽管现代操作系统很少使用。这些描述符都是 64-bit 的数据,同时 GDT 的第一个元素是置 0 的。 为了使用 GDT,Intel设计了一个寄存器 GDTR 用来存放 GDT 的入口地址,程序确定 GDT 数据后,就可以使用 LGDT 指令来装入其所在内存的位置。这样,CPU就根据此寄存器中的地址来访问 GDT。尽管可以通过 GDT 来获取 LDT 的地址,IA-32 还是为 LDT 提供了一个入口地址寄存器 LDTR。因为在任何时刻只能有一个任务在运行,所以LDT寄存器全局也只需要有一个。通过 LLDT 将其 LDT 的地址装入此寄存器。不同的是,LGDT 指令的操作数是一个 32-bit 的内存地址,这个内存地址就是GDT的入口地址,而LLDT指令的操作数是一个的选择器,利用这个选择子就就可以在 GDT 找到 LDT 的描述符。这里涉及的两个指令都是 Ring 0 权限等级的,后面作解析。 其实段描述符就是一个 64-bit 的数据结构,结构如下: 32-bit的段基址 Segment Base 20-bit的段极值 Segment Limit 访问权限字节 Access Rights Byte 控制位 Control Bits B Bits 80286 80386 B 0 00..07, 0..7 limit bits 0..15 of limit 0 1 08..15, 0..7 1 2 16..23, 0..7 base address bits 0..23 of base address 2 3 24..31, 0..7 3 4 32..39, 0..7 4 5 40..47, 0..7 attribute flags #1 5 6 48..51, 0..3 unused bits 16..19 of limit 6 52..55, 4..7 attribute flags #2 7 56..63, 0..7 bits 24..31 of base address 7 Attribute flags #2 52 4 unused, available for operating system 53 5 reserved, should be zero 54 6 default flag / D-bit 55 7 granularity flag / G-bit 段极值与粒度控制位 G-bit 配合,当 G-bit 为 0,表示段极值可以为 1-Byte 至 220-Byte 任意字节长度的值,即粒度为1-Byte,即此时每段最大可以为220=1MB;当 G-bit 为 1 时,粒度为 212-Byte,即段极植最大可以为220*212=2GB!当每页机制关闭时,base address + limit * G 得到的线性地址就是物理地址,否就将得到的线性地址作为分页机制的输入参数。此时将起用虚拟内存地址,段寄存器基址、偏移地址、还有CPU内部的分段单元推断得到的32-bit线性地址都是虚拟地址 Virtual Address,或称作逻辑地址 Logical Address!通过 CR0 寄存器的 PG 位来关闭分页机制,80386 就像 80286 一样使用内存分段模型,不通过分页,直接通过段描述符提供的基址和偏移值组成的物理地址访问内存。 分页机制与段选择器 从逻辑地址到物理地址,中间需要做的工作是复杂的,其中重要的一步就是从逻辑地址到线性地址 Linear Address 的转换。这个过程也称之为段转换 Segment Translation,从线性地址得到物理地址的过程也称之为页转换 Page Translation,后者是可以选的,可以通过 CR0 的 PG 位关闭,由操作系统来实现。线性地址就是需要通过分页表格来间接表达物理地址的一种虚拟地址表达,它的低 12-bit 作为偏移值,接下 10-bit 作为页表格项的索引值,剩下高端的 10-bit 作为页目录表项的索引值,分页表格这一概念后面细讲。如果关闭分页,由逻辑地址转换得到的线性地址其实就是物理地址,通过它就可以直接访问内存了。 分段单元 Segmentation Unit (SU),是CPU内部专门用来处理分段内存模型的线性地址的推导及运算的部件。一个逻辑地址可以表达为 SELECTOR:OFFSET,即虚拟地址由一个16-bit的段选择器,也就是一个段的“选择子”和一个 32-bit (80286 使用 16-bit) 的偏移地址组成,这里的段选择器提供了 13+1 个地址位。段选择器也必需位于段寄存器中,最低两位含有 2-bit 的请求者特权等级 Requestor's Privilege Level (RPL)用来做访问约束,还有 1-bit 的符表指示器 Table Indicator (TI),和高端的 13-bit 索引值用于定位 GDT 或 LDT 上的段描述符,亦即后二者将提供 13+1 个作为寻址用的地址位。这里的索引值就是前面提到,存放于段寄存器中的索引值,因此,在80386 的保护模式中,段寄存器也就可以等价段选择器来使用。这一点就是段寄存器在分段内存模型与分页内存模型使用中最大的区别,在分段内存模型中,段寄存器就是段基址,简单至极。更多内容可以参考 80386 的程序员参考手册,虽然此书是 Intel 1986 年出版的,但一点也不落后,右侧逻辑地址与线性地址关系图就可以从此书的第5章内存管理看到。 GDT/LDT与段选择器的关系 由前一节段描述符可以了解到 80386 的实模式下,内存的分段不再是由段寄存器简单地指出段所在的基址。而是通过段描述符来详细记录段的信息,配合分页机制来实现虚拟内存与物理内存地址的映射。取得段描述符就意味着取得了段所在地址,即虚拟地址。而获取段描述符的关键就是段选择器,它指向 GDT 或 LDT 中的段描述符记录了段的详细信息。例如图 Segment 2,它是通过选择器 0x0027 选中的段,这个选择器可以存放在任意的段寄存器中。0x0027 的高13-bit可以通过右称 3-bit 运算得到,就是 0x04,即 LDT 中的第5个描述符,图中对应绿色的部分。这里提个问题,为什么 0x0027 这个选择器会得到 LDT 上的段描述符呢?答案就是后面。 在逻辑地址输入到 SU 进行转换时,处理器就根据 TI 即选择器的第3位 bit2 来确认从 GDT 或 LDT 入一个64-bit 的段描述符,对应 TI=0、TI=1。这里同时引入了两个概念,GDT 和 LDT,一个称之全局描述符表 Global Descriptor Table (GDT),另一个称之局部描述符表 Local Descriptor Table (LDT)。这里简单理解它们为用来存储段描述符的数组就可以了,后面补充说明。取得段描述符后,处理器进行特权检查: max(CPL, RPL) ≤ DPL 式中 CPL 表示当前特权等级 Current Privilege Level,即 CS 寄存器的最低 2-bit。RPL 在前面提到了,DPL 就是段描述符特权等级 Descriptor Privilege Level,位于段描述符中。这三个特权等级值取 0~3,值越小表示权越高。当不等式不成立,即 CPL、DPL、RPL中,DPL的特权最高,因此进程特权不足以调用段描述符,则处理器产生一个常规保护错误 General Protection (GP),终止系统。否则,SU 继续执行,取出 32-bit 或 16-bit 的偏移值与段极值比较,如果大于段极值,同样产生 GP 宕机。SU 还会用一个 46-bit 的程序逻辑地址对运算得到的逻辑地址进行验证。这个程序逻辑地址由16-bit 段寄存器中的 14-bit,除余下两位用于标记特权等级,外加 32-bit 的偏移值组成。通过后,处理器的分页单元将段描述符内 32-bit (80286 是 24-bit) 的段基址与偏移值相加得到一个物理地址,80386 处理器通过分页机制得到的物理地址是 32-bit 的,而在新的支持物理内存扩展 Physical Address Extension 的处理器上会更大。 这里可以发现,段寄存器存储的索引不仅用来在 GDT 或 LDT 查找段描述符,还用在分页机制中生成物理地址。特权级检查需要完全加载段寄存器,因为段描述符会在段寄存器的隐藏位缓存,这些隐藏位对于开发者是透明的。 逻辑地址、线性地址与物理地址的转换 分页机制的使用有效地避免了内存在长时间使用过程中变得散乱的问题,因为通过分页,程序直接使用的是虚拟地块,通过分页机制的映射,可以有效地利用散乱的物理内存,而虚拟地址总是常用常新,永不散乱。它也是虚拟内存实现的重要环节,通过映射可以将外部存储器如硬盘当作内存来使用,这样即在物理内存很小的系统上,程序也能正常使用完整的寻址空间。x86 内存页通过页目录 Page Directories 和页表格 Page Table 这两个数组来管理。初始时,页目录表为一个页的大小即 4KB,含有 1024 个页目录项 Page Directory Entity (PDE),每项 32-bit。后续增强的处理器可以使用更大的页,而不仅是 4KB。每个 PDE 指向一个页表格,每个页表格初始时也是 4KB,包含了 1024 个页表格项 Page Table Entity (PTE),而每个 PTE 则指向一个物理页的起始地址。而且只有页表格的项都填满时才会被系统使用。其实每一个 PTE 和 PDE 的数据结构是一致的,因此称为页容器 Page Frame (PF)更为恰当。就是一条虚拟地址到物理地址的映射,虚拟内存映射关系就是通过页表格完成的页表格就是映射地址的集合。在寻址时,PF 只有高 20-bit 作为地址数据,低 12-bit 则作为状态控制位使用的。其中有一个状态位 Present (P),当 P=1 时表示 PF 指向的地址有效。另外在每一时刻,只能有一个页目录处于激活状态,由 32-bit 的 CR3 寄存器指明地址,因此 CR3 就是这些表的总入口。两个表合起来就可以寻址 220=1M 个页,而每页为 212=4KB,合共至少可以寻址 220 * 212=4GB。 操作系统实现分页机制时的一个主要功能就是处理 CPU 在进行地址转换时产生的异常,当程序访问未准备好的页即 P=0 的页,CPU 就产生的一个 page-not-present 异常。此时操作系统实现的分页机制就起到以下作用: 确认数据所在二级存储器的地址; 在物理内存中取得一个空页作为页数据容器 Page Frame; 装入受求的数据到空页; 更新页表格以呈现新的数据; 返回控制到引发异常的程序,隐式地执行引发异常的那条代码。 Page Table 与内存映射 查找页表格时,通常会有两种失败的可能。一是使用了无效的虚拟地址,通常是软件原因引起的,系统必需进行处理。现代的操作系统会发出一个称为分段异常 Segmentation Fault 的信息。另一种情况就是查找的页并未装入物理内存,通常是因为物理不足时引起页的卸载以腾出空间给正在请求内存的程序。这种情况下,未装入的页数据通常在二级存储器的交换文件、页文件或交换分区上,因此只需要将数据装回到物理内存中即可。在物理内存不足的情况下,卸载页以腾出空间是很常有的事。在装入页数据后,就需要及时更新 TLB 的缓存,以保证映射正确无误。 这个过程可以用以下的流程图说明,CPU 内部的内存管理单元 Memory Management Unit (MMU)暂存着就近使用的页表格映射, 当接收到需要转换的虚拟地址时,转换备用缓冲 Translation Lookaside Buffer (TLB) 就从 MMU 暂存的页表格映射中查找,如果找到对应的物理地址就直接返回给程序,表示命中缓存 Cache Hit。如果没有找到,则表示缓存错失 Cache Miss,这时就到系统的页表格中查找,如果找到物理地址的映射,则回写到 TLB。因为硬件访问内存时是要通过 TLB 的,所以这个操作是必需的。如果在页表格也没找到对应的物理地址映射,那么就引发异常。 Intel 内存模型与编程 正如前面所讲,寻址涉及的知识点是非常广泛的,它不仅和指令的用法,还和 CPU 内部对内存的处理方式紧密相关。由此,在实际的编程工作中就产生了一系列的在内存编程模型,及指针约定: 模型 数据指针 代码指针 约定 Tiny near CS=DS=SS=ES Small near near DS=SS Medium near far DS=SS,多个代码段 Compact far near 单一代码段,多个数据段 Large far far 多个代码段及数据段 Huge huge huge 多个代码段及数据段,单个数组可以超过 64 KB 编译器将会根据程序设定的内存模型进行处理源代码的编译,由上表可知,near 指针是较快的,因为它不用修改段寄存器或很少修改,节省了 CPU 执行指令的时钟周期。而 far 指针则会包含 DS 或 CS 值,访问内存时会修改段寄存器,完成后再恢复,因此会比 near 指针要慢。但 far 在性能上的牺牲带来了寻址容量上补偿,它可以寻址超过 1MiB 的空间。MiB 是 IEC 推荐的通用单位,因为 MB 在硬件厂商方面可以解析为 106,在用户方面又可以解析为220。在本文中 MB=MiB,不再进行更改。而 huge 指针则是最慢的,但它可以跨跃多个段,因此可以执行精确的指针比较。如果在平滑的内存模型中,huge 指针可以指向的完整内存空间的任一地址,因此,两个指向同一物理内存地址的 huge 指针一定是相等的,不像其它的指针一样可以将物理地址分解成不同的段值和偏移值。 平滑虚拟寻址 系统还可以使用一种简易的内存模型——平滑虚拟寻址 Flat Virtual Address。平滑就是这种模型的特色,通过设置段寄存器指向一个特定的段描述符就可以实现,这就是简易之处。这个特定的段描述符包含的值为 offset=0、limit=232。这样所有程序在无需处理段寄存器的情况下就可以使用232=4GB的寻址空间了!平滑虚拟寻址模型的实现,得益于 80386 CPU 对通用寄存器的扩展,即在 8086 上的 AX、BX、CX、DX 寄存器被扩展成了32-bit的 EAX、EBX、ECX、EDX,通常段描述符中的段基址也一样会被扩展成32-bit。这样通过寄存器寻址就可以完成4GB内存的访问,完全可以不管段寄存器。 段寄存器的隐含功能 段寄存器除了在分页机制上的重要作用外,还有一系列隐含的功能。 如 CPU 取指令时隐含地使用 CS 作为段选择器。 大多数使用数据的指令的寻址隐含地使用 DS 作为段选择器。当然可以使用 ES 作为段选择器,如果以 ES 前缀在指令的寻址处。此外还可以使用 CS、SS 作为段选择器,而不论是目标操作数还是源操作数。例句,将使用 ES 作为目标操作数的段选择器,而不是默认的数据段选择器 DS,这种显式指定段选择器的方式也称为段超越: MOV ES:[BX], AX 处理器的堆栈可以通过 PUSH、POP 等等指令来隐含使用 SS 作为段选择器。而或者通过 (E)SP 或 (E)BP 显式使用SS 作为段选择器,这样的寻址方式亦可以称之为堆栈存储器寻址。 字符串指令如 STOS、MOVS等等,可以使用 DS 作为段选择器来访问数据段或额外数据段 ES 作为选择器。 特权等级 保护模式下的保护环 为什么讲寻址的章节会谈及特权等级!这不是一个让人前着惊的内容编排,80386 作为首个32-bit CPU,它引入的保护特性是一个重要的部分,这里的保护直接体现在内存的保护行为上。前面讲到分页机制时,出现了三个概念 CPL、DPL、RPL。用于标记特权等级的标志取值 0~3,值越小表示权越高,约束越少,对应称为 Ring 0、Ring 1、Ring 2、Ring 3。然而,特权等级远不只在分页机制上应用,它还用来约束软件的数据访问、调用门、执行指令行为,大多数据情况下,系统内核及驱动程序运行在最高特权等级,即最小的约束或无约束的 Ring 0 等级,而应用程序则运行在 Ring 3 即完全受约束的特权等级, 这样一来系统的稳定性就得到了大的增强。如此一来,可以明显知道特权对内存访问的影响是巨大的,对指令的寻址也同样影响深远。右图,在计算科学上称之为保护环 Protection Rings。 80386 提供了内存段级别和分页级别的保护,通过段描述符和页表项指定的保护机制的信息来约束进程,并确认特定的进程是否拥有一些特权指令的使用权。进而增强了系统的强壮性,也为系统的调试带来了便利。 附录、参考资料 IA-32 Intel Architecture Software Developer's Manual, Volumes 1, 2A, 2B, and 3 http://www.intel.com/content/www/us/en/processors/architectures-software-developer-manuals.html IA-32 Intel Architecture Optimization Reference Manual Intel 80386 Reference Programmer's Manual http://pdos.csail.mit.edu/6.828/2005/readings/i386/toc.htm X86 Memory Segmentation http://en.wikipedia.org/wiki/X86_memory_segmentation Hight Memory Area http://en.wikipedia.org/wiki/High_Memory_Area Intel Memory Model http://en.wikipedia.org/wiki/Intel_Memory_Model Protected Mode http://en.wikipedia.org/wiki/Protected_mode Intel x86 Microprocessors http://en.wikipedia.org/wiki/Category:Intel_x86_microprocessors 《Intel微处理器》第8版 Barry B. Brey 著 http://book.51cto.com/art/201006/208446.htm Intel x86 CPU Datasheet http://datasheets.chipdb.org/Intel/x86/