GDT 与分段机制
CPU开机时运行于实模式,寻址方式是段寄存器 \(\times\) 10+偏移寄存器=物理地址,主要原因是因为 8086 地址线和数据线不匹配导致的。但是这种寻址方式既不安全也不支持现代操作系统所需的、多任务支持、cpu 特权模式等。
在实模式下,对于基址,变址寻址的寄存器有明确要求。在保护模式下,除了 esp 以外的所有通用寄存器均可以用于基址,变址寻址。
在 x86 引入的保护模式下,CPU的32条地址线全部有效,可寻址高达4G字节的物理地址空间。为了维护保护模式所支持的各类信息,同时为了兼容,x86 仍然采用分段的方式划分内存,这些关于内存段的限制信息放在一个叫做 全局描述符表(Global Descriptor Table,全球描述符表)的结构里。全局描述符表中含有一个个表项,每一个表项称为段描述符(Descriptor)。而在保护模式下要生成最终的地址,显然就变成了先到 GDT 里拿段基址,再和偏移地址组合起来。而 GDT 由于存了很多段,所以就需要有个指针指向哪个段,这个指针就是段选择子(Selector),平时放在段寄存器里。
注意:GDT 的第 0 个描述符被保留为无效
目的是为了防止非法的段访问,并提供一种有效的机制来检测和处理无效的段引用。通过这种设计,可以增强系统的安全性和稳定性。
由于历史原因,段的基址与界限等被分割为几个块存放到描述符中。
- 段基址(32 位):是该内存段的基地址
- 段界限表示段边界的扩张最值,即最大扩展多少或最小扩展多少,用20位来表示,它的单位可以是字节,也可以是 4KB,这是由G位决定的(G为1时表示单位为4KB)。
- 段的属性和权限标志,它与界限的高 4 位组合在一起。典型的段属性包括:
- 段类型(可执行、可读写等)
- DPL(Descriptor Privilege Level,描述符特权级别)
- P 位(Present 位,段是否存在)
- G 位(Granularity,粒度位)
- AVL 保留备用
- D/B 位(默认操作大小位,用于指示段是16位模式还是32位模式)
这样,每个段在GDT中都规定了大小然后选择子选择了段后,只能访问这个段内的内存,CPU 在越界访问会发生异常。达到了保护模式的效果。
段选择子(Selector) 实际上是 GDT 表索引与三位属性的组合:
- 低 2 位即第 0~1 位, 用来存储 RPL,即请求特权级 ( 0、 1、 2、 3 四种特权级,数字越小权限越大)
- 第 2 位是 TI 位,即 Table Indicator,用来指示选择子是在 GDT 中,还是 LDT 中索引描述符
于是,x86 的分段寻址机制如图所示:
LDT: 历史的遗留
LDT(Local Descriptor Table,本地描述符表)是 x86 架构中用于内存管理的一个结构,最早是在 x86 保护模式下引入的。它的主要作用是定义任务或进程的内存段。LDT 是 GDT(Global Descriptor Table,全局描述符表)的补充,但它针对的是每个任务或进程的局部内存段定义。
在 x86 的段式内存管理中,内存通过段(segment)来访问。段可以定义代码段、数据段或堆栈段等不同类型的内存区域。每个段有自己的基地址、限制(size),以及访问权限控制。这些段的信息存储在一个描述符表中。GDT 和 LDT 就是用于存储这些段描述符的表。
LDT 的设计是为了支持多任务和进程的隔离。它允许每个任务定义自己的段,使得内存保护和隔离更加灵活。LDT 的选择子可以为 1.
但由于现代操作系统普遍采用扁平内存模型和分页机制,LDT 的使用逐渐减少甚至被废弃。它的功能已被分页机制很好地取代,同时也简化了系统的内存管理和任务切换。
简单的引导程序:进入 32 位保护模式
进入保护模式,需要
- 设定 GDT 表,GDT 通常第一个描述符是空描述符,它的基地址和段界限都为 0。
- 加载 GDTR:寄存器GDTR用来存放GDT的入口地址,程序员将GDT设定在内存中某个位置之后,可以通过LGDT指令将GDT的入口地址装入此寄存器,从此以后,CPU就根据此寄存器中的内容作为GDT的入口来访问GDT了。
- 关中断 (cli):保护模式的中断机制与默认实模式不同
- 打开地址线A20: 历史遗留问题,这是为了避免“回卷”现象出现
- 将 cr0 寄存器的 PE 位置为 1,此时 CPU 就已经进入保护模式
- 跳转到 保护模式的代码段 (jmp dword)
描述符构造宏
%macro Descriptor 3
dw %2 & 0FFFFh ; 段界限1
dw %1 & 0FFFFh ; 段基址1
db (%1 >> 16) & 0FFh ; 段基址2
dw ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh) ; 属性1 + 段界限2 + 属性2
db (%1 >> 24) & 0FFh ; 段基址3
%endmacro ; 共 8 字节
示例:在 DOS 下进入 x86 保护模式
%include "pm.inc" ; 常量, 宏, 以及一些说明
org 0100h
jmp LABEL_BEGIN
[SECTION .gdt]
; GDT
; 段基址, 段界限 , 属性
LABEL_GDT: Descriptor 0, 0, 0 ; 空描述符
LABEL_DESC_CODE32: Descriptor 0, SegCode32Len - 1, DA_C + DA_32; 非一致代码段
LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW ; 显存首地址
; GDT 结束
GdtLen equ $ - LABEL_GDT ; GDT长度
GdtPtr dw GdtLen - 1 ; GDT界限
dd 0 ; GDT基地址
; GDT 选择子
SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT
SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT
; END of [SECTION .gdt]
[SECTION .s16]
[BITS 16]
LABEL_BEGIN:
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0100h
; 初始化 32 位代码段描述符
xor eax, eax
mov ax, cs
shl eax, 4
add eax, LABEL_SEG_CODE32
mov word [LABEL_DESC_CODE32 + 2], ax
shr eax, 16
mov byte [LABEL_DESC_CODE32 + 4], al
mov byte [LABEL_DESC_CODE32 + 7], ah
; 为加载 GDTR 作准备
xor eax, eax
mov ax, ds
shl eax, 4
add eax, LABEL_GDT ; eax <- gdt 基地址
mov dword [GdtPtr + 2], eax ; [GdtPtr + 2] <- gdt 基地址
; 加载 GDTR
lgdt [GdtPtr]
; 关中断
cli
; 打开地址线A20
in al, 92h
or al, 00000010b
out 92h, al
; 准备切换到保护模式
mov eax, cr0
or eax, 1
mov cr0, eax
; 真正进入保护模式
jmp dword SelectorCode32:0 ; 执行这一句会把 SelectorCode32 装入 cs,
; 并跳转到 Code32Selector:0 处
; END of [SECTION .s16]
[SECTION .s32]; 32 位代码段. 由实模式跳入.
[BITS 32]
LABEL_SEG_CODE32:
mov ax, SelectorVideo
mov gs, ax ; 视频段选择子(目的)
mov edi, (80 * 11 + 79) * 2 ; 屏幕第 11 行, 第 79 列。
mov ah, 0Ch ; 0000: 黑底 1100: 红字
mov al, 'P'
mov [gs:edi], ax
; 到此停止
jmp $
SegCode32Len equ $ - LABEL_SEG_CODE32
; END of [SECTION .s32]
从 x86 保护模式返回实模式
[SECTION .gdt]
; GDT
;
LABEL_DESC_NORMAL: Descriptor 0, 0ffffh, DA_DRW ; Normal 描述符
; ....
[SECTION .s16]
[BITS 16]
LABEL_BEGIN:
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0100h
mov [LABEL_GO_BACK_TO_REAL+3], ax
mov [SPValueInRealMode], sp
;....
LABEL_REAL_ENTRY: ; 从保护模式跳回到实模式就到了这里
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, [SPValueInRealMode]
in al, 92h ; `.
and al, 11111101b ; | 关闭 A20 地址线
out 92h, al ; /
sti ; 开中断
mov ax, 4c00h ; `.
int 21h ; / 回到 DOS
; END of [SECTION .s16]
; 16 位代码段. 由 32 位代码段跳入, 跳出后到实模式
[SECTION .s16code]
ALIGN 32
[BITS 16]
LABEL_SEG_CODE16:
; 跳回实模式:
mov ax, SelectorNormal
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
mov eax, cr0
and al, 11111110b
mov cr0, eax
LABEL_GO_BACK_TO_REAL:
jmp 0:LABEL_REAL_ENTRY ; 段地址会在程序开始处被设置成正确的值
Code16Len equ $ - LABEL_SEG_CODE16
; END of [SECTION .s16code]
指令扩展
在 实模式,保护模式下的部分指令 mul, div, push, pop 等行为有明显差异,例如 push
- 如果操作数为 8 位,实模式会扩展到 16 位,保护模式 (32 位) 户扩展到 32 位压入栈
- 如果操作数为 16 位或32位,实模式和保护模式都直接压入 sp - 2 / sp - 4