操作系统实践之第二章(页目录表切换)

本文通过实例演示了如何利用操作系统的分页机制使两个相同线性地址的程序独立运行,通过切换页目录表来改变地址映射关系,实现不同程序间的地址隔离。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前面我们已经学习了操作系统的分页机制,现在就来体验一下分页的妙处。

如果我们有一个自己写的程序,现在复制一份到其它目录下,再对两个程序同时调试,这时候会发现,其中的寄存器和变量地址都是一样的。但是,在这样的情况下,它们的功能并没有因为“一样的”地址而混淆却各司其职,这就是分页机制的功劳,那么我们就来模拟这一效果吧。

先执行某个线性地址的模块,然后通过改变cr3来转变地址映射关系,再执行同一个线性地址处的模块,由于地址映射已经改变,所以两次得到的应该是不同的输出。

映射关系转换前的情形如下图所示:


开始,我们让ProcPagingDemo中的代码实现向LinearAddrDemo这个线性地址的转移,而LinearAddrDemo映射到物理地址空间中的ProcFoo处。我们让ProcFoo打印出红色的字符串Foo,所以执行时我们应该可以看到红色的Foo。随后我们改变地址映射关系,让其变化成如下形式:


页目录表和页表的切换让LinearAddrDemo映射到ProcBar(物理地址空间)处,所以当我们再一次调用过程ProcPagingDemo时,程序将转移到ProcBar处执行,我们将看到红色的字符串Bar。

下面我们就主要的代码来进行讨论。首先,我们将要用到另外的一套页目录表和页表,所以原先的页目录段和页表段已经不够用了。上一个程序用两个段分别存放页目录表和页表,是为了更加直观和形象。现在可以把它们放到同一个段中,同时把增加的一套页目录和页表也放到这个段中。

为了操作方便,增加一个段,其线性地址空间为0~4GB。由于分页机制启动之前线性地址等同于物理地址,所以通过这个段可以方便地存取特定的物理地址。段定义如下:

LABEL_DESC_FLAT_C:  Descriptor 0,0fffffh, DA_CR|DA_32|DA_LIMIT_4K; 0~4G
LABEL_DESC_FLAT_RW: Descriptor 0,0fffffh, DA_DRW|DA_LIMIT_4K     ; 0~4G
…
SelectorFlatC		equ	LABEL_DESC_FLAT_C	- LABEL_GDT
SelectorFlatRW		equ	LABEL_DESC_FLAT_RW- LABEL_GDT

DA_CR是可执行可读代码段;DA_DRW是可读写数据段;DA_32表示32位段;DA_LIMIT_4K表示段界限粒度为4KB,之所以可以表示4GB就是由2^20*4KB得到的。

之所以用了两个描述符来描述这个段,是因为不仅仅要读写这段内存,而且要执行其中的代码,而这对描述符的属性要求时不一样的(一个是数据段,一个是代码段)。这两个段的段基址都是0,长度都是4GB。

接下来将启动分页的代码做如下的修改:

SetupPaging:
	; 根据内存大小计算应初始化多少PDE以及多少页表
……
.no_remainder:
	mov	[PageTableNumber], ecx	; 暂存页表个数

	; 为简化处理, 所有线性地址对应相等的物理地址. 并且不考虑内存空洞.

	; 首先初始化页目录
	mov	ax, SelectorFlatRW
	mov	es, ax
	mov	edi, PageDirBase0	; 此段首地址为 PageDirBase0
	xor	eax, eax
	mov	eax, PageTblBase0 | PG_P  | PG_USU | PG_RWW
.1:
	stosd
	add	eax, 4096		; 为了简化, 所有页表在内存中是连续的.
	loop	.1

	; 再初始化所有页表
	mov	eax, [PageTableNumber]	; 页表个数
	mov	ebx, 1024		; 每个页表 1024 个 PTE
	mul	ebx
	mov	ecx, eax		; PTE个数 = 页表个数 * 1024
	mov	edi, PageTblBase0	; 此段首地址为 PageTblBase0
	xor	eax, eax
	mov	eax, PG_P  | PG_USU | PG_RWW
.2:
	stosd
	add	eax, 4096		; 每一页指向 4K 的空间
	loop	.2

	mov	eax, PageDirBase0
	mov	cr3, eax

原来我们并没有把页表个数保存起来,但是现在情况发生了变化,手上有两个页表,为了初始化另外的页表时方便起见,在这里增加了一个变量PageTableNumber,页表的个数就在这里。

在整个初始化页目录和页表的过程中,ES始终未SelectorFlatRW。这样,想存钱物理地址的时候,只需要将地址赋值给EDI,那么ES:EDI指向的就是相应的物理地址。为了让程序结构看起来更清晰,我们把所有与分页有关的内容全都放进一个新建的函数PagingDemo中。

在本例中会用到几个函数,但实际上就是内存中指定的地址,我们把它们定义为常量:

LinearAddrDemo    	equ 00401000h
ProcFoo      	 	equ 00401000h
ProcBar       		equ 00501000h
ProcPagingDemo    	equ 00301000h

因为程序开始时LinearAddrDemo指向ProcFoo并且线性地址和物理地址是对等的,所以这两个函数的地址相等。而ProcFoo和ProcBar应该是指定的物理地址,所以LinearAddrDemo也应该是指定的物理地址。也正因为如此,我们使用它们时应该确保使用的是FLAT段(线性地址对应物理地址),即段选择子是SelectorFlatC或者SelectorFlatRW。

为了将我们的代码放置在ProcFoo和ProcBar这两个地方,我们先写两个函数,在程序运行时将这两个函数的执行码赋值过去就可以了。

ProcPagingDemo要调用FLAT段中的LinearAddrDemo,所以如果不想使用段间转移,我们需要把ProcPagingDemo也放进FLAT段中。我们需要写一个函数,然后把代码复制到ProcPagingDemo处。

将代码填充进这些内存地址的实现如下:

; 测试分页机制 ------------------------------------------------------
PagingDemo:
	mov	ax, cs
	mov	ds, ax
	mov	ax, SelectorFlatRW
	mov	es, ax

	push	LenFoo
	push	OffsetFoo
	push	ProcFoo
	call	MemCpy
	add	esp, 12

	push	LenBar
	push	OffsetBar
	push	ProcBar
	call	MemCpy
	add	esp, 12

	push	LenPagingDemoAll
	push	OffsetPagingDemoProc
	push	ProcPagingDemo
	call	MemCpy
	add	esp, 12

	mov	ax, SelectorData
	mov	ds, ax			; 数据段选择子
	mov	es, ax

	call	SetupPaging		; 启动分页

	call	SelectorFlatC:ProcPagingDemo
	call	PSwitch			; 切换页目录,改变地址映射关系
	call	SelectorFlatC:ProcPagingDemo

	ret
; -------------------------------------------------------------------

其中用到了名为MemCpy的函数,它复制三个过程到指定的内存地址,类似于C语言中的memcpy。但有一点不同,它假设源数据放在DS段中,而目的段在ES中。所以在函数的开头对ES和DS进行了赋值。MemCpy函数的定义如下:

; -------------------------------------------------------------------
; 内存拷贝,仿 memcpy
; -------------------------------------------------------------------
; void* MemCpy(void* es:pDest, void* ds:pSrc, int iSize);
; -------------------------------------------------------------------
MemCpy:
	push	ebp
	mov	ebp, esp

	push	esi
	push	edi
	push	ecx

	mov	edi, [ebp + 8]	; Destination对应调用处Proc*
	mov	esi, [ebp + 12]	; Source对应调用处offset*
	mov	ecx, [ebp + 16]	; Counter对应调用处Len*
.1:
	cmp	ecx, 0		; 判断计数器
	jz	.2		; 计数器为零时跳出

	mov	al, [ds:esi]		; ┓
	inc	esi			; ┃
					; ┣ 逐字节移动
	mov	byte [es:edi], al	; ┃
	inc	edi			; ┛

	dec	ecx		; 计数器减一
	jmp	.1		; 循环
.2:
	mov	eax, [ebp + 8]	; 返回值

	pop	ecx
	pop	edi
	pop	esi
	mov	esp, ebp
	pop	ebp

	ret			; 函数结束,返回
; MemCpy 结束--------------------------------------------------------

[ebp + 8]是对应的调用处最后一个push的参数,因为函数开头push了ebp,而且调用时call指令会push一下CS和IP,所以此处执行了+8的操作。

其中,被复制的三个过程的代码如下:

PagingDemoProc:
OffsetPagingDemoProc	equ	PagingDemoProc - $$
	mov	eax, LinearAddrDemo
	call	eax
	retf
LenPagingDemoAll	equ	$ - PagingDemoProc

foo:
OffsetFoo		equ	foo - $$
	mov	ah, 0Ch			; 0000: 黑底    1100: 红字
	mov	al, 'F'
	mov	[gs:((80 * 17 + 0) * 2)], ax	; 屏幕第 17 行, 第 0 列。
	mov	al, 'o'
	mov	[gs:((80 * 17 + 1) * 2)], ax	; 屏幕第 17 行, 第 1 列。
	mov	[gs:((80 * 17 + 2) * 2)], ax	; 屏幕第 17 行, 第 2 列。
	ret
LenFoo			equ	$ - foo

bar:
OffsetBar		equ	bar - $$
	mov	ah, 0Ch			; 0000: 黑底    1100: 红字
	mov	al, 'B'
	mov	[gs:((80 * 18 + 0) * 2)], ax	; 屏幕第 18 行, 第 0 列。
	mov	al, 'a'
	mov	[gs:((80 * 18 + 1) * 2)], ax	; 屏幕第 18 行, 第 1 列。
	mov	al, 'r'
	mov	[gs:((80 * 18 + 2) * 2)], ax	; 屏幕第 18 行, 第 2 列。
	ret
LenBar			equ	$ - bar

下面是切换页表目录的核心函数PSwitch:

; 切换页表 ----------------------------------------------------------
PSwitch:
	; 初始化页目录
	mov	ax, SelectorFlatRW
	mov	es, ax
	mov	edi, PageDirBase1	; 此段首地址为 PageDirBase1
	xor	eax, eax
	mov	eax, PageTblBase1 | PG_P  | PG_USU | PG_RWW
	mov	ecx, [PageTableNumber]
.1:
	stosd
	add	eax, 4096		; 为了简化, 所有页表在内存中是连续的.
	loop	.1

	; 再初始化所有页表
	mov	eax, [PageTableNumber]	; 页表个数
	mov	ebx, 1024		; 每个页表 1024 个 PTE
	mul	ebx
	mov	ecx, eax		; PTE个数 = 页表个数 * 1024
	mov	edi, PageTblBase1	; 此段首地址为 PageTblBase1
	xor	eax, eax
	mov	eax, PG_P  | PG_USU | PG_RWW
.2:
	stosd
	add	eax, 4096		; 每一页指向 4K 的空间
	loop	.2

	; 在此假设内存是大于 8M 的
	;这段代码在求LinearAddrDemo在分页后的物理地址
	mov	eax, LinearAddrDemo
	shr	eax, 22
	mov	ebx, 4096
	mul	ebx
	mov	ecx, eax
	mov	eax, LinearAddrDemo
	shr	eax, 12
	and	eax, 03FFh	; 1111111111b (10 bits)
	mov	ebx, 4
	mul	ebx
	add	eax, ecx
	add	eax, PageTblBase1
	;给LinearAddrDemo重新赋值
	mov	dword [es:eax], ProcBar | PG_P | PG_USU | PG_RWW
	;设置cr3
	mov	eax, PageDirBase1
	mov	cr3, eax
	jmp	short .3
.3:
	nop

	ret
; -------------------------------------------------------------------

该函数前面部分与SetupPaging一样,都是完成初始化页目录表和页表的过程,只是后面又增加了改变线性地址LinearAddrDemo对应的物理地址的语句。改变以后,LinearAddrDemo将不再对应ProcFoo,而是对应ProcBar。在后半部分,我们把cr3的值改成了PageDirBase1,这个切换过程宣告完成。

最后代码的执行效果如下(完整代码chapter3/h/pmtest8.asm):


我们看到执行结果中红色的Foo和Bar,这说明我们的页表切换起作用了。其实这和之前在上一篇中提到的不同进程有相同的地址原理是类似的,也是在任务切换时通过改变cr3的值来切换页目录,从而改变地址映射关系。


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值