x86架构学习笔记保护模式

        8086 有 20 根地址线,可以寻址 1MB 内存。但是,它内部的寄存器是16 位的,无法在程序中访问整个 1MB 内存。所以,它也是第一款支持内存分段模型的处理器。还有,8086 处理器只有一种工作模式,即实模式。

        由于 8086 处理器的成功,推动着 Intel 公司不断地研发更新的处理器,32 位的时代就这样到来了。尽管 8086 是 16 位的处理器,但它也是 32位架构内的一部分。原因在于,32 位的处理器架构是从 8086 那里发展来的,是基于 8086 的,具有延续性和兼容性。

        在 16 位处理器内,有 8 个通用寄存器 AX、BX、CX、DX、SI、DI、BP 和 SP,其中,
前 4 个还可以拆分成两个独立的 8 位寄存器来用,即 AH、AL、BH、BL、CH、CL、DH 和
DL。如图 10-1 所示,32 位处理器在 16 位处理器的基础上,扩展了这 8 个通用寄存器的长度,

        为了在汇编语言程序中使用经过扩展(Extend)的寄存器,需要给它们命名,它们的名字
分别是 EAX、EBX、ECX、EDX、ESI、EDI、ESP 和 EBP。可以在程序中使用这些寄存器,
即使是在实模式下:

mov eax,0xf0000005
mov ecx,eax
add edx,ecx

        但是,就像以上指令所示的那样,指令的源操作数和目的操作数必须具有相同的长度,个别特殊用途的指令除外。因此,像这样的搭配是不允许的,在程序编译时,编译器会报告错误:

mov eax,cx ;错误的汇编语言指令

        如果目的操作数是 32 位寄存器,源操作数是立即数,那么,立即数被视为 32 位的:

mov eax,0xf5 ;EAX←0x000000f5

        32 位通用寄存器的高 16 位是不可独立使用的,但低 16 位保持同 16 位处理器的兼容性。因此,在任何时候它们都可以照往常一样使用:

mov ah,0x02
mov al,0x03
add ax,si

        可以在 32 位处理器上运行 16 位处理器上的软件。但是,它并不是 16 位处理器的简单增强。

        事实上,32 位处理器有自己的 32 位工作模式,32 位模式特指 32 位保护模式。在这种模式下,可以完全、充分地发挥处理器的性能。同时,在这种模式下,处理器可以使用它全部的 32根地址线,能够访问 4GB 内存。

        在 32 位模式下,为了生成 32 位物理地址,处理器需要使用 32 位的指令指针寄存器。为此,32 位处理器扩展了 IP,使之达到 32 位,即 EIP。当它工作在 16 位模式下时,依然使用 16 位的 IP;工作在 32 位模式下时,使用的是全部的 32 位 EIP。和往常一样,即使是在 32 位模式下,EIP 寄存器也只由处理器内部使用,程序中是无法直接访问的。对 IP 和 EIP 的修改通常是用某些指令隐式进行的,这此指令包括 JMP、CALL、RET 和 IRET 等等

        


        FS,GS是在保护模式提出的段寄存器

保护模式的提出:

        8086 具有 16 位的段寄存器、指令指针寄存器和通用寄存器(CS、SS、DS、ES、IP、AX、
BX、CX、DX、SI、DI、BP、SP),因此,我们称它为 16 位的处理器。尽管它可以访问 1MB
的内存,但是只能分段进行,而且由于只能使用 16 位的段内偏移量,故段的长度最大只能是64KB。8086 只有一种工作模式,即实模式。当然,这个名称是后来才提出来的。1982 年的时候,Intel 公司推出了 80286 处理器。这也是一款 16 位的处理器,大部分的寄存器都和 8086 处理器一样。因此,80286 和 8086 一样,因为段寄存器是 16 位的,而且只能使用 16 位的偏移地址,在实模式下只能使用 64KB 的段;尽管它有 24 根地址线,理论上可以访问 2^24 ,即 16MB 的内存,但依然只能分成多个段来进行。80286 和 8086 不一样的地方在于,它第一次提出了保护模式的概念。在保护模式下,段寄存器中保存的不再是段地址,而是段选择子,真正的段地址位于段寄存器的描述符高速缓存中,是 24 位的。因此,运行在保护模式下的 80286 处理器可以访问全部 16MB 内存。

        80286 处理器访问内存时,不再需要将段地址左移,因为在段寄存器的描述符高速缓存
器中有 24 位的段物理基地址。这样一来,段可以位于 16MB 内存空间中的任何位置,而不
再限于低端 1MB 范围内,也不必非得是位于 16 字节对齐的地方。不过,由于 80286 的通用
寄存器是 16 位的,只能提供 16 位的偏移地址,因此,和 8086 一样,即使是运行在保护模式
下,段的长度依然不能超过 64KB。对段长度的限制妨碍了 80286 处理器的应用,这就是 16
位保护模式很少为人所知的原因。

        1985 年的 80386 处理器是 Intel 公司的第一款 32 位产品,而且获得了极大成功,是后续所
有 32 位产品的基础。本书中的绝大多数例子,都可以在 80386 上运行。和 8086/80286 不同,
80386 处理器的寄存器是 32 位的,而且拥有 32 根地址线,可以访问 2^32 ,即 4GB 的内存。

        80386,以及所有后续的 32 位处理器,都兼容实模式,可以运行实模式下的 8086 程序。
而且,在刚加电时,这些处理器都自动处于实模式下,此时,它相当于一个非常快速的 8086
处理器。只有在进行一番设置之后,才能运行在保护模式下。在保护模式下,所有的 32 位处理器都可以访问多达 4GB 的内存,它们可以工作在分段模型下,每个段的基地址是 32 位的,段内偏移量也是 32 位的,因此,段的长度不受限制。在最典型的情况下,可以将整个 4GB 内存定义成一个段来处理,这就是所谓的平坦模式。在平坦模式下,可以执行 4GB 范围内的控制转移,也可以使用 32 位的偏移量访问任何 4GB 范围内的任何位置。32 位保护模式兼容 80286 的 16 位保护模式。

        32 位模式特指 IA-32 处理器上的 32 位保护模式。不存在所谓的 32 位实模式,实模式的概念实质上就是 8086 模式。

        线性地址:

为 IA-32 处理器编程,访问内存时,需要在程序中给出段地址和偏移量,因为分段是 IA-32架构的基本特征之一。传统上,段地址和偏移地址称为逻辑地址,偏移地址叫做有效地址(Effective Address,EA),在指令中给出有效地址的方式叫做寻址方式(Addressing Mode)。比如:

inc word [bx+si+0x06]

        在这里,指令中使用的是基址加变址的方式来寻找最终的操作数。段的管理是由处理器的段部件负责进行的,段部件将段地址和偏移地址相加,得到访问内存的地址。一般来说,段部件产生的地址就是物理地址。IA-32 处理器支持多任务。在多任务环境下,任务的创建需要分配内存空间;当任务终止后,还要回收它所占用的内存空间。在分段模型下,内存的分配是不定长的,程序大时,就分配一大块内存;程序小时,就分配一小块。时间长了,内存空间就会碎片化,就有可能出现一种情况:内存空间是有的,但都是小块,无法分配给某个任务。为了解决这个问题,IA-32 处理器支持分页功能,分页功能将物理内存空间划分成逻辑上的页。页的大小是固定的,一般为 4KB,通过使用页,可以简化内存管理。如图所示,当页功能开启时,段部件产生的地址就不再是物理地址了,而是线性地址(LinearAddress),线性地址还要经页部件转换后,才是物理地址。

        在 16 位处理器上,指令中的操作数可以是 8 位或者 16 位的寄存器、指向 8 位或者 16 位实际操作数的 16 位内存地址,以及 8 位或 16 位的立即数。 

        如果指令中包含了内存地址操作数,那么,它必然是一个 16 位的段内偏移地址,称为有效
地址。通过有效地址,可以间接取得 8 位或者 16 位的实际操作数。指定有效地址可以使用基址
寄存器 BX、BP,变址(索引)寄存器 SI 和 DI,同时还可以加上一个 8 位或 16 位的偏移量。比
如:

mov ax,[bx]
mov ax,[bx+di]
mov al,[bx+si+0x02]

        32 位处理器兼容 16 位处理器的工作模式,可以运行传统的 16 位代码。但是,它有自己独立的32 位运行模式,而且只有在这种模式下才能发挥最高的运行效率。在 32 位模式下,默认使用 32 位宽度的寄存器。如:

mov eax,ebx

        如果指令中使用了立即数,那么,该数值默认是 32 位的:

mov ecx,0x55 ;ECX←0x00000055

        还有,如果指令中的操作数是指向内存单元的地址,那么,该地址默认是 32 位的段内偏移地
址,或者叫段内偏移量

mov edx,[mem] ;mem 是一个 32 位的段内偏移地址

        这就是说,如果指令中包含了内存地址操作数,那么,它必然默认地是一个 32 位的有效地址。通过有效地址,可以间接取得 32 位的实际操作数。如图 10-6 所示,指定有效地址可以使用全部的32 位通用寄存器作为基址寄存器。同时,还可以再加上一个除 ESP 之外的 32 位通用寄存器作为变址寄存器。变址寄存器还允许乘以 1、2、4 或者 8 作为比例因子。最后,还允许加上一个 8 位或者32 位的偏移量。

 例如:

add eax,[0x2008] ;有效地址为 0x00002008
sub eax,[eax+0x08] ;有效地址是 32 位的
mov ecx,[eax+ebx*8+0x02] ;有效地址是 32 位的

        值得说明的是,在 16 位模式下,内存寻址方式的操作数不允许使用堆栈指针寄存器 SP。因此,象这条指令就是不正确的:

mov ax,[sp]

        但是,在 32 位模式下,允许在内存操作数中使用堆栈指针寄存器 ESP。因此,下面的指令形式是合法的:

mov eax,[esp]

        在编写程序的时候,就应当考虑到指令的运行环境。为了指明程序的默认运行环境,编译器提供了伪指令 bits,用于指明其后的指令应该被编译成 16 位的,还是 32 位的。

由于 32 位的处理器都拥有 32 位的寄存器和算术逻辑部件,而且同内存芯片之间的数据通路至
少是 32 位的,因此,所有以寄存器或者内存单元为操作数的指令都被扩充,以适应 32 位的算术逻辑操作。而且,这些扩展的操作即使是在 16 位模式下(实模式和 16 位保护模式)也是可用的。比如加法指令 ADD,在 32 位处理器上,除了允许 8 位或者 16 位的操作数外,32 位的操作数现在也是可用的:

add al,bl
add ax,bx
add eax,ebx
add dword [ecx],0x0000005f

        除了双操作数指令,单操作数指令也同样允许 32 位操作数。比如:

inc al
inc dword [0x2000]
dec dword [eax*2]

         shl、shr 等,目的操作数也扩展至 32 位,但用于指定移动次数的源操作数足够应付 32 位的环境,没有变化。举例:

shl eax,1
shl eax,9
shl dword [eax*2+0x08],cl

        和 16 位时代一样,在 32 位处理器上,逻辑移动指令的源操作数如果是寄存器的话,则依然必须使用 CL。同时,32 位处理器在实际执行时,要先将源操作数(在 CL 寄存器内)同 0x1F 做逻辑与。也就是说,仅保留源操作数的低 5 位,因此,实际移动的次数最大为 31。

        在 16 位处理器上,loop 指令的循环次数在寄存器 CX 中。在 32 位处理器上,如果当前的运行模式是 16 位的(bits 16,8086 实模式或者 16 位保护模式),那么,loop 指令执行时,依然使用 CX寄存器;否则,如果运行在 32 位模式下(bits 32),则使用的是 ECX 寄存器。

        在 16 位处理器上,无符号数乘法指令 mul 的格式为

mul r/m8 ;AX ← AL×r/m8
mul r/m16 ;DX:AX ← AX×r/m16

        在 32 位处理器上,除了依然支持上述操作外,还支持以下扩展的格式:

mul r/m32 ;EDX:EAX ← EAX×r/m32   EDX:EAX=EAX*(寄存器或者32位内存单元)

        这样,两个 32 位的数相乘,得到一个 64 位的结果。这里有个例子:

mov eax,0x10000
mov ebx,0x20000
mul ebx

        有符号数乘法指令 imul 与此相同.

相应地,无符号数和有符号数除法也做了 32 位扩展:

div r/m32
idiv r/m32

        在这里,被除数是 64 位的,高 32 位在 EDX 寄存器;低 32 位在 EAX 寄存器。除数是 32 位的,位于 32 位的寄存器,或者存放有 32 位实际操作数的内存地址。指令执行后,32 位的商在 EAX 寄存器,32 位的余数在 EDX 寄存器。

        32 位处理器的堆栈操作指令 push 和 pop 也有所扩展,允许压入双字操作数。特别是,它现在支持立即数压栈操作。立即数压栈操作的指令格式为

push imm8 ;操作码为 6A
push imm16 ;操作码为 68
push imm32 ;操作码为 68

举个例子可能更清楚一些。比如:

push byte 0x55

        在这里,关键字“byte”仅仅是给编译器用的,告诉它,压入的是字节(毕竟立即数 0x55 可以解释为字 0x0055 或者双字 0x00000055),而不是用来在编译后的机器指令前添加指令前缀。这条指令的 16 位形式(用 bits 16 编译)和 32 位形式(用 bits 32 编译)是一样的,机器代码都是
6A 55,但是,当它执行时,就不同了。注意,无论在什么时候,处理器都不会真的压入一字节,要么压入字,要么压入双字。因此,在 16 位模式下,默认的操作数字长是 16,处理器在执行时,将该字节的符号位扩展到高 8 位,然后压入堆栈,压栈时使用 SP 寄存器,且先将 SP 的内容减去 2。这就是说,实际压入堆栈中的数值是 0x0055;在 32 位模式下,压入的内容是该字节操作数符号位扩展到高 24位的结果,即 0x00000055。压栈时使用 ESP 寄存器,且先将 ESP 的内容减去 4。如果压入的是字操作数,则必须用关键字“word”来修饰。如:

push word 0xfffb

        在 16 位模式下,默认的操作数字长是 16,处理器在执行时,直接压入该字,压栈时使用 SP寄存器,且先将 SP 的内容减去 2;在 32 位模式下,压入的内容是该操作数符号位扩展到高 16位的结果,即 0xFFFFFFFB,压栈时使用 ESP 寄存器,且先将 ESP 的内容减去 4。

push dword 0xfb

        则无论是在 16 位模式下,还是在 32 位模式下,压入的都是 0x000000FB,而且堆栈指针寄存器(SP 或者 ESP)都先减去 4

        对于实际操作数位于通用寄存器,或者位于内存单元的情况,只能压入字或者双字,指令格式为:

push r/m16
push r/m32

        如果是寄存器,则可以使用 16 位或者 32 位的通用寄存器。比如:

push ax
push edx

        如果被压入的 16 位或者 32 位操作数位于内存单元中,则必须用关键字“word”或者“dword”修饰,以指示操作数的大小:

push word [0x2000]
push dword [ecx+esi*2+0x02]

        无论被压入的数位于寄存器,还是位于内存单元,在 16 位模式下,如果压入的是字操作数,那么先将 SP 的内容减去 2;如果压入的是双字,应当先将 SP 的内容减去 4。在 32 位模式下,如果压入的是字操作数,那么先将ESP 的内容减去2;如果压入的是双字,应当先将ESP 的内容减去4。

压入段寄存器的操作比较特殊。以下是压入段寄存器的 push 指令格式:

push cs ;机器指令为 0E
push ds ;机器指令为 1E
push es ;机器指令为 06
push fs ;机器指令为 0F A0
push gs ;机器指令为 0F A8
push ss ;机器指令为 16

        在 16 位模式下,先将 SP 的内容减去 2,然后直接压入段寄存器的内容;在 32 位模式下,要先将段寄存器的内容用零扩展到 32 位,即高 16 位为全零。然后,将 ESP 的内容减去 4,再压入扩展后的 32 位值。

        总结:16位模式下Dword压栈4字节其余小于4字节的都压栈2字节,32位模式下只有16位寄存器和小于4字节的内存单元压栈双字节,其余统一四字节包括单字节常数例如:push 0x55 (sp-4)

保护模式:    

    为了让程序在内存中能自由浮动而又不影响它的正常执行,处理器将内存划分
成逻辑上的段,并在指令中使用段内偏移地址。在保护模式下,对内存的访问仍然使用段地址
和偏移地址,但是,在每个段能够访问之前,必须先进行登记。

        这种情况好有一比。就像是开公司做生意,在实模式下,开公司不需要登记,卖什么都没有人管,随时都可以开张。但在保护模式下就不行了,开公司之前必须先登记,登记的信息包括住址(段的起始地址)、经营项目(段的界限等各种访问属性)。这样,每当你做的买卖和项目不符,就会被阻止。对段的访问也是一样,当你访问的偏移地址超出段的界限时,处理器就会阻止这种访问,并产生一个叫做内部异常的中断。

        和一个段有关的信息需要 8 个字节来描述,所以称为段描述符(Segment Descriptor),每个段都需要一个描述符。为了存放这些描述符,需要在内存中开辟出一段空间。在这段空间里,所有的描述符都是挨在一起,集中存放的,这就构成一个描述符表。

        最主要的描述符表是全局描述符表(Global Descriptor Table,GDT),所谓全局,意味着该表是为整个软硬件系统服务的。在进入保护模式前,必须要定义全局描述符表。

        为了跟踪全局描述符表,处理器内部有一个 48 位的寄存器,称为全局描述符表寄存器(GDTR)。该寄存器分为两部分,分别是 32 位的线性地址和 16 位的边界。32 位的处理器具有 32 根地址线,可以访问的地址范围是 0x00000000 到 0xFFFFFFFF,共 2^32 字节的内存,即 4GB 内存。所以,GDTR 的 32 位线性基地址部分保存的是全局描述符表在内存中的起始线性地址,16 位边界部分保存的是全局描述符表的边界(界限),其在数值上等于表的大小(总字节数)减一。

         因为 GDT 的界限是 16 位的,所以,该表最大是 2^16 字节,也就是 65536 字节(64KB)。又因为一个描述符占 8 字节,故最多可以定义 8192 个描述符。实际上,不一定非得这么多,到底有多少,视需要而定,但最多不能超过 8192 个。

        理论上,全局描述符表可以位于内存中的任何地方。但是,如图 11-2 所示,由于在进入保护模式之后,处理器立即要按新的内存访问模式工作,所以,必须在进入保护模式之前定义 GDT。但是,由于在实模式下只能访问 1MB 的内存,故 GDT 通常都定义在 1MB 以下的内存范围中。当然,允许在进入保护模式之后换个位置重新定义 GDT。

        

         G 位是粒度(Granularity)位,用于解释段界限的含义。当 G 位是“0”时,段界限以字节为单位。此时,段的扩展范围是从 1 字节到 1 兆字节(1B~1MB),因为描述符中的界限值是 20 位的。相反,如果该位是“1”,那么,段界限是以 4KB 为单位的。这样,段的扩展范围是从 4KB
到 4GB。

        S 位用于指定描述符的类型(Descriptor Type)。当该位是“0”时,表示是一个系统段;为“1”时,表示是一个代码段或者数据段(堆栈段也是特殊的数据段)。系统段将在以后介绍。

        DPL 表示描述符的特权级(Descriptor Privilege Level,DPL)。这两位用于指定段的特权级。共有 4 种处理器支持的特权级别,分别是 0、1、2、3,其中 0 是最高特权级别,3 是最低特权级别。

        P 是段存在位(Segment Present)。P 位用于指示描述符所对应的段是否存在。一般来说,描述符所指示的段都位于内存中。但是,当内存空间紧张时,有可能只是建立了描述符,对应的内存空间并不存在,这时,就应当把描述符的 P 位清零,表示段并不存在。另外,同样是在内存空间紧张的情况下,会把很少用到的段换出到硬盘中,腾出空间给当前急需内存的程序使用(当前正在执行的),这时,同样要把段描述符的 P 位清零。当再次轮到它执行时,再装入内存,然后将 P 位置 1。P 位是由处理器负责检查的。每当通过描述符访问内存中的段时,如果 P 位是“0”,处理器就会产生一个异常中断。通常,该中断处理过程是由操作系统提供的,该处理过程的任务是负责将该段从硬盘换回内存,并将 P 位置 1。在多用户、多任务的系统中,这是一种常用的虚拟内存调度策略。当内存很小,运行的程序很多时,如果计算机的运行速度变慢,并伴随着繁忙的硬盘操作时,说明这种情况正在发生。

        D/B 位是“默认的操作数大小”(Default Operation Size)或者“默认的堆栈指针大小”(Default
Stack Pointer Size),又或者“上部边界”(Upper Bound)标志。
设立该标志位,主要是为了能够在 32 位处理器上兼容运行 16 位保护模式的程序。该标志位对不同的段有不同的效果。

        对于代码段,此位称做“D”位,用于指示指令中默认的偏移地址和操作数尺寸。D=0 表示指令中的偏移地址或者操作数是 16 位的;D=1,指示 32 位的偏移地址或者操作数。如果代码段描述符的 D 位是 0,那么,当处理器在这个段上执行时,将使用 16位的指令指针寄存器 IP 来取指令,否则使用 32 位的 EIP。

         对于堆栈段来说,该位被叫做“B”位,用于在进行隐式的堆栈操作时,是使用 SP 寄存器还是ESP 寄存器。隐式的堆栈操作指令包括 push、pop 和 call 等。如果该位是“0”,在访问那个段时,使用 SP 寄存器,否则就是使用 ESP 寄存器。同时,B 位的值也决定了堆栈的上部边界。如果 B=0,那么堆栈段的上部边界(也就是 SP 寄存器的最大值)为 0xFFFF;如果 B=1,那么堆栈段的上部边界(也就是 ESP 寄存器的最大值)为 0xFFFFFFFF。

        L 位是 64 位代码段标志(64-bit Code Segment),保留此位给 64 位处理器使用。

        TYPE 字段共 4 位,用于指示描述符的子类型,或者说是类别。如表 11-1 所示,对于数据段来说,这 4 位分别是 X、E、W、A 位而对于代码段来说,这 4 位则分别是 X、C、R、A 位

 

         X 表示是否可以执行(eXecutable)。数据段总是不可执行的,X=0;代码段总是可以执行的,因此,X=1。E 位指示段的扩展方向。E=0 是向上扩展的,也就是向高地址方向扩展的,
是普通的数据段;E=1 是向下扩展的,也就是向低地址方向扩展的,通常是堆栈段。W 位指示段
的读写属性,或者说段是否可写,W=0 的段是不允许写入的,否则会引发处理器异常中断;      W=1  的段是可以正常写入的。C 位指示段是否为特权级依从的(Conforming)。C=0 表示非依从的代码段,这样的代码段可以从与它特权级相同的代码段调用,或者通过门调用;C=1 表示允许从低特权级的程序转移到该段执行。R 位指示代码段是否允许读出。代码段总是可以执行的,但是,为了防止程序被破坏,它是不能写入的。至于是否有读出的可能,由 R 位指定。R=0 表示不能读出,如果企图去读一个 R=0 的代码段,会引发处理器异常中断;如果 R=1,则代码段是可以读出的,即可以把这个段的内容当成 ROM 一样使用。

        数据段和代码段的 A 位是已访问(Accessed)位,用于指示它所指向的段最近是否被访问过。在描述符创建的时候,应该清零。之后,每当该段被访问时,处理器自动将该位置“1”。对该位的清零是由软件(操作系统)负责的,通过定期监视该位的状态,就可以统计出该段的使用频率。当内存空间紧张时,可以把不经常使用的段退避到硬盘上,从而实现虚拟内存管理。

        AVL 是软件可以使用的位(Available),通常由操作系统来用,处理器并不使用它。如果你把它理解成“好吧,该安排的都安排了,最后多出这么一位,不知道干什么用好,就给软件用吧”,
我也不反对,也许 Intel 公司也不会说些什么。

        描述符表总结:

  1.         放置段基地址
  2.         放置段界限
  3.         段界限粒度
  4.         D/B说明是32位代码段,数据段或者16位代码段数据段
  5.         L留给64位系统使用
  6.         AVL保留处理器不用刻意操作系统使用
  7.         P存在表示位1代表存在0代表不存在
  8.         DPL特权级位
  9.         S描述符类型位说明,0是系统段,1是代码或者数据段(堆栈段)
  10.         TYPE描述读写执行属性

保护模式的使用与大于1M地址的访问:

        在实模式下不能访问大于1M字节的地址,就算打开A20地址也不行,必须要在保护模式下才能访问大于1M字节的地址,打开A20地址一般使用0x92这个端口地址的第位,他的第位是软件复位。打开A20地址后设置lgdt寄存器和lgdt表的内容之后,设置CR0控制寄存器第0位打开保护模式,跳转到保护模式下代码运行即可访问你设置的限制之内的内存,最大可以设置4GB,也就是整个32位地址空间。

保护模式使用示例:

	org 0x7c00
section protect1 align=32	
	bits 16
	
	in al,0x92	; 打开A20地址线使能32位地址
	or al,0x02
	out 0x92,al

	mov ax,16		;设置LGDT的表内容填充gdt的地址
	mov [lgdtr],ax
	mov ax,gdttable
	mov	[lgdtr+2],ax
	
	mov ax,0xFFFF			;限制跳转地址大小
	mov [gdttable+8],ax		;是指偏移地址
	mov ax,0x0000
	mov  [gdttable+2+8],ax	;设置32位模式d/b s dpl l 
	mov ax,0x9A00
	mov  [gdttable+4+8],ax
	mov ax,0x0040
	mov  [gdttable+6+8],ax
	

	
	lgdt [lgdtr]	;加载LGDT表地址
	mov EAX,CR0		;开启保护模式
	or eax,0x01
	mov cr0,eax

	jmp 0x0008:code32
	
code32:				;测试代码
	bits 32
	mov EAX,0x0005 
	mov EBX,0x1234
	mul EBX
	
	jmp $
	
code32end:

gdttable:
	times 2*2 dd 0x000000000
	
lgdtr:
	dw	0x0000
	dd	0x000000000
END_CHECK:
	times 510-($-$$) db 0
	db 0x55,0xaa

        在实模式下,处理器访问内存的方式是将段寄存器的内容左移 4 位,再加上偏移地址,以形成 20 位的物理地址。8086 处理器的段寄存器是 16 位的,共有 4 个:CS、DS、ES 和 SS。而在 32 位处理器内,段寄存器是 80 位的(16 位段选择器和 64 位描述符高速缓存器)。而且,在原先的基础上又增加了两个段寄存器 FS 和 GS。32 位处理器的这 6 个段寄存器又分为两部分,前 16 位和 8086 相同,在实模式下,它们用于按传统的方式寻址 1MB 内存,使用方法也没有变化,所以使得 8086 的程序可以继续在 32 位处理器上运行。同时,每个段寄存器还包括一个不可见的 64 位部分,称为描述符高速缓存器,用来存放段的线性基地址、段界限和段属性。既然不可见,那就是处理器不希望我们访问它。事实上,我们也没有任何办法来访问这些不可见的部分,它是由处理器内部使用的。

        在实模式下,访问内存用的是逻辑地址,即将段地址乘以 16,再加上偏移地址。下面是一个例子:

mov cx,0x2000
mov ds,cx
mov [0xc0],al
mov cx,0xb800
mov ds,cx
mov [0x02],ah

        以上,首先将段寄存器 DS 的内容置为 0x2000,这是逻辑段地址。接着,向该段内偏移地址为0x00c0 的地方写入 1 字节(在寄存器 AL 中),写入时,处理器将 DS 的内容左移 4 位,加上偏移地址,实际写入的物理地址是 0x200c0。

         在 8086 处理器上,这是正确的。但是,在 32 位处理器上,这个过程稍有不同。首先,每当引用一个段时,处理器自动将段地址左移 4 位,并传送到描述符高速缓存器。此后,就一直使用描述符高速缓存器的内容做为段地址。所谓引用一个段,就是执行将段地址传送到段寄存器的指令。如:

jmp 0xf000:0x5000

        以上是引用代码段的一个例子,因为代码段的修改通常是用转移和调用指令进行的。如果是引用数据段,则一般采用以下形式:

mov ax,0x2000
mov ds,ax

        只要不改变段寄存器 DS 的内容,以后每次内存访问都直接使用 DS 描述符高速缓存器中的内
容。但是,在实模式下只能向段寄存器传送 16 位的逻辑段地址,故,处理器仍然只能访问 1MB 内存。也就是说,在实模式下,段寄存器描述符高速缓存器的内容仅低 20 位有效,高 12 位全部是零。

        实模式下的 6 个段寄存器 CS、DS、ES、FS、GS 和 SS,在保护模式下叫做段选择器。和实模式不同,保护模式的内存访问有它自己的方式。在保护模式下,尽管访问内存时也需要指定一个段,但传送到段选择器的内容不是逻辑段地址,而是段描述符在描述符表中的索引号。

        在保护模式下访问一个段时,传送到段选择器的是段选择子。它由三部分组成,第一部分是描述符的索引号,用来在描述符表中选择一个段描述符。TI 是描述符表指示器TableIndicator),TI=0 时,表示描述符在 GDT 中;TI=1 时,描述符在 LDT 中。LDT 的知识将在后面进行介绍,它也是一个描述符表,和 GDT 类似。RPL 是请求特权级,表示给出当前选择子的那个程序的特权级别,正是该程序要求访问这个内存段。每个程序都有特权级别,也将在后面慢慢介绍,现在只需要将这两位置成“00”即可。

        这里注意代码段是32位或者16位的是由描述符的D位决定的,打开保护模式立即使用跳转jmp或者转移CALL时会刷新cs缓冲区,在保护模式之前缓冲区的D位是0,转移后D位是由新的GDT表决定的。保护模式后其他段的引用也需要在gdt表里面定义才能使用。

        堆栈的属性也是在gdt表描述符定义,D/B位定义的是上部边界。如果 B=0,那么堆栈段的上部边界(也就是 SP 寄存器的最大值)为 0xFFFF;如果 B=1,那么堆栈段的上部边界(也就是 ESP 寄存器的最大值)为 0xFFFFFFFF。G位定义粒度和段界限一起使用,G=0段界限描述字节,G=1段界限描述4kb的单位。堆栈是向下扩展的,因此,描述符中的段界限,和向上扩展的段含义不同。对于向上扩展的段,段内偏移量是从 0 开始递增,偏移量的最大值是界限值和粒度的乘积而对于向下扩展的段来说,因为它经常用做堆栈段,而堆栈是从高地址向低地址方向推进的,故段内偏移量的最小值是界限值和粒度的乘积加一。在32 位代码中,是用ESP 作为堆栈指针的。因此,这里的段界限,用来和段粒度一起,决定ESP 寄存器所能具有的最小值。即,堆栈操作时,必须符合条件:

        ESP > 段界限×粒度值

        对于堆栈段来说,段界限的值加一,就是段内偏移量的最小值。堆栈会先减sp的值,再以sp的值为基地址存数据,这是栈过程。

        段寄存器(选择器)的值只能用内存单元或者通用寄存器来传送,一般的指令格式
为:

mov sreg,r/m16

        GDT的基地址和界限,都在寄存器 GDTR 中。描述符在内存中的地址,是用索引号乘以 8,再和描述符表的线性基地址相加得到的,而这个地址必须在描述符表的地址范围内。换句话说,索引号乘以 8 得到的数值,必须位于描述符表的边界范围之内。换句话说,处理器从 GDT 中取某个描述符时,就要求描述符的 8 个字节都在 GDT 边界之内,也就是索引号×8+7 小于等于边界。属性是只执行的代码段只能放在CS代码段里面。属性是指gdt表里面的type字段。

        

         对于 DS、ES、FS 和 GS 的选择器,可以向其加载数值为 0 的选择子,尽管在加载的时候不会有任何问题,但在,真正要用来访问内存时,就会导致一个异常中断。这是一个特殊的设计,处理器用它来保证系统安全。对于 CS 和 SS 的选择器来说,不允许向其传送为 0 的选择子。

        EIP 寄存器的内容加上取得的指令长度减 1,都必须小于等于 指令段的段界限制,否则将引发处理器异常中断。

        

         做为一个额外的例子,现在,假设当前代码段的粒度是 4KB,那么,因为描述符中的段界限值是 0x001FF,故实际使用的段界限是

0x1FF×0x1000+0xFFF=0x001FFFFF

        可以认为,此数值就是当前段内最后一个允许访问的偏移地址。任何时候,EIP 寄存器的内
容加上取得的指令长度减 1,都必须小于等于 0x001FFFFF,否则将引发处理器异常中断。任何指令都不允许,也不可能向代码段写入数据。而且,只有在代码段可读的情况下(由其描述符指定),才能由指令读取其内容。

        使用堆栈段还的注意一点例子:

       

         ;代码清单12-1
         ;文件名:c12_mbr.asm
         ;文件说明:硬盘主引导扇区代码 
         ;创建日期:2011-10-27 22:52

         ;设置堆栈段和栈指针 
         mov eax,cs      
         mov ss,eax
         mov sp,0x7c00
      
         ;计算GDT所在的逻辑段地址
         mov eax,[cs:pgdt+0x7c00+0x02]      ;GDT的32位线性基地址 
         xor edx,edx
         mov ebx,16
         div ebx                            ;分解成16位逻辑地址 

         mov ds,eax                         ;令DS指向该段以进行操作
         mov ebx,edx                        ;段内起始偏移地址 

         ;创建0#描述符,它是空描述符,这是处理器的要求
         mov dword [ebx+0x00],0x00000000
         mov dword [ebx+0x04],0x00000000  

         ;创建1#描述符,这是一个数据段,对应0~4GB的线性地址空间
         mov dword [ebx+0x08],0x0000ffff    ;基地址为0,段界限为0xfffff
         mov dword [ebx+0x0c],0x00cf9200    ;粒度为4KB,存储器段描述符 

         ;创建保护模式下初始代码段描述符
         mov dword [ebx+0x10],0x7c0001ff    ;基地址为0x00007c00,512字节 
         mov dword [ebx+0x14],0x00409800    ;粒度为1个字节,代码段描述符 

         ;创建以上代码段的别名描述符
         mov dword [ebx+0x18],0x7c0001ff    ;基地址为0x00007c00,512字节
         mov dword [ebx+0x1c],0x00409200    ;粒度为1个字节,数据段描述符

         mov dword [ebx+0x20],0x7c00fffe
         mov dword [ebx+0x24],0x00cf9600
         
         ;初始化描述符表寄存器GDTR
         mov word [cs: pgdt+0x7c00],39      ;描述符表的界限   
 
         lgdt [cs: pgdt+0x7c00]
      
         in al,0x92                         ;南桥芯片内的端口 
         or al,0000_0010B
         out 0x92,al                        ;打开A20

         cli                                ;中断机制尚未工作

         mov eax,cr0
         or eax,1
         mov cr0,eax                        ;设置PE位
      
         ;以下进入保护模式... ...
         jmp dword 0x0010:flush             ;16位的描述符选择子:32位偏移
                                             
         [bits 32]                          
  flush:                                     
         mov eax,0x0018                      
         mov ds,eax
      
         mov eax,0x0008                     ;加载数据段(0..4GB)选择子
         mov es,eax
         mov fs,eax
         mov gs,eax
      
         mov eax,0x0020                     ;0000 0000 0010 0000
         mov ss,eax
         xor esp,esp                        ;ESP <- 0
      
         mov dword [es:0x0b8000],0x072e0750 ;字符'P'、'.'及其显示属性
         mov dword [es:0x0b8004],0x072e074d ;字符'M'、'.'及其显示属性
         mov dword [es:0x0b8008],0x07200720 ;两个空白字符及其显示属性
         mov dword [es:0x0b800c],0x076b076f ;字符'o'、'k'及其显示属性

         ;开始冒泡排序 
         mov ecx,pgdt-string-1              ;遍历次数=串长度-1 
  @@1:
         push ecx                           ;32位模式下的loop使用ecx 
         xor bx,bx                          ;32位模式下,偏移量可以是16位,也可以 
  @@2:                                      ;是后面的32位 
         mov ax,[string+bx] 
         cmp ah,al                          ;ah中存放的是源字的高字节 
         jge @@3 
         xchg al,ah 
         mov [string+bx],ax 
  @@3:
         inc bx 
         loop @@2 
         pop ecx 
         loop @@1
      
         mov ecx,pgdt-string
         xor ebx,ebx                        ;偏移地址是32位的情况 
  @@4:                                      ;32位的偏移具有更大的灵活性
         mov ah,0x07
         mov al,[string+ebx]
         mov [es:0xb80a0+ebx*2],ax          ;演示0~4GB寻址。
         inc ebx
         loop @@4
      
         hlt 

;-------------------------------------------------------------------------------
     string           db 's0ke4or92xap3fv8giuzjcy5l1m7hd6bnqtw.'
;-------------------------------------------------------------------------------
     pgdt             dw 0
                      dd 0x00007e00      ;GDT的物理地址
;-------------------------------------------------------------------------------                             
     times 510-($-$$) db 0
                      db 0x55,0xaa

        这里的第四段是堆栈段,堆栈段是向下扩展的,每当往堆栈中压入数据时,ESP 的内容要减去操作数的长度。所以,和向高地址方向扩展的段相比,非常重要的一点就是,实际使用的段界限就是段内不允许访问的最低端偏移地址。至于最高端的地址,则没有限制,最大可以是 0xFFFFFFFF。也就是说,在进行堆栈操作时,必须符合以下规则:

        实际使用的段界限+1≤(ESP 的内容-操作数的长度)≤0xFFFFFFFF

在本代码段中因为段界限的粒度是 4KB(G=1),故实际使用的段界限为:

        0xFFFFE×0x1000+0xFFF=0xFFFFEFFF

又因为 ESP 的最大值是 0xFFFFFFFF在操作该段时,处理器的检查规则是:

        0xFFFFF000≤(ESP 的内容-操作数的长度)≤0xFFFFFFFF

        堆栈指针寄存器 ESP 的内容仅仅在访问堆栈时提供偏移地址,操作数在压入堆栈时的物理地址要用段寄存器的描述符高速缓存器中的段基址和 ESP 的内容相加得到。因此,该堆栈最低端的有效物理地址是        

        0x00007C00+0xFFFFF000=0x00006C00

最高端的有效物理地址是

        0x00007C00+0xFFFFFFFF=0x00007BFF

        也就是说,当前程序所定义的堆栈空间介于地址为 0x00006C00~0x00007BFF 之间,大小是 4KB。因此,当第一次进行压栈操作时,假如压入的是一个双字(4 字节):

push ecx

因为压栈操作是先减 ESP,然后再访问堆栈,故 ESP 的新值是:
        0-4=0xFFFFFFFC

这个结果符合上面的限制条件,允许操作。此时,被压入的那个双字,其线性地址为

        0x00007C00+0xFFFFFFFC=0x00007BFC

        堆栈段一般使用向下增加的数据段,而一般的数据访问使用向上属性的数据段,因为是向上扩展的,所以代码段的检查规则同样适用于数据段。不同之处仅仅在于,对于取指令来说,是否越界取决于指令的长度;而对于数据段来说,则取决于操作数的尺寸。考虑以下指令:

mov [0x2000],edx

        这条指令将访问内存,并将 EDX 寄存器的内容写入当前段内偏移量为 0x2000 的双字单元。指令中给出了内存单元的有效地址 EA(0x2000),也给出了操作数的大小(4)。

        0≤(EA+操作数大小-1)≤实际使用的段界限

        在任何时候,段界限之外的访问企图都会被阻止,并引发处理器异常中断。在 32 位处理器上,尽管段界限的检查总在进行着,但如果段界限具有最大值,则对任何内存地址的访问都将不会违例。比如一个具有 4GB 长的段,段的基地址是 0x00000000,段界限是 0xFFFFF,粒度为 4KB。因此,实际使用的段界限是

                0xFFFFF×0x1000+0xFFF=0xFFFFFFFF

在这样的段内,访问任何一个内存单元都是允许的,针对段界限的检查都会获得通过。

        在 32 位模式下,处理器使用 32 位的段基地址加上 32 位的偏移量,共同形成 32 位的物理地址来访问内存。段基地址由段描述符指定,而偏移量由指令直接或者间接给出。很显然,在段最大的时候,可以自由访问 4GB 空间内的任何一个单元。

        代码段不能写入,需要读写就需要定义别的段访问这段数据,上面的代码定义了别名段访问这段代码包含的数据段也就是字符串。

        总结:从8086实模式到32位的保护模式经历了不少变化,包括指令的变化内存操作的变化,指令变化和堆栈操作的变化以上有详细介绍,关键点在于LGDT这个的增加,理解了LGDT表的属性就算基本理解了保护模式的基本操作,关于特权级中断系统段的笔记会在后面给出。

参考资料:

        《x86汇编语言-从实模式到保护模式》        作者:李忠

        《自己动手写操作系统》                              作者:于渊

        《Orange‘s一个操作系统的实现》                作者:于渊

        《汇编语言》                                                 作者:王爽

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值