【X86汇编语言 从实模式到保护模式】07 进入保护模式

1 为何要使用保护模式

一般来说,操作系统负责整个计算机软、硬件的管理,它做任何事情都是可以的。但是,用户程序却应当有所限制,只允许它访问属于自己的数据,即使是转移,也只允许在自己的各个代码段之间进行。

但是在实模式下,用户程序对内存的访问非常自由,没有任何限制,随随便便就可以修改任何一个内存单元。

在多用户、多任务时代,内存中会有多个用户(应用)程序在同时运行。为了使它们彼此隔离,防止因某个程序的编写错误或者崩溃而影响到操作系统和其他用户程序,使用保护模式是必要的。

2. 全局描述符表

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

和一个段有关的信息需要 8 个字节,64 位来描述,称为段描述符(Segment Descriptor),每个段都需要一个描述符。而存放这些描述符内存区域构成一个描述符表。

描述符表分**全局描述符表(Global Descriptor Table,GDT)**和局部描述符表(Local Descripto Table,LDT)。全局描述符表是为整个软硬件系统服务的。因此在进入保护模式前,必须要定义全局描述符表。

如图 1 所示,处理器内部有一个 48 位的寄存器,用来访问全局描述符表,称为全局描述符表寄存器(GDTR)
在这里插入图片描述

该寄存器分成 32 位的线性地址和 16 位的边界。GDTR 的 32 位线性基地址部分保存的是全局描述符表在内存中的起始线性地址,16 位边界部分保存的是全局描述符表的边界(界限),其在数值上等于表的大小(总字节数)减一。

GDT 的界限是 16 位的,所以,该表最大是 216 字节,也就是 65536 字节(64KB)。又因为一个描述符占 8 字节,故最多可以定义 8192 个描述符。

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

3 存储器的段描述符

第 7~9 行用于初始化堆栈,使堆栈段的逻辑段地址和代码段相同,并使堆栈指针寄存器 SP 指向 0x7c00。

后面开始定义主引导扇区代码所使用的数据段、代码段和堆栈段。在保护模式下,内存的访问必须通过描述符来进行。因此这些段必须重新在 GDT 中定义。

第 96 行,声明了标号 gdt_base 并初始化了一个双字 0x00007e00,我们决定从这个地方开始创建全局描述符表(GDT)。因为在实模式下,主引导程序的加载位置是 0x0000:0x7c00,32 位下对应着物理地址 0x00007c00,而主引导扇区大小 512 字节,所以把 GDT 设在主引导程序之后,也就是物理地址 0x00007e00 处。其内存分布如图 3 所示。
在这里插入图片描述
一旦确定了 GDT 在内存中的起始位置,下一步的工作就是确定要访问的段,并在 GDT 中为这
些段创建各自的描述符。如图 4 所示,每个描述符在 GDT 中占 8 字节,即 64 位。图中,下面是低 32 位,上面是高 32 位。
在这里插入图片描述
由上图可知,段描述符中:

  1. 段起始地址 32 位,段边界 20 位。在 32 位保护模式下,段地址是 32 位的线性地址,如果未开启分页功能,该线性地址就是物理地址。段基地址可以是 0~4GB 范围内的任意地址。20 位的段界限用来限制段的扩展范围。因为访问内存的方法是用段基地址加上偏移量。对于向上扩展的段,如代码段和数据段来说,偏移量是从 0 开始递增,段界限决定了偏移量的最大值;对于向下扩展的段,如堆栈段来说,段界限决定了偏移量的最小值。
  2. G 位是粒度(Granularity)位:用于解释段界限的含义。为 0 时,段界限以字节为单位,段的扩展范围是从 1 字节到 1 兆字节(1B~1MB)。为 1 时,段界限是以 4KB 为单位,段的扩展范围是从 4KB 到 4GB。
  3. D/B 位是默认的操作数大小(Default Operation Size)或者默认的堆栈指针大小(Default
    Stack Pointer Size),又或者上部边界(Upper Bound)标志。
    1. 对于代码段,此位称做 D 位,用于指示指令中默认的偏移地址和操作数尺寸。D=0 表示指令中的偏移地址或者操作数是 16 位的;D=1,指示 32 位的偏移地址或者操作数。
    2. 对于堆栈段来说,该位被叫做 B 位,用于在进行隐式的堆栈操作时,是使用 SP 寄存器还是 ESP 寄存器。隐式的堆栈操作指令包括 push、pop 和 call 等。如果该位是 0 ,在访问那个段时,使用 SP 寄存器,否则就是使用 ESP 寄存器。同时,B 位的值也决定了堆栈的上部边界。如果 B=0,那么堆栈段的上部边界(也就是 SP 寄存器的最大值)为 0xFFFF;如果 B=1,那么堆栈段的上部边界(也就是 ESP 寄存器的最大值)为 0xFFFFFFFF。
  4. L 位是 64 位代码段标志(64-bit Code Segment)位:保留此位给 64 位处理器使用。
  5. AVL 是软件可以使用的位(Available),通常由操作系统来用,处理器并不使用它。
  6. P 位是段存在位(Segment Present):用于指示描述符所对应的段是否存在。为 0 时,表示段不存在。P 位是由处理器负责检查的。每当通过描述符访问内存中的段时,如果 P 位是 0,处理器就会产生一个异常中断。通常,该中断处理过程是由操作系统提供的,该处理过程的任务是负责将该段从硬盘换回内存,并将 P 位置 1 。在多用户、多任务的系统中,这是一种常用的虚拟内存调度策略。
  7. DPL 表示描述符的特权级(Descriptor Privilege Level,DPL)。这两位用于指定段的特权级。共有 4 种处理器支持的特权级别,分别是 0、1、2、3,其中 0 是最高特权级别,3 是最低特权级别。一般是操作系统代码是 0,而普通的用户程序是 3。
  8. S 位是描述符类型(Descriptor Type)位:为 0 时,表示是一个系统段。为 1 时,表示是一个代码段或数据段。
  9. Type:用于指示描述符的子类型,或者说是类别。对于数据段来说,这 4 位分别是 X、E、W、A 位;而对于代码段来说,这 4 位则分别是 X、C、R、A 位。其关系如下表所示。

数据段:

XEWA含义
000x只读
001x读、写
010x只读,向下扩展
011x读、写,向下扩展

E 位指示段的扩展方向。E=0 是向上扩展的,也就是向高地址方向扩展的,是普通的数据段;E=1 是向下扩展的,也就是向低地址方向扩展的,通常是堆栈段。

W 位指示段的读写属性,或者说段是否可写,W=0 的段是不允许写入的,否则会引发处理器异常中断;W=1 的段是可以正常写入的。

代码段:

XCRA含义
100x只执行
101x执行、读
110x只执行,依从的代码段
111x执行、读,依从的代码段

C 位指示段是否为特权级依从的(Conforming)。C=0 表示非依从的代码段,这样的代码段可以从与它特权级相同的代码段调用,或者通过门调用;C=1 表示允许从低特权级的程序转移到该段执行。

R 位指示代码段是否允许读出。代码段总是可以执行的,但是,为了防止程序被破坏,它是不能写入的。至于是否有读出的可能,由 R 位指定。R=0 表示不能读出,如果企图去读一个 R=0 的代码段,会引发处理器异常中断;如果 R=1,则代码段是可以读出的,即可以把这个段的内容当成 ROM 一样使用。

X 表示是否可以执行(eXecutable)。数据段总是不可执行的,X=0;代码段总是可以执行的,因此,X=1。

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

4 安装存储器的段描述符并加载 GDTR

由于现在还在实模式下,因此,在 GDT 中安装描述符,必须将 GDT 的线性地址转换成段地址和偏移地址。

第 12 - 13 行分别将 GDT 线性基地址的低 16 位 和高 16 位传送到寄存器 AX 和 DX 中。
第 14 - 17 行将线性基地址转换为逻辑地址,即将 DX:AX 除以 16,得到的商是逻辑段地址,余数是偏移地址。将 AX 中的逻辑段地址传送到数据段寄存器 DS 中,将 DX 中的偏移地址传送到寄存器 BX 中。

处理器规定,GDT 中的第一个描述符必须是空描述符,或者叫哑描述符或 NULL 描述符。很多时候,寄存器和内存单元的初始值会为 0,再加上程序设计有问题,就会在无意中用全 0 的索引来选择描述符。因此,处理器要求将第一个描述符定义成空描述符。

第 20 - 21 行将两个全 0 的双字分别写入偏移地址为 BX 和 BX+4 的地方。
第 24 - 25 行开始安装代码段描述符,该描述符的低 32 位是 0x7c0001ff,高 32 位是 0x00409800。据此可分析该描述符的基本情况如下:

线性基地址:0x00007C00
段界限:0x001FF
G:0,粒度为字节。则段大小为 512 字节
D:1,32 位的段。
L:0,32 位。
AVL:0。
P:1,段位于内存中。
DPL:00,特权级别为 0。
S:1,属于存储器的段。
Type:1000,只能执行。

该段就是正在执行的主引导程序所在的区域。

第 28 - 29 行安装一个数据段的描述符。低 32 位是 0x8000ffff,高 32 位是 0x0040920b。这个段具有以下性质:

线性基地址:0x000B800
段界限:0x0FFFF。
G:0,粒度为字节。则段大小为 64 KB。
D:1,32 位的段。
L:0,32 位。
AVL:0。
P:1,段位于内存中。
DPL:00,特权级别为 0。
S:1,属于存储器的段。
Type:0010,读写向上扩展的数据段。

第 32 - 33 行,安装堆栈段的描述符。低 32 位是 0x00007a00,高 32 位是 0x00409600。这个段具有以下性质:

线性基地址:0x00000000
段界限:0x07A00。
G:0,粒度为字节。
D:1,32 位的段。
L:0,32 位。
AVL:0。
P:1,段位于内存中。
DPL:00,特权级别为 0。
S:1,属于存储器的段。
Type:0110,读写向下扩展的数据段。

在这里,段界限的值 0x07a00 加上 1(0x07a01),就是 ESP 寄存器所允许的最小值。当执行 push、call 这样的隐式堆栈操作时,处理器会检查 ESP 寄存器的值,一旦发现它小于等于这里指定的数值,会引发异常中断。

第 36 行,将 GDT 表的界限值 31 写入标号 gdt_size 所在的内存单元。这里共有 4 个描述符(包括空描述符),每个描述符占 8 字节,一共是 32 字节。GDT 表的界限值是表的总字节数减去一,所以是 31。
第 38 行,把从标号 gdt_size 开始的 6 字节加载到 GDTR 寄存器。

5 关于第 21 条地址线 A20 的问题

处理器的第 21 根地址线,编号 A20。在 8086 处理器上运行程序不存在 A20 问题,因为它只有 20 根地址线。

实模式下的程序只能寻址 1MB 内存,那是因为它依赖 16 位的段地址左移 4 位,加上 16 位的偏移地址来访问内存。当逻辑段地址达到最大值 0xFFFF 时,再加一,就会因进位而绕回到 0x0000,因为段寄存器只能保留 16 位的结果。至于段内偏移地址,也是如此。

32 位保护模式下,地址线不止 20 根,比 0x0FFFFF 大的数是 0x100000,进位不会被丢弃。为了处理器上运行 8086 程序而不会因地址线而产生问题,因此机器一起的,就强制第 21 根地址线恒为 0。

端口 0x92 的位 1 用于控制 A20,叫做替代的 A20 门控制(Alternate A20 Gate,ALT_A20_GATE),它和来自键盘控制器的 A20 控制线一起,通过或门连接到处理器的 A20M#引脚。

第 40 - 42 行先从该端口读出原数据,接着,将第 2 位(位 1)置“1”,然后再写入该端口,这样就打开了 A20。

6 保护模式下的内存访问

控制这实模式和保护模式切换的开关是一个叫 CR0 的寄存器。CR0 是处理器内部的控制寄存器(Control Register,CR)。之所以有个“0”后缀,是因为还有 CR1、CR2、CR3 和 CR4 控制寄存器。

CR0 是 32 位的寄存器,包含了一系列用于控制处理器操作模式和运行状态的标志位。如图 5 所示它的第 1 位(位 0)是保护模式允许位(Protection Enable,PE),如果把该位置“1”,则处理器进入保护模式,按保护模式的规则开始运行。
在这里插入图片描述
第 44 行:关闭中断。因为保护模式下的中断机制和实模式不同,原有的中断向量表不再适用,而且在保护模式下,BIOS 中断都不能再用。

第 46 - 48 行:将 CR0 寄存器的内容传送到 EAX,并将其位 0 置 1 后,重新传回 CR0。

32 位处理器下有 6 个段寄存器 CS、DS、SS、ES、FS、GS。

如图 6 所示,这 6 个段寄存器又分为两部分,前 16 位和 8086 相同,在实模式下,它们用于按传统的方式寻址 1MB 内存,所以使得 8086 的程序可以继续在 32 位处理器上运行。同时,每个段寄存器还包括一个不可见的 64 位部分,称为描述符高速缓存器,用来存放段的线性基地址、段界限和段属性。它们只能由处理器内部使用。
在这里插入图片描述
在保护模式下访问一个段时,传送到段选择器的是段选择子。它由三部分组成,第一部分是描述符的索引号,用来在描述符表中选择一个段描述符。TI 是描述符表指示器(Table Indicator),TI=0 时,表示描述符在 GDT 中;TI=1 时,描述符在 LDT 中。RPL 是请求特权级,表示给出当前选择子的那个程序的特权级别,正是该程序要求访问这个内存段。如图 7 所示。
在这里插入图片描述
第 56 - 57 行:将描述符选择子 0x0010(二进制数 0000_0000_ 00010_0_00)传送到段选择器 DS 中。从选择子的二进制形式可以看出,指定的描述符索引号是 2,指定的描述符表是 GDT,请求特权级 RPL 是 00。

GDT 的线性基地址在 GDTR 中,又因为每个描述符占 8 字节,因此,描述符在表内的偏移地址是索引号乘以 8。当处理器在执行任何改变段选择器的指令时(比如 pop、mov、jmp far、call far、iret、retf),就将指令中提供的索引号乘以 8 作为偏移地址,同 GDTR 中提供的线性基地址相加,以访问 GDT。处理器会自动将找到的描述符加载到不可见的描述符高速缓存部分。如图 8 所示。
在这里插入图片描述

加载的部分包括段的线性基地址、段界限和段的访问属性。在当前的例子中,线性基地址是 0x000b8000,段界限是 0x0ffff,段的属性是向上扩展,可读写的数据段,粒度为字节。此后,每当有访问内存的指令时,就不再访问 GDT 中的描述符,直接用当前段寄存器描述符高速缓存器提供线性基地址。

第 60 行:执行这条指令时,处理器用 DS 描述符高速缓存中的线性基地址加上指令中给出的偏移量 0x00,形成 32 位物理地址 0x000b8000,并将字符“P”的 ASCII 码写入该处。

7 清空流水线并串行化处理器

在实模式下,段寄存器的描述符高速缓存器也被用于访问内存,仅低 20 位有效,高 12 位是全零。在保护模式下,对段的解释是不同的,处理器会把段选择器里的内容看成是描述符选择子,而不是逻辑段地址。

在进入保护模式前,有很多指令已经进入了流水线。因为处理器工作在实模式下,所以它们都是按 16 位操作数和 16 位地址长度进行译码的,即使是那些用 bits 32 编译的指令。进入保护模式后,由于对段地址的解释不同,对操作数和默认地址大小的解释也不同,有些指令的执行结果可能会不正确,所以必须清空流水线。

使用 32 位远转移指令 jmp 或者远过程调用指令 call,处理器遇到这种指令,一般会清空流水线,并串行化执行。在设置了控制寄存器 CR0 的 PE 位之后,立即用 jmp 或者 call 转移到当前指令流的下一条指令上。为此,第 51 行,用 32 位远转移指令来转移到紧挨着当前指令的下一条指令。

8 保护模式下的堆栈

第 77 -79 行:用于初始化保护模式下的堆栈。堆栈段描述符是 GDT 中的第 4 个(3 号)描述符,堆栈的 32 位线性基地址是 0x00000000,段界限为 0x07a00,粒度为字节,属于可读可写、向下扩展的数据段。

堆栈是向下扩展的,因此,描述符中的段界限,和向上扩展的段含义不同。对于向上扩展的段,段内偏移量是从 0 开始递增,偏移量的最大值是界限值和粒度的乘积;而对于向下扩展的段来说,因为它经常用做堆栈段,而堆栈是从高地址向低地址方向推进的,故段内偏移量的最小值是界限值和粒度的乘积加一。

9 程序运行结果

在这里插入图片描述

11 附录

代码清单 11-1

         ;代码清单11-1
         ;文件名:c11_mbr.asm
         ;文件说明:硬盘主引导扇区代码 
         ;创建日期:2011-5-16 19:54

         ;设置堆栈段和栈指针 
         mov ax,cs      
         mov ss,ax
         mov sp,0x7c00
      
         ;计算GDT所在的逻辑段地址 
         mov ax,[cs:gdt_base+0x7c00]        ;16位 
         mov dx,[cs:gdt_base+0x7c00+0x02]   ;16位 
         mov bx,16        
         div bx            
         mov ds,ax                          ;令DS指向该段以进行操作
         mov bx,dx                          ;段内起始偏移地址 
      
         ;创建0#描述符,它是空描述符,这是处理器的要求
         mov dword [bx+0x00],0x00
         mov dword [bx+0x04],0x00  

         ;创建#1描述符,保护模式下的代码段描述符
         mov dword [bx+0x08],0x7c0001ff     
         mov dword [bx+0x0c],0x00409800     

         ;创建#2描述符,保护模式下的数据段描述符(文本模式下的显示缓冲区) 
         mov dword [bx+0x10],0x8000ffff     
         mov dword [bx+0x14],0x0040920b     

         ;创建#3描述符,保护模式下的堆栈段描述符
         mov dword [bx+0x18],0x00007a00
         mov dword [bx+0x1c],0x00409600

         ;初始化描述符表寄存器GDTR
         mov word [cs: gdt_size+0x7c00],31  ;描述符表的界限(总字节数减一)   
                                             
         lgdt [cs: gdt_size+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 0x0008:flush             ;16位的描述符选择子:32位偏移
                                            ;清流水线并串行化处理器 
         [bits 32] 

    flush:
         mov cx,00000000000_10_000B         ;加载数据段选择子(0x10)
         mov ds,cx

         ;以下在屏幕上显示"Protect mode OK." 
         mov byte [0x00],'P'  
         mov byte [0x02],'r'
         mov byte [0x04],'o'
         mov byte [0x06],'t'
         mov byte [0x08],'e'
         mov byte [0x0a],'c'
         mov byte [0x0c],'t'
         mov byte [0x0e],' '
         mov byte [0x10],'m'
         mov byte [0x12],'o'
         mov byte [0x14],'d'
         mov byte [0x16],'e'
         mov byte [0x18],' '
         mov byte [0x1a],'O'
         mov byte [0x1c],'K'

         ;以下用简单的示例来帮助阐述32位保护模式下的堆栈操作 
         mov cx,00000000000_11_000B         ;加载堆栈段选择子
         mov ss,cx
         mov esp,0x7c00

         mov ebp,esp                        ;保存堆栈指针 
         push byte '.'                      ;压入立即数(字节)
         
         sub ebp,4
         cmp ebp,esp                        ;判断压入立即数时,ESP是否减4 
         jnz ghalt                          
         pop eax
         mov [0x1e],al                      ;显示句点 
      
  ghalt:     
         hlt                                ;已经禁止中断,将不会被唤醒 

;-------------------------------------------------------------------------------
     
         gdt_size         dw 0
         gdt_base         dd 0x00007e00     ;GDT的物理地址 
                             
         times 510-($-$$) db 0
                          db 0x55,0xaa
  • 1
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
x86汇编语言从实模式保护模式》是一本经典的计算机软件和硬件教材。该书通过详细讲解x86汇编语言的实模式保护模式两个阶段的特点和转换过程,帮助读者深入了解计算机的底层工作原理。 实模式是早期x86处理器的工作模式,它具有简单和直接访问内存的特点,但存在一些限制,例如只能寻址最多1MB的内存空间。《x86汇编语言从实模式保护模式》首先介绍了实模式的基本概念和操作指令,然后通过实际的编程示例,让读者熟悉实模式下的汇编语言编程。 接着,《x86汇编语言从实模式保护模式》详细介绍了保护模式的内存管理、特权级和中断处理等概念和机制。保护模式是现代操作系统常用的工作模式,它具有更强大的内存管理能力和安全性,可以有效地利用计算机的资源。通过学习保护模式的相关知识,读者可以了解操作系统的内核和应用程序的执行过程,并且能够编写更强大和高效的程序。 《x86汇编语言从实模式保护模式》还介绍了x86处理器的特殊指令和编程技巧,例如x87浮点指令和SSE指令集等。这些指令和技巧可以提高程序的性能和效率,使程序员能够充分发挥x86体系结构的优势。 总之,《x86汇编语言从实模式保护模式》是一本全面且实用的教材,通过对x86汇编语言的学习,读者可以更深入地了解计算机的底层原理和操作系统的工作方式。无论是想成为程序员还是深入研究计算机体系结构的人员,这本书都是一本必读之作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值