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 3dw %2 & 0FFFFh ; 段界限1dw %1 & 0FFFFh ; 段基址1db (%1 >> 16) & 0FFh ; 段基址2dw ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh) ; 属性1 + 段界限2 + 属性2db (%1 >> 24) & 0FFh ; 段基址3
%endmacro ; 共 8 字节
示例:在 DOS 下进入 x86 保护模式
%include "pm.inc" ; 常量, 宏, 以及一些说明org 0100hjmp 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, csmov ds, axmov es, axmov ss, axmov sp, 0100h; 初始化 32 位代码段描述符xor eax, eaxmov ax, csshl eax, 4add eax, LABEL_SEG_CODE32mov word [LABEL_DESC_CODE32 + 2], axshr eax, 16mov byte [LABEL_DESC_CODE32 + 4], almov byte [LABEL_DESC_CODE32 + 7], ah; 为加载 GDTR 作准备xor eax, eaxmov ax, dsshl eax, 4add eax, LABEL_GDT ; eax <- gdt 基地址mov dword [GdtPtr + 2], eax ; [GdtPtr + 2] <- gdt 基地址; 加载 GDTRlgdt [GdtPtr]; 关中断cli; 打开地址线A20in al, 92hor al, 00000010bout 92h, al; 准备切换到保护模式mov eax, cr0or eax, 1mov cr0, eax; 真正进入保护模式jmp dword SelectorCode32:0 ; 执行这一句会把 SelectorCode32 装入 cs,; 并跳转到 Code32Selector:0 处
; END of [SECTION .s16][SECTION .s32]; 32 位代码段. 由实模式跳入.
[BITS 32]LABEL_SEG_CODE32:mov ax, SelectorVideomov 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, csmov ds, axmov es, axmov ss, axmov sp, 0100hmov [LABEL_GO_BACK_TO_REAL+3], axmov [SPValueInRealMode], sp
;....LABEL_REAL_ENTRY: ; 从保护模式跳回到实模式就到了这里mov ax, csmov ds, axmov es, axmov ss, axmov 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, SelectorNormalmov ds, axmov es, axmov fs, axmov gs, axmov ss, axmov eax, cr0and al, 11111110bmov cr0, eaxLABEL_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