《自己动手写操作系统》第三章pmtest8源码解析——多任务系统下的地址映射

摘要:本节,我们主要接触现代操作系统的一个重要特性——虚拟地址下的多任务。我们都知道,编写高级语言程序使用的地址都是虚拟线性地址,屏蔽了底层实际的物理地址。也就是说,在相同的物理内存布局条件下,对于任务AB,相同的线性地址line,对应的物理地址内容是不一样的。本节,我们就从实践的角度来看看,这种原理是如何实现的。另外,结合调试过程中出现的错误,讲解关于call和ret指令以及段描述符寄存器的相关知识。

一、理论基础

核心部分的逻辑如下:

在刚刚进入保护模式下,进行内存的初始布局;
开启分页机制,设定第一个映射关系,打印线性地址lineNumber下对应的物理内容;
变换映射关系,改变其中一个页对应的物理地址,打印同样线性地址下的对应的物理地址的内容,对比前后二者的差异。
示意图如下:

其中,上下两条箭头,分别代表两个不同的页表,对于同一个线性地址的映射关系。

二、代码解惑:

接下来,我们看一下,需要添加哪些内容,才能完成相应的多目录切换:

1)添加两个别名段,用于存放页表

LABEL_DESC_FLAT_C       Descriptor  0,0fffffh   ,DA_CR |DA_32 | DA_LIM
LABEL_DESC_FLAT_RW      Descriptor  0,0fffffh   ,DA_DRW | DA_LIMIT_4K 

SelectorFlatC       equ     LABEL_DESC_FLAT_C   -   LABEL_GDT         
SelectorFlatRW      equ     LABEL_DESC_FLAT_RW  -   LABEL_GD

2)我们需要定义四个常量,用于表示内存物理地址中,三个函数的布局,注意,其中有两个是相同的物理地址

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

3)相应的,我们需要改动一下,设置分页机制的函数

A、设置第一个函数的分页机制这个读者根据相关内容自行改动,需要注意的是,在上一节中,页目录和页表是分开的;这一节中,页目录和页表都放在一个段之中。另外,我们定义了一个变量:Pagetablenumber用于存放页表的个数

4)由于要展现地址映射机制,我们需要把对应函数移动到相应的物理地址,这里,我们定义一个函数,PaingDemo,并将它放在32b的代码段中。同时,这个函数用于帮助我们完成整个两次分页机制的变换。

注意:这下面的几个call指令,完成内存的搬运,push是函数的输入变量,这个顺序不能改变,add esp实际上是让这些无效的参数出堆栈。调试心得:开始,将32b代码段设定成DR_C+DR_32,然后调试到下面第二句:mov dsax的时候出现提示load_seg_reg(DS, 0x0028): not data or readable code。这里,我们需要注意:mov ds,ax这句话,绝对不仅仅是将ax中的数值赋值给ds那么简单。如果我们打印ds的详细信息,会发现这些:ds:0x0028, dh=0x00409b03, dl=0x2820039b, valid=1 Code segment, base=0x00032820, limit=0x0000039b, Execute/Read, Non-Conforming, Accessed, 32-bit猜想,段寄存器的赋值,绝对不仅仅是表面上看起来那么简单。在保护模式下,mov ds,ax包含两层意思:A.ax中的值赋值给dsB.ax对应段选择符中隐含的段描述符信息也赋值给ds中寄存器隐含的部分。所以,我们如果改动32b代码段的属性为DA_R+DA_32,就会发现上述错误。
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
; ---------------------------------------------------------------------------

4.1)这里,我们去了解一下memcpy,看一下它的实现;

尤其需要注意的是:为什么需要使用ebp+8?该函数的执行需要提前设定dses,为什么需要这样?——我们从中可以感受到段寄存器在函数执行时候的巨大影响,所以段寄存器的正确设置,是汇编程序正常运行的关键
;memory copy
;para from stack:Dest,Source,Len
MemCpy:
	push	ebp
	mov		ebp,esp

	push	esi
	push	edi
	push	ecx

	mov		edi,[ebp+8]
	mov		esi,[ebp +12]
	mov		ecx,[ebp + 16]

.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];return value

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

	ret
;MemCpy

4.2)接下来,我们定义三个局部函数和相应的变量

注意:这三个函数中,第一个是段外部函数,从段外调用,所以是retf;另外两个是段内函数,所以是ret调试心得:由于不小心漏掉了foo函数的ret,结果出现提示信息:@……#@¥……(猜猜看会是什么?)如此,我们不得不深入思考callret的含义:首先,call(注意call的变种是callf)指令的执行过程是:push cspush ip;ip=newline;执行返回的时候,pop ippop cs,并且可能伴随有堆栈切换。
; PagingDemoProc ------------------------------------------------------------

PagingDemoProc:

OffsetPagingDemoProc	equ	PagingDemoProc - $$

	mov	eax, LinearAddrDemo

	call	eax

	retf

; ---------------------------------------------------------------------------

LenPagingDemoAll	equ	$ - PagingDemoProc

; ---------------------------------------------------------------------------





; foo -----------------------------------------------------------------------

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 -----------------------------------------------------------------------

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

; ---------------------------------------------------------------------------

4.3)接下来,我们进入最关键的环节——页表切换。

实际上,这两个任务大部分的页表是相同的,我们仅仅需要将linerAddrDemo对应线性地址所在页表进行更换即可。代码如下:; 切换页表 ------------------------------------------------------------------
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 的

	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

	mov	dword [es:eax], ProcBar | PG_P | PG_USU | PG_RWW



	mov	eax, PageDirBase1

	mov	cr3, eax

	jmp	short .3

.3:

	nop



	ret

其中,中间过程是在运算liner—对应的页表编号。注意,因为原来linear---的宏定义和ProcFoo是一样的,所以有简单的对应关系。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值