操作系统:保护模式(一)GDT 与分段机制

GDT 与分段机制

CPU开机时运行于实模式,寻址方式是段寄存器 \(\times\) 10+偏移寄存器=物理地址,主要原因是因为 8086 地址线和数据线不匹配导致的。但是这种寻址方式既不安全也不支持现代操作系统所需的、多任务支持、cpu 特权模式等。

在实模式下,对于基址,变址寻址的寄存器有明确要求。在保护模式下,除了 esp 以外的所有通用寄存器均可以用于基址,变址寻址。

在 x86 引入的保护模式下,CPU的32条地址线全部有效,可寻址高达4G字节的物理地址空间。为了维护保护模式所支持的各类信息,同时为了兼容,x86 仍然采用分段的方式划分内存,这些关于内存段的限制信息放在一个叫做 全局描述符表(Global Descriptor Table,全球描述符表)的结构里。全局描述符表中含有一个个表项,每一个表项称为段描述符(Descriptor)。而在保护模式下要生成最终的地址,显然就变成了先到 GDT 里拿段基址,再和偏移地址组合起来。而 GDT 由于存了很多段,所以就需要有个指针指向哪个段,这个指针就是段选择子(Selector),平时放在段寄存器里。

注意:GDT 的第 0 个描述符被保留为无效
目的是为了防止非法的段访问,并提供一种有效的机制来检测和处理无效的段引用。通过这种设计,可以增强系统的安全性和稳定性。

由于历史原因,段的基址与界限等被分割为几个块存放到描述符中。

  1. 段基址(32 位):是该内存段的基地址
  2. 段界限表示段边界的扩张最值,即最大扩展多少或最小扩展多少,用20位来表示,它的单位可以是字节,也可以是 4KB,这是由G位决定的(G为1时表示单位为4KB)。
  3. 段的属性和权限标志,它与界限的高 4 位组合在一起。典型的段属性包括:
    • 段类型(可执行、可读写等)
    • DPL(Descriptor Privilege Level,描述符特权级别)
    • P 位(Present 位,段是否存在)
    • G 位(Granularity,粒度位)
    • AVL 保留备用
    • D/B 位(默认操作大小位,用于指示段是16位模式还是32位模式)

\[实际段界限边界值=(描述符中的段界限+1) \times 段界限的单位大小(即字节或4KB))-1 \]

这样,每个段在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 位保护模式

进入保护模式,需要

  1. 设定 GDT 表,GDT 通常第一个描述符是空描述符,它的基地址和段界限都为 0。
  2. 加载 GDTR:寄存器GDTR用来存放GDT的入口地址,程序员将GDT设定在内存中某个位置之后,可以通过LGDT指令将GDT的入口地址装入此寄存器,从此以后,CPU就根据此寄存器中的内容作为GDT的入口来访问GDT了。
  3. 关中断 (cli):保护模式的中断机制与默认实模式不同
  4. 打开地址线A20: 历史遗留问题,这是为了避免“回卷”现象出现
  5. 将 cr0 寄存器的 PE 位置为 1,此时 CPU 就已经进入保护模式
  6. 跳转到 保护模式的代码段 (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

posted on 2024-10-01 17:12  Himu  阅读(31)  评论(0编辑  收藏  举报