《ORANGE‘s 一个操作系统的实现》--保护模式进阶

保护模式进阶

大内存读写

GDT段

;GDT
[SECTION .gdt]
;                            段基址,        段界限 , 属性
LABEL_GDT:         Descriptor    0,              0, 0         ; 空描述符
LABEL_DESC_NORMAL: Descriptor    0,         0ffffh, DA_DRW    ; Normal 描述符
LABEL_DESC_CODE32: Descriptor    0, SegCode32Len-1, DA_C+DA_32; 非一致代码段, 32
LABEL_DESC_CODE16: Descriptor    0,         0ffffh, DA_C      ; 非一致代码段, 16
LABEL_DESC_DATA:   Descriptor    0,      DataLen-1, DA_DRW    ; Data
LABEL_DESC_STACK:  Descriptor    0,     TopOfStack, DA_DRWA+DA_32; Stack, 32 位
LABEL_DESC_TEST:   Descriptor 0500000h,     0ffffh, DA_DRW
LABEL_DESC_VIDEO:  Descriptor  0B8000h,     0ffffh, DA_DRW    ; 显存首地址
; GDT 结束

GdtLen		equ	$ - LABEL_GDT	; GDT长度
GdtPtr		dw	GdtLen - 1	; GDT界限
		dd	0		; GDT基地址

; GDT 选择子
SelectorNormal		equ	LABEL_DESC_NORMAL	- LABEL_GDT
SelectorCode32		equ	LABEL_DESC_CODE32	- LABEL_GDT
SelectorCode16		equ	LABEL_DESC_CODE16	- LABEL_GDT
SelectorData		equ	LABEL_DESC_DATA		- LABEL_GDT
SelectorStack		equ	LABEL_DESC_STACK	- LABEL_GDT
SelectorTest		equ	LABEL_DESC_TEST		- LABEL_GDT
SelectorVideo		equ	LABEL_DESC_VIDEO	- LABEL_GDT
; END of [SECTION .gdt]

认识保护模式当中的代码类似,这里也是定义了GDT的描述符、DT表的属性、选择子等内容

其中LABEL_DESC_TEST的段基址被设定为了0500000h远远超过实模式的寻址上限

LABEL_DESC_VIDEO指向了显存的首地址,用于将特定字符显示在屏幕上

数据段

;数据段
[SECTION .data1]
ALIGN	32
[BITS	32]
LABEL_DATA:
SPValueInRealMode	dw	0
; 字符串
PMMessage:		db	"In Protect Mode now. ^-^", 0	; 在保护模式中显示
OffsetPMMessage		equ	PMMessage - $$
StrTest:		db	"ABCDEFGHIJKLMNOPQRSTUVWXYZ", 0
OffsetStrTest		equ	StrTest - $$
DataLen			equ	$ - LABEL_DATA
; END of [SECTION .data1]

OffsetPMMessage定义为PMMessage相对于$$(即LABEL_DATA)的偏移地址

OffsetStrTest定义为StrTest相对于$$(即LABEL_DATA)的偏移地址

DataLen定义了数据段的长度

堆栈段

; 全局堆栈段
[SECTION .gs]
ALIGN	32
[BITS	32]
LABEL_STACK:
	times 512 db 0

TopOfStack	equ	$ - LABEL_STACK - 1

; END of [SECTION .gs]

这段代码定义了一个512个字节的堆栈段LABEL_STACK和一个栈顶指针TopOfStack

实模式->保护模式代码段

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

	; 初始化 16 位代码段描述符
	mov	ax, cs
	movzx	eax, ax
	shl	eax, 4
	add	eax, LABEL_SEG_CODE16
	mov	word [LABEL_DESC_CODE16 + 2], ax
	shr	eax, 16
	mov	byte [LABEL_DESC_CODE16 + 4], al
	mov	byte [LABEL_DESC_CODE16 + 7], ah

	; 初始化 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

	; 初始化数据段描述符
	xor	eax, eax
	mov	ax, ds
	shl	eax, 4
	add	eax, LABEL_DATA
	mov	word [LABEL_DESC_DATA + 2], ax
	shr	eax, 16
	mov	byte [LABEL_DESC_DATA + 4], al
	mov	byte [LABEL_DESC_DATA + 7], ah

	; 初始化堆栈段描述符
	xor	eax, eax
	mov	ax, ds
	shl	eax, 4
	add	eax, LABEL_STACK
	mov	word [LABEL_DESC_STACK + 2], ax
	shr	eax, 16
	mov	byte [LABEL_DESC_STACK + 4], al
	mov	byte [LABEL_DESC_STACK + 7], ah

实模式代码段首先将各个段段首的真实地址写入描述符的段基址当中

注意这两行代码:

	mov	[LABEL_GO_BACK_TO_REAL+3], ax
	mov	[SPValueInRealMode], sp

这两行代码将实模式下的cs赋值给LABEL_GO_BACK_TO_REAL标签往后数第三、四个字节,将实模式下的sp赋值给SPValueInRealMode

其中LABEL_GO_BACK_TO_REAL+3实际上是jmp 0:LABEL_REAL_ENTRY中的0,观察下图jmp指令的内存占用不难发现,byte4byte5指向了jmp指令的段基址,因此这里是修改jmp指令的段基址到实模式下的cs

image-20240911210235987

	; 为加载 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  处

然后加载GDTR,在关中断后打开地址线A20切换到保护模式

保护模式代码段

[SECTION .s32]; 32 位代码段. 由实模式跳入.
[BITS	32]

LABEL_SEG_CODE32:
	mov	ax, SelectorData
	mov	ds, ax			; 数据段选择子
	mov	ax, SelectorTest
	mov	es, ax			; 测试段选择子
	mov	ax, SelectorVideo
	mov	gs, ax			; 视频段选择子

	mov	ax, SelectorStack
	mov	ss, ax			; 堆栈段选择子

	mov	esp, TopOfStack

保护模式开始先将各个数据段赋给对应的寄存器

	; 下面显示一个字符串,数据段的基址就是LABLE_DATA的物理地址
	mov	ah, 0Ch			; 0000: 黑底    1100: 红字
	xor	esi, esi
	xor	edi, edi
	mov	esi, OffsetPMMessage	; 源数据偏移(相对于LABEL_DESC_DATA的偏移量)
	mov	edi, (80 * 10 + 0) * 2	; 目的数据偏移。屏幕每行有 80 个字符,每个字符占用两个字节(一个用于字符,一个用于属性)
	cld
.1:								;刷新标志寄存器
	lodsb				; 从 esi 指向的内存地址加载一个字节到 al 寄存器,并递增 esi
	test	al, al		; 测试 al 寄存器的值是否为零
	jz	.2				; 为0则是终止符,表示显示完毕
	mov	[gs:edi], ax	; 
	add	edi, 2
	jmp	.1
.2:	; 显示完毕

然后将ah置为0ch,设置屏幕属性为黑底红字,将esiedi清零[,然后将OffsetPMMessage(即PMMessage相对于LABEL_DATA的偏移地址)送到esi,设定目标数据的偏移地址并刷新标志寄存器

然后进入循环,从esi指向的内存地址加载一个字节到al寄存器,并递增esi,然后判断al寄存器是否为0('\0'),如果不是终止字符,则向段基址为gs(即SelectorVideo),偏移地址为edi的显存地址中写入数据ax(ah为显示属性,al为显示字符)

.2:	; 显示完毕

	call	DispReturn

	call	TestRead
	call	TestWrite
	call	TestRead

	; 到此停止
	jmp	SelectorCode16:0

显示完毕后依次调用

  • DispReturn:模拟回车显示
  • TestRead :从内存中读内容到显存
  • TestWrite:向内存中写内容

从先后两次调用TestRead得到的不同的显示结果可以判断是否成功进行了大内存读写

TestRead
TestRead:
	xor	esi, esi
	mov	ecx, 8
.loop:
	mov	al, [es:esi]
	call	DispAL
	inc	esi
	loop	.loop

	call	DispReturn

	ret

函数首先设定ecx为8,表示读取8个字节的数据

随后进入循环,从es:esi(即SelectorTest:esi)中读取一个字节的内容到al,并调用DispAL函数将它以16进制的方式打印出来

循环结束后模拟打印一个回车并结束函数

TestWrite
TestWrite:
	push	esi
	push	edi
	xor	esi, esi
	xor	edi, edi
	mov	esi, OffsetStrTest	; 源数据偏移
	cld
.1:
	lodsb
	test	al, al
	jz	.2
	mov	[es:edi], al
	inc	edi
	jmp	.1
.2:

	pop	edi
	pop	esi

	ret

函数将esi赋值为OffsetStrTest(即StrTest的偏移地址)

然后进入循环,将整个字符串写入es(即SelectorTest)中

保护模式->实模式

[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

程序首先将实模式的选择子赋给ax,并初始化其他寄存器,然后置cr0寄存器PE标志位为0,设定程序处于实模式

jmp 0:LABEL_REAL_ENTRY指令已经在前面修改为jmp 实模式下cs对应的地址:LABEL_REAL_ENTRY,因此程序再次跳转到实模式(即[SECTION .s16]代码段)下LABEL_REAL_ENTRY标签处

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]

最后关闭A20地址线并开中断,正式回到实模式

LDT(Local Descriptor Table)

本节内容仅仅介绍pmtest3.asm相对于pmtest2.asm做的改变,并省略初始化描述符代码

GDT中添加LDT描述符

[SECTION .gdt]
; GDT
;......
;                               段基址,       段界限      , 属性
LABEL_DESC_LDT:    Descriptor       0,        LDTLen - 1, DA_LDT	; LDT

; GDT 结束
GdtLen		equ	$ - LABEL_GDT	; GDT长度
GdtPtr		dw	GdtLen - 1	; GDT界限
			dd	0		; GDT基地址

; GDT 选择子
;......
SelectorLDT			equ	LABEL_DESC_LDT		- LABEL_GDT
;......
; END of [SECTION .gdt]

这部分完成了在GDT中定义LDT描述符的过程,其中LABEL_DESC_LDT的段基址被填充为标签LABEL_LDT的真实地址

问:为什么要在GDT中定义LDT?

答:全局描述符表(GDT)是用于存储段描述符的表,而局部描述符表(LDT)是特定于某个进程的描述符表。LDT允许进程拥有自己的段描述符,这有助于实现内存保护和隔离。

LDT段

; LDT
[SECTION .ldt]
ALIGN	32
LABEL_LDT:
;                            段基址,      段界限  ,    属性
LABEL_LDT_DESC_CODEA: Descriptor 0, CodeALen - 1, DA_C + DA_32 ; Code, 32 位

LDTLen		equ	$ - LABEL_LDT

; LDT 选择子
SelectorLDTCodeA	equ	LABEL_LDT_DESC_CODEA	- LABEL_LDT + SA_TIL
; END of [SECTION .ldt]

LABEL_LDT_DESC_CODEA的基址被填充为标签LABEL_CODE_A的真实地址

注意SelectorLDTCodeA(即选择子)的定义中多了一项SA_TIL,其定义为SA_TIL EQU 4,即将选择子的TIL标志位设置为1

TIL标志位用于区分GDT选择子和LDT选择子,如果TIL为1,则系统会从当前LDT中寻找相应的描述符

LDT代码段

; CodeA (LDT, 32 位代码段)
[SECTION .la]
ALIGN	32
[BITS	32]
LABEL_CODE_A:
	mov	ax, SelectorVideo
	mov	gs, ax			; 视频段选择子(目的)

	mov	edi, (80 * 12 + 0) * 2	; 屏幕第 10 行, 第 0 列。
	mov	ah, 0Ch			; 0000: 黑底    1100: 红字
	mov	al, 'L'
	mov	[gs:edi], ax

	; 准备经由16位代码段跳回实模式
	jmp	SelectorCode16:0
CodeALen	equ	$ - LABEL_CODE_A
; END of [SECTION .la]

LDT代码段的实现与32位代码段大致相同,就是在屏幕上以黑底红字打印字母L并模拟回车显示,并在代码段的末尾调转到SelectorCode16选择子指向的代码段LABEL_SEG_CODE16

LDT和GDT区别

  1. 作用范围

    • GDT(Global Descriptor Table):全局描述符表是系统级别的数据结构,它为整个操作系统定义段描述符。所有的进程和线程都共享同一个GDT。
    • LDT(Local Descriptor Table):局部描述符表是特定于进程的数据结构,每个进程可以有自己的LDT,用于定义该进程特有的段描述符。
  2. 权限和隔离

    • GDT:由于GDT是全局的,它通常包含操作系统核心代码和数据的段描述符,这些描述符通常具有较高的权限级别。
    • LDT:LDT允许进程拥有自己的段描述符,这有助于实现进程间的内存隔离。每个进程的LDT可以有不同的权限设置,从而提供更细粒度的访问控制。
  3. 内容

    • GDT:GDT通常包含代码段、数据段、任务状态段(TSS)、门描述符等。
    • LDT:LDT通常包含该进程特定的代码段、数据段、资源段等。
  4. 使用方式

    • GDT:操作系统在启动时初始化GDT,并在进程切换时使用GDT中的描述符来加载新的段寄存器。
    • LDT:进程在创建时可以创建自己的LDT,并通过特定的系统调用(如set_thread_areamodify_ldt)来加载和切换LDT。
  5. 性能影响

    • GDT:由于所有进程共享GDT,频繁的GDT更新可能会影响系统性能。
    • LDT:每个进程有自己的LDT,因此LDT的更新不会影响到其他进程,这可以在一定程度上减少系统开销。
  6. 安全性

    • GDT:由于GDT是全局的,对GDT的不当修改可能会影响整个系统的稳定性和安全性。
    • LDT:LDT提供了额外的隔离层,即使一个进程的LDT被破坏,也不会影响到其他进程。
  7. 操作系统支持

    • GDT:几乎所有的x86操作系统都支持GDT。
    • LDT:现代操作系统对LDT的支持有所减少,因为现代操作系统更多地依赖于扁平的内存模型和页式内存管理,而不是传统的段式内存管理。

特权级

在IA32的分段机制下,操作系统总共有4个特权级,从高到低分别是0、1、2、3。数字越小表示的特权级越大

image-20240912185040718
CPL

CPL当前执行的程序或任务的特权级

  • 通常情况下:CPL等于代码所在的段的特权级。当程序转移到不同特权级的代码段时,处理器将改变CPL
  • 遇到一致代码段:一致代码段可以被相同或者更低特权级的代码访问,当处理器访问一个与CPL特权级不同的一致代码段时,CPL不会被改变
DPL

DPL表示段或者门的特权级

  • 数据段:DPL规定了可以访问此段的最低特权级
  • 非一致代码段(无调用门):DPL规定访问此段的特权级
  • 调用门:DPL规定了当前执行的程序或任务可以访问此调用门的最低特权级
  • 一致代码段和通过调用门访问的非一致代码段:DPL规定了访问此段的最高特权级
  • TSSDPL规定了可以访问此TSS最低特权级
RPL

对于非一致代码段,处理器通过检查RPLCPL来确认一个访问请求是否合法

  • RPL>CPL:比较目标段的DPL和当前RPL
  • RPL<CPL:比较目标段的DPL和当前CPL

特权级转移

jmpcall实现转移
  • 规则:

    • 非一致代码段:CPL必须等于目标段的DPL,同时要求RPL小于等于DPL

    • 一致代码段:则要求CPL大于或者等于目标段的DPLRPL不做检查,转移后CPL不会发生变化

  • 缺点:

    • 对于非一致代码段,只能在相同特权级代码段之间转移
    • 遇到一致代码段也最多能从低到高
调用门
调用门结构
image-20240912191132232
调用门作用

笔者认为,调用门实际上充当一个中间人作用,使得低特权级的代码段可以访问高特权级代码

假设现在需要由A代码段使用call指令经调用门G访问B代码段,设如下几个标记:

CPL RPL_A DPL_B DPL_G

  • B代码段为一致代码段:
    • A代码段访问调用门G:
      • 对于调用门G,DPL_G规定了访问此调用门的最低特权级
      • CPL ≤ \le DPL_GRPL_A ≤ \le DPL_G
    • 调用门G访问B代码段:
      • 对于一致代码段,DPL_B规定了访问此段的最高特权级
      • DPL_B ≤ \le CPL
  • B代码段为非一致代码段:
    • A代码段访问调用门G:
      • 对于调用门G,DPL_G规定了访问此调用门的最低特权级
      • CPL ≤ \le DPL_GRPL_A ≤ \le DPL_G
    • 调用门G访问B代码段:
      • 对于一致代码段使用call指令时,DPL_B规定了访问此段的最高特权级
      • DPL_B ≤ \le CPL(如果使用jmp指令则DPL_B = = =CPL)

如图所示

image-20240912194306782

有特权级变化时堆栈的变换

TSS

由于x86架构下,各个特权级之间不共享堆栈,因此程序共需要四个堆栈.这些信息被存储在TSS数据结构中:

image-20240912200103957

可以看到,TSS中一共存储了3套ssesp,分别对应0特权级2特权级(由低特权级到高特权级切换时新堆栈才会从TSS中取得,所以TSS中没有位于最外层的堆栈信息)

call过程堆栈变化
  1. 根据目标代码段的DPL(新的CPL)从TSS中选择应该切换至哪个ssesp
  2. 从TSS中读取新的ssesp。在这过程中如果发现ssesp或者TSS界限错误都会导致异常
  3. ss描述符进行检验,如果发生错误,同样产生异常
  4. 暂时性地保存当前ssesp的值
  5. 加载新的ssesp(用新的ssesp去替换旧的)
  6. 将刚刚保存起来的ssesp的值压入新栈
  7. 从调用者堆栈(原堆栈)中将参数复制到被调用者堆栈(新堆栈)中,复制参数的数目由调用门中Param Count一项来决定。如果Param Count是零的话,将不会复制参数
  8. 将当前的cseip压栈
  9. 加载调用门中指定的新的cseip,开始执行被调用者过程。
image-20240912203234672
ret过程堆栈变化
  1. 检查保存的cs中的RPL以判断返回时是否要变换特权级
  2. 弹出并加载被调用者堆栈上的cseip(指向call语句的下一条命令),并会进行代码段描述符和选择子类型和特权级检验
  3. 如果ret指令含有参数,则增加esp的值以跳过参数,然后esp将指向被保存过的调用者ssesp
  4. 弹出并加载ssesp,切换到调用者堆栈,被调用者的ssesp被丢弃。在这里将会进行ss描述符、esp以及ss段描述符的检验
  5. 如果ret指令含有参数,增加esp的值以跳过参数(此时已经在调用者堆栈中)
  6. 检查dsesfsgs的值,如果其中哪一个寄存器指向的段的DPL小于CPL(此规则不适用于一致代码段),那么一个空描述符会被加载到该寄存器。
image-20240912203247364
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值