全局描述符表
在进入保护模式前,必须定义全局描述符表。
全局描述符表里面定义的段描述符,许多的段描述符就构成了一张表,称为全局描述符表(GDT)。
段描述符:用 8 个字节来描述该段相关的信息,每个段都需要一个描述符。
全局描述符表寄存器(GDTR)
-
这个是 48 位的寄存器。
-
该寄存器分为两部分,分别是 32 位线性地址和 16 位的边界。
-
GDTR 32 位线性基地址部分:保存全局描述符表在内存中的起始线性地址。
GDTR 16 位边界部分:保存全局描述符表的边界(界限),值 = 表的大小(总字节数)- 1。
因为 GDT 的界限是 16 位的,所以该表最大是
2
16
2^{16}
216 字节,也就是 65536 字节(64KB)。又因为一个描述符占 8 字节,故此最多可定义 8192 个描述符。
由于在进入保护模式后,处理器立即要按新的内容访问模式工作,所以,必须在进入保护模式前定义 GDT。
实模式下只能访问 1MB 内存,故 GDT 通常都定义在 1MB 以下内存访问中。
允许进入保护模式后换个位置重新 定义 GDT。
存储器的段描述符
实模式和保护模式在内存访问上的区别:保护模式下,无法随便访问任意内存,要访问某个内存空间需要先在 GDT 内定义要访问的内存段。
描述符不是由用户程序自己建立的,而是在加载时,由操作系统根据你的程序结构而建立的,而用户通常是无法建立和修改 GDT 的。
在这种情况下,若未按约定访问内存,则会被处理阻止。
20 位的段界限用来限制段的扩展范围:
- 对于向上扩展的段,段界限决定了偏移量的最大值(例如:代码段)。
- 对于向下扩展的段,段界限决定了偏移量的最小值(例如:栈段)。
符号 | 含义 |
---|---|
G(颗粒度) | G=0 时,段界限以字节为单位,此时段的扩展范围是从 1 byte 到 1 MB,因为段界限是 20 位。 G=1 时,段界限以 4KB 为单位,此时段的扩展范围是从 4KB 到 4GB。 |
S(描述符类型) | 用于指定描述符类型,S = 0 时,表示是一个系统段。S = 1 时,表示是一个代码段或数据段。 |
DPL(描述的特权级) | 用于指定段的使用权限,从高到低为 0、1、2、3。刚进入保护模式时执行的代码具有最高特权级 0,这些代码通常都是操作系统的代码。刚加载入的程序会被赋予一个较低的初始特权级。 |
P(段存在位) | 用于指示描述符对应的段是否存在。每当通过描述符访问内存中的段时,若 P = 0,则处理器会产生一个异常中断。 |
D/B | D/B = 0,表示使用 16 位。D/B = 1,表示使用 32 位。 若该段是代码段,该位称为 D 位,用于偏移地址和操作数。 若该段是栈段,该位称为 B 位,决定是用 SP 还是 ESP。 |
L(64 位代码段标志) | L = 1,表示该段采用 64 位。 |
TYPE | 用于指示描述符的子类型。TYPE 字段共 4 位,对于数据段则是 X、E、W、A,对于代码段则是 X、C、R、A。 |
AVL | 是软件可以使用的位,通常由操作系统来使用,处理器并不使用它。 |
TYPE 符号 | 含义 |
---|---|
X | 是否可以执行。数据段总是不可以执行的,X = 0;代码段总是可以执行的,X = 1。 |
E | 指示段的扩展方向。E = 0 是向上扩展,是普通的数据段。E = 1 是向下扩展,通常是栈段。 |
W | 表示段是否可写。W = 0 表示段不可写入,否则引发处理器的异常中断。W = 1 表示段可写。 |
C | 表示段是否为特权级依从的。C = 0 表示不是,C = 1 表示是。 对于代码段来说,C = 0 这样的代码段可以从与它特权级相同的代码段调用,或通过门调用。C = 1 表示允许从低特权级的程序转移到该段执行。 |
R | 表示是否可读。处理器要执行代码需要读指令,这里的 R 并不会限制处理器读取代码执行,而是对程序和指令的限制。例如使用 CS:IP 直接访问某个不可读的段。 |
A | 表示该段最近是否被访问过。在初始时应该置为 0。之后若该段被访问时,处理器自动将其置为 1。操作系统负责该位的清零操作,操作系统定期监视该位的状态,就可以统计出该段的使用频率。 |
安装存储器的段描述符并加载 GDTR
目前还处于实模式,因此在 GDT 中按照描述符,必须将 GDT 的线性地址(物理地址)转换成逻辑段地址和偏移地址。
;计算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 ;段内起始偏移地址
Tips:因为主引导程序的实际加载位置的逻辑地址为 0x0000:0x7c00,因此需要加上 0x7c00。
处理器规定,GDT 中的第一个描述符必须是空描述符。
;创建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 寄存器。
;初始化描述符表寄存器GDTR
mov word [cs: gdt_size+0x7c00],31 ;描述符表的界限(总字节数减一)
;4个描述符,每个描述符8byte,GDT总字节数为4*8=32
;总字节数 - 1 = 31
;
lgdt [cs: gdt_size+0x7c00]
指令介绍:
lgdt m48
- 该操作数是一个 48 位(6 字节)的内存区域。
- 这 6 字节内存区域的划分必须严格按照标准的 GDTR 要求来,由于程序由上至下执行,内存也是从上至下扩展,因此前(低)16 位是 GDT 的界限值,后(高)32 位是 GDT 的基地址。
保护模式下的内存访问
控制实模式和保护模式的开关:CR0 寄存器。
CR0 寄存器是处理器内部的控制寄存器,32 位寄存器,包含了一系列用于控制处理器操作模式和运行状态的标志位。
- 其中第 1 位(位 0)是保护模式的允许位,置为 1 即开启保护模式。
保护模式下的中断机制和实模式不同,因此原有的中断向量表不再适用,且在保护模式下,BIOS 中断都不能使用。所以在重新设置保护模式下的中断环境之前,必须关闭中断。
cli ;保护模式下中断机制尚未建立,应
;禁止中断
设置 PE 位。
mov eax,cr0
or eax,1
mov cr0,eax ;设置PE位
保护模式下的每个段寄存器都包含了一个不可见的部分,称为描述符高速缓存器,用来存放段的线性基地址、段界限和段属性。
在 32 位非保护模式下,访问内存,每当引用一个段时,处理器自动将段左移 4 位,并保存到描述符高速缓存器中。此后就一直使用描述符高速缓存器的内容作为段地址(不然每次都需要访问内存,会影响效率,因此将得到的数据放入寄存器的高速缓存器中,此后就会一直使用缓存器中的内容作为段地址)。
在保护模式下,送到段选择器(描述符高速缓存器)的内容不再是逻辑段地址,而是段描述符在描述表中的索引号。
在保护模式下,访问一个段时,送到段选择器的是段选择子。它由三部分组成:
- 第一部分:描述符的索引号。
- 第二部分:TI 是描述符表指示器,TI = 0 表示描述符在 GDT 中,TI = 1 表示描述符在 LDT 中。
- 第三部分:RPL 是请求特权级,表示给出当前选择子的那个程序的特权级别,正是该程序要求访问这个内存段。
例如:假设某个描述符的索引号是 2,类型是 GDT,请求特权级 RPL 是 00。
对应段选择子为:0000 0000 00010 0 00
若处理器执行了改变段选择器的指令,则会刷新描述符高速缓存器中的内容,具体过程如下:
此后,每当有访问内存的指令时,就不再访问 GDT 中的描述符,直接用当前段寄存器描述符高速缓存器提供的线性基地址。
清空流水线并串行化处理器
进入保护模式后,在实模式中段寄存器的高速缓存存储器中的内容其实还存在,这可能会导致进入保护模式后运行代码的结果会有问题。
进入保护模式前,已有指令进入流水线,在实模式中这些指令都是按 16 位进行译码的。进入保护模式后,受 CS 段描述符高速缓存器中实模式残留内容的影响,处理器将进入 16 位保护模式工作。若保护模式下的代码是 16 位的,影响可能不大,但若使用了 bits 32 进行编译(即变成 32 位代码),那么可能会导致结果的不正确。
使用 jmp 或 call,处理遇到这种指令,一般会清空流水线,并串行化执行。也会重新加载段选择器 CS,并刷新描述符高速缓存器中的内存。
解决方案:设置完 CR0 的 PE 位后,立即使用 jmp 或 call 转移到保护模式。
⚠️ 值得注意的是,jmp cs:ip 中段基地址已经不再是逻辑段基地址,而是段选择子,因为此时已经进入了保护模式。
mov cs, ax
注意:在保护模式下,不允许使用 mov 改变段寄存器 CS 的内容,否则引发异常中断。