Orange'S:一个操作系统的实现

体会分页机制

Posted by BINBIN Blog on October 31, 2019

克勤克俭用内存

分页的益处其实体现在多个方面,以下举例,先有个初步认识!

如果你写一个程序(在linux 或者windows下面均可),并改个名字复制一份,然后同时调试,你会发现,从变量地址到寄存器的值,几乎全部都是一样的! 而这些“一样的”地址之间完全不会混淆起来,而是各自完成自己的职责。这就是分页的功劳!下面模拟一下这个效果!

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

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

开始,我们让ProcPagingDemo中的代码实现向LinearAddrDemo这个线性地址的转移,而LinerAddrDemo映射到物理地址空间的ProcFoo处。

我们让ProcFoo 打印红色的字符串Foo,所以执行时我们应该可以看到红色的Foo。随后我们改变地址映射关系,变化成如图所示的情形。

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

下面分析需要在pmtest7.asm 做哪些修改!

首先,我们用到了另外一套页目录表和页表,所以原先的页目录段和页表段已经不够用了。事实上,前面的程序中我们用两个段分别存放页目录表和页表,是为了让读者阅读时更加直观和形象。在pmtest8.asm 中,我们把它们放到同一个段中,同时把增加的一套页目录和页表也放到这个段中。

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


...
26 	LABEL_DESC_FLAT_C:  Descriptor 0,        0fffffh, DA_CR|DA_32|DA_LIMIT_4K; 0~4G
27 	LABEL_DESC_FLAT_RW: Descriptor 0,        0fffffh, DA_DRW|DA_LIMIT_4K     ; 0~4G
...

41 	SelectorFlatC		equ	LABEL_DESC_FLAT_C	- LABEL_GDT
42 	SelectorFlatRW		equ	LABEL_DESC_FLAT_RW	- LABEL_GDT



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

下面我们就将启动分页的代码做相应的修改。


257	; 启动分页机制 --------------------------------------------------------------
258	SetupPaging:
259		; 根据内存大小计算应初始化多少PDE以及多少页表
260		xor	edx, edx
261		mov	eax, [dwMemSize]
262		mov	ebx, 400000h	; 400000h = 4M = 4096 * 1024, 一个页表对应的内存大小
263		div	ebx
264		mov	ecx, eax	; 此时 ecx 为页表的个数,也即 PDE 应该的个数
265		test	edx, edx
266		jz	.no_remainder
267		inc	ecx		; 如果余数不为 0 就需增加一个页表
268	.no_remainder:
269		mov	[PageTableNumber], ecx	; 暂存页表个数
270
271		; 为简化处理, 所有线性地址对应相等的物理地址. 并且不考虑内存空洞.
272
273		; 首先初始化页目录
274		mov	ax, SelectorFlatRW
275		mov	es, ax
276		mov	edi, PageDirBase0	; 此段首地址为 PageDirBase0
277		xor	eax, eax
278		mov	eax, PageTblBase0 | PG_P  | PG_USU | PG_RWW
279	.1:
280		stosd
281		add	eax, 4096		; 为了简化, 所有页表在内存中是连续的.
282		loop	.1
283
284		; 再初始化所有页表
285		mov	eax, [PageTableNumber]	; 页表个数
286		mov	ebx, 1024		; 每个页表 1024 个 PTE
287		mul	ebx
288		mov	ecx, eax		; PTE个数 = 页表个数 * 1024
289		mov	edi, PageTblBase0	; 此段首地址为 PageTblBase0
290		xor	eax, eax
291		mov	eax, PG_P  | PG_USU | PG_RWW
292	.2:
293		stosd
294		add	eax, 4096		; 每一页指向 4K 的空间
295		loop	.2
296
297		mov	eax, PageDirBase0
298		mov	cr3, eax
299		mov	eax, cr0
300		or	eax, 80000000h
301		mov	cr0, eax
302		jmp	short .3
303	.3:
304		nop
305
306		ret
307	; 分页机制启动完毕 ----------------------------------------------------------
308

原来并没有把页表个数保存起来,现在情况发生变化,我们不只有一个页目录和页表,为了初始化另外的页表方便起见,在这里增加了一个变量PageTableNumber,页表的个数就存在里面了!

在整个初始化页目录和页表的过程中,es始终为selectorFlatRW。这样,想存取物理地址的时候,只需将地址赋值给edi,那么es:edi指向的就是相应的物理地址。比如页目录物理地址为PageDirBase0,第276行将edi赋值位PageDirBase0,es:di于是指向地址PageDirBase0处,赋值通过指令stosd来实现。初始化页表也是同样的原理。

这样页目录和页表的准备工作完成了。不过我们不再原来的位置调用它,而是新建一个函数PagingDemo,把所有与分页有关的内容全都放进里面,这样,程序看去来结构清晰一些。

根据上面两个图,也就是映射前和映射后的图,我们可以认为在这个程序的实现中有四个要关注的要素,分别是ProcPagingDemo、LinearAddrDemo、ProcFoo、ProcBar,我们把它们成为F4。

因为程序开始时LinearAddrDemo指向ProcFoo并且线性地址和物理地址是对等的,所以LinearAddrDemo应该等于ProcFoo。而ProcFoo和ProcBar应该是指定的物理地址,所以LinearAddrDemo也应该是指定的物理地址。

也正是因为如此我们使用他们时应该确保使用的FLAT段,即段选择子应该是SelectorFlatC或者SelectorFlatRW。

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

这样看来,F4虽然都是当做函数使用,但实际上却都是内存中指定的地址。我们把它们定义为常量

定义的常量:



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


代码填充内存地址的代码就是提到的PagingDemo中。


310	; 测试分页机制 --------------------------------------------------------------
311	PagingDemo:
312		mov	ax, cs
313		mov	ds, ax
314		mov	ax, SelectorFlatRW  ;这应该是selectorFlatRW所对应的段描述符的首地址,参见pmtest7.asm
315		mov	es, ax
316
317		push	LenFoo ;0x0000001c                    sp:0x000001f7 作为参考
318		push	OffsetFoo  ;0x000001a0                   sp:0x000001f3  作为参考
319		push	ProcFoo ;ProcFoo equ 00401000h   sp:0x000001ef
320		call	MemCpy ;前面几个push是MemCpy的参数,sp:0x000001eb,call会由系统自动压入参数,所以前面的sp要加4。

                         ;这段call的功能是把程序拷贝到ProcFoo处
321		add	esp, 12
322
323		push	LenBar
324		push	OffsetBar
325		push	ProcBar
326		call	MemCpy
327		add	esp, 12
328
329		push	LenPagingDemoAll
330		push	OffsetPagingDemoProc
331		push	ProcPagingDemo
332		call	MemCpy
333		add	esp, 12
334
335		mov	ax, SelectorData
336		mov	ds, ax			; 数据段选择子
337		mov	es, ax
338
339		call	SetupPaging		; 启动分页
340
341		call	SelectorFlatC:ProcPagingDemo
342		call	PSwitch			; 切换页目录,改变地址映射关系
343		call	SelectorFlatC:ProcPagingDemo
344
345		ret
346	; ---------------------------------------------------------------------------
347

其中用到了名为MemCpy的函数,它复制三个过程到指定的内存地址,类似于C语言的中memcpy。但有一点不同,它假设源数据放在ds段中,而目的在es段中。所以在函数开头,你找到分别为ds和es赋值的语句。函数MemCpy页放进文件lib.inc! 补充:selectorRW和SelectorFlatC所指向都是同一个段,段基地址0,段界限为4G,只是属性不一样。其实段基址0不起作用,起作用的还是偏移量,偏移量就代表了地址。Call MemCpy需要数据的读写,所以用SelectorRW,Call SelectorFlatC:ProcPagingDemo是代码的执行,所以用SelctorFlatC。



; ------------------------------------------------------------------------
; 内存拷贝,仿 memcpy
; ------------------------------------------------------------------------
; void* MemCpy(void* es:pDest, void* ds:pSrc, int iSize);
; ------------------------------------------------------------------------
MemCpy:
	push	ebp ;sp:0x000001e7 (这是因为call的参数占有了0x000001eb的堆栈) ebp=0x00000000
	mov	ebp, esp  ;ebp=esp=0x000001e7

	push	esi   ; sp:0x000001e3 ,bp:0x000001e7   
	push	edi   ; sp:0xooooo1df ,bp:0x000001e7 
	push	ecx    ; sp:0x000001db, bp:0x000001e7  ;为什么要用bp呢,这是因为sp在不断变化中,不适合作为读取指针

	mov	edi, [ebp + 8]	; Destination  ;ebp+8=0x000001ef,指向ProcFoo,值为:00401000h            (见pmtest8.asm,319行)
	mov	esi, [ebp + 12]	; Source ;ebp+12=0x000001f3,指向OffsetFoo,值为:0x000001a0
	mov	ecx, [ebp + 16]	; Counter ;ebp+16=0x000001f7,指向LenFoo,值为:0x0000001c
.1:
	cmp	ecx, 0		; 判断计数器
	jz	.2		; 计数器为零时跳出

	mov	al, [ds:esi]		; ┓  ;es等于selectorFlatRW所对应的段描述符的首地址(见pmtest8.asm,314行)
	inc	esi			; ┃
					; ┣ 逐字节移动
	mov	byte [es:edi], al	; ┃ ;es等于selectorFlatRW所对应的段描述符的首地址(见pmtest8.asm,314行)
	inc	edi			; ┛

	dec	ecx		; 计数器减一 ;这段程序的功能是把cs:OffsetFoo的程序拷贝到selectorFLatRW段:ProcFoo的地方。 
	jmp	.1		; 循环    ;selectorFlatRW段首地址为0,所以就是拷贝到ProcFoo的地方,即00401000h处  
.2:
	mov	eax, [ebp + 8]	; 返回值  ;eax=00401000h

	pop	ecx      ;sp:0x000001df
	pop	edi      ;sp:0x000001e3
	pop	esi      ;sp:0x000001e7
	mov	esp, ebp   ;ebp=0x000001e7,esp=ebp,esp=0x000001e7
	pop	ebp        ;sp:0x000001eb,ebp=0x00000000

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

被复制的三个代码如下:


402	PagingDemoProc:
403	OffsetPagingDemoProc	equ	PagingDemoProc - $$
404		mov	eax, LinearAddrDemo
405		call	eax
406		retf
407	LenPagingDemoAll	equ	$ - PagingDemoProc
; 设计一个Flat段,尽管他的基址为0,这样做的好处是逻辑很清楚,只要用到这个段名,他们涉及到的代码和数据就都在这个段里。这样逻辑就不会混乱。
call eax时根据cr3的页目录基址,加上LinearAddDemo的变换找到真实物理地址。

我们来看看:假设cr3中放的是PageDirBase0,即200000h,这个页目录里放的PTE为201000h

00401000 转换为2进制就是:0000,0000,0100,0000,0001,0000,0000,0000

前十位0000,0000,01,是页目录偏移量,即偏移一个页目录变为200004,则这个页目录里的PTE为202000h,存放的页为400000h

中十位0000,0000,01,是页表的偏移量,即偏移一个页表,即202004h,则这个页表PTE里页为401000h

 后12位是页偏移地址为0,所以最后的物理地址为401000h,正好是foo函数的地址,去执行foo函数。
408
409	foo:
410	OffsetFoo		equ	foo - $$
411		mov	ah, 0Ch			; 0000: 黑底    1100: 红字
412		mov	al, 'F'
413		mov	[gs:((80 * 17 + 0) * 2)], ax	; 屏幕第 17 行, 第 0 列。
414		mov	al, 'o'
415		mov	[gs:((80 * 17 + 1) * 2)], ax	; 屏幕第 17 行, 第 1 列。
416		mov	[gs:((80 * 17 + 2) * 2)], ax	; 屏幕第 17 行, 第 2 列。
417		ret
418	LenFoo			equ	$ - foo
419
420	bar:
421	OffsetBar		equ	bar - $$
422		mov	ah, 0Ch			; 0000: 黑底    1100: 红字
423		mov	al, 'B'
424		mov	[gs:((80 * 18 + 0) * 2)], ax	; 屏幕第 18 行, 第 0 列。
425		mov	al, 'a'
426		mov	[gs:((80 * 18 + 1) * 2)], ax	; 屏幕第 18 行, 第 1 列。
427		mov	al, 'r'
428		mov	[gs:((80 * 18 + 2) * 2)], ax	; 屏幕第 18 行, 第 2 列。
429		ret
430	LenBar			equ	$ - bar
431

其中,代码第405行只是一个短调用。foo和bar两个函数中为了简化对段寄存器的使用,仍然使用直接将单个字符写入显存的方法。

大部分语句都是内存赋值工作,主要在于最后四个call指令。他们首先启动分页机制,然后调用ProcPagingDemo,在切换页目录,最后又调用一遍ProcPagingDemo.

现在 ProcPagingDemo、 ProcFoo、ProcBar的内容我们都已经知道了! 由于LinearAddrDemo和ProcFoo相等,并且函数SetupPaging 建立起来的是对等的映射关系,所以第一次对ProcPagingDemo的调用反映的就是上图 开始时的内存映射关系!

下面调用PSwitch,我们看一下这个切换页目录的函数是怎样的!

改变地址映射关系


349	; 切换页表 ------------------------------------------------------------------
350	PSwitch:
351		; 初始化页目录
352		mov	ax, SelectorFlatRW
353		mov	es, ax
354		mov	edi, PageDirBase1	; 此段首地址为 PageDirBase1 ; 此段首地址为 PageDirBase1  ,PageDirBase1 equ 210000h ; 
355		xor	eax, eax
356		mov	eax, PageTblBase1 | PG_P  | PG_USU | PG_RWW
357		mov	ecx, [PageTableNumber]
358	.1:
359		stosd
360		add	eax, 4096		; 为了简化, 所有页表在内存中是连续的.
361		loop	.1
362
363		; 再初始化所有页表
364		mov	eax, [PageTableNumber]	; 页表个数
365		mov	ebx, 1024		; 每个页表 1024 个 PTE
366		mul	ebx
367		mov	ecx, eax		; PTE个数 = 页表个数 * 1024
368		mov	edi, PageTblBase1	; 此段首地址为 PageTblBase1
369		xor	eax, eax
370		mov	eax, PG_P  | PG_USU | PG_RWW
371	.2:
372		stosd
373		add	eax, 4096		; 每一页指向 4K 的空间
374		loop	.2
375
376		; 在此假设内存是大于 8M 的
377		mov	eax, LinearAddrDemo  ;mov eax,0x00401000,转化为2进制为:0000,0000,0100,0000,0001,0000,0000,0000
378		shr	eax, 22    ;shr eax,0x16. eax变为0x00000001
379		mov	ebx, 4096     ;mov ebx,0x00001000
380		mul	ebx           ;mul eax,ebx;eax变为0x00001000     
381		mov	ecx, eax      ;ecx=0x00001000    
382		mov	eax, LinearAddrDemo   ;eax=0x00401000,转化为2进制为:0000,0000,0100,0000,0001,0000,0000,0000 
383		shr	eax, 12       ;eax=0x00000401
384		and	eax, 03FFh	; 1111111111b (10 bits) eax=0x00000001  
385		mov	ebx, 4      ;ebx=0x00000001
386		mul	ebx         ;eax=0x00000004
387		add	eax, ecx      ;eax=0x00001004
388		add	eax, PageTblBase1   ;add eax,0x0021100,eax变为0x00212004 
389		mov	dword [es:eax], ProcBar | PG_P | PG_USU | PG_RWW    ;es指向SelectorFlatRW的0x00212004,这句肯定把原来函数地址变了。  ;相对于ox00211000页表首地址,多了1004h,0x00211000对应的是00000000h,

                                                                                      ;多了1004就是401000h物理地址,对于这个地址赋值 ProcBar的首地址。见pmtest6.asm
390
391		mov	eax, PageDirBase1
392		mov	cr3, eax
393		jmp	short .3
394	.3:
395		nop
396
397		ret
398	; ---------------------------------------------------------------------------
399

这个函数前面初始化页目录表和页表的过程与SetupPaging是差不多的,只是紧接着程序增加了改变线性地址LinearAddrDemo对应的物理地址的语句。改变后,LinearAddrDemo将不再对应ProcFoo,而是对应ProcBar。 PSwitch函数的功能是建立好页表后,把与202004h类似的212004(由cr3不同引起的)的这个PTE里的页地址由原来的按顺序建立的00401000h,改为了00501000h,变成了bar函数的地址。 所以此函数调用完成之后,对ProcPagingDemo的调用就变成了上图(后来的内存映射关系)

在代码后半部分,将cr3的值改成了PageDirBase1,这个切换过程完成了

程序运行如图所示!

后来补一下图

可以看到Foo 和 Bar ,说明我们的页表切换起了作用。

前面的章节也有提到不同进程有相同的地址,原理跟本例是类似的,也是在任务切换时通过改变CR3的值来切换页目录,从而改变地址映射关系!

这就是分页妙处!! 由于分页机制的存在,程序使用的都是线性地址空间,而不再直接是物理地址。这好像操作系统为应用程序提供了一个不依赖于硬件(物理内存)的平台。应用程序不必关心实际上有多少物理内存,也不必关心正在使用是哪一段内存。

总之,操作系统全权负责了这其中的转换工作,我们了解了!

前面程序中,用4MB 得空间来存放页表,并用它映射了4GB的内存空间,而我们的物理内存不见得这么大,如果仅仅是对等映射的话,16MB 的内存主要4个页表就够了(一个页表项是4字节 ,有二级页表有1024个表项,每一个表项是对应一个物理页4k的,所以一个页表映射4MB,4个页表映射16MB,注意这个指的是映射空间,一个页表本身占用的4字节乘以 1024 = 4096字节)!所以我们有必要知道内存有多大!方便内存管理!

程序如何知道机器有多少内存!有很多方法,但是书上作者就提供了通用型强的方法,利用中断15h。

  • eax int 15h 可完成许多任务,主要由ax的值决定,我们获取的内存信息,需要将ax赋值位0E820.
  • ebs 放置着“后续值 (continuation value)”,第一次调用时ebx必须为0.
  • es:di 指向一个地址范围描述符结构ARDS (Address Range Descriptor Structure),BIOS将会填充此结构。
  • ecs es:di 所指向的地址范围描述符的大小,以字节为单位。无论es:di 所指向的结构如何设置, BIOS 最多将会填充ecx个字节。不过,通常情况下无论ecx为多大,BIOS 只填充20字节,有些BIOS 忽略ecx的值,总是填充20个字节!
  • edx 0534D4150h (‘SMAP’) —– BIOS将会使用此标志,对调用者将要请求的系统映像信息进行校验,这些信息会被BIOS 放置到es:di所指向的结构中。

中断调用之后,结果存放于下列寄存器之中。

  • CF CD=0 表示没有错误,否则存在错误。
  • eax 0534D4150h(‘SMAP’)。
  • es:di 返回的地址范围描述符结构指针,和输入值相同。
  • ecs BIOS填充在地址范围描述符中的字节数量,被BIOS所返回的最小值是20字节!
  • ebx 这里放置着为等到下一个地址描述符所需要的后续值,这个值得世纪形式依赖于具体的BIOS的实现,调用者不必关心它的具体形式,只需在下次迭代时将其原封不动的放置到ebx中,就可以通过它获取下一个地址范围描述符。如果它的值为0,并且CF 没有进位,表示它是最后一个地址范围描述符。

上面提到的地址范围描述符结构(Address Range Descriptor Structure),如表所示

其中Type的取值和其意义如表:

从上面的说明,我们可以看出,ax = 0E820h时调用int 15h得到的不仅仅是内存的大小,还包括对不同内存段的一些描述。而且,这些描述都被保存在一个缓冲区。所以,在我们调用int 15h之前,必须现有缓冲区。我们可以在每得到一次内存描述时都使用同一个缓冲区,然后针对缓冲区里的数据进行处理,也可以将每次得到数据放进不同的位置,比如一块连续的内存,然后在想要处理它们时再读取。后一种方式可能更方便一些,所以在这里定义了一块256字节的缓冲区 ,在后面附录的代码的(pmtest7.asm)第65行有定义!它最多可以存放12个20字节大小的结构体。

我们将把每次得到的内存信息连续写入这块缓冲区,形成一个结构体数组。然后在保护模式下把它们读出来,显示在屏幕上,并且凭借它们得到内存的容量。

得到内存信息的代码:


...

65 	_MemChkBuf:	times	256	db	0

...

111		; 得到内存数
112		mov	ebx, 0
113		mov	di, _MemChkBuf
114	.loop:
115		mov	eax, 0E820h
116		mov	ecx, 20
117		mov	edx, 0534D4150h
118		int	15h
119		jc	LABEL_MEM_CHK_FAIL
120		add	di, 20
121		inc	dword [_dwMCRNumber]
122		cmp	ebx, 0
123		jne	.loop
124		jmp	LABEL_MEM_CHK_OK
125	LABEL_MEM_CHK_FAIL:
126		mov	dword [_dwMCRNumber], 0
127	LABEL_MEM_CHK_OK:

从代码可以看出,定义了256字节的缓存! 代码使用了一个循环.loop 循环次数是20,一旦CF被置位或者ebx为0,循环将结束!

第一次循环开始之前,eax的被赋值0E820h,ebx 为0,ecx为20,edx 为0534D4150h,es:di 指向了_MemChkBuf的开始处。

在每一次循环进行时,寄存器di的值会递增,每次的增量位20字节。另外,eax,ecx,edx的值都不会变,ebx的值我们置之不理。

同时,每次循环我们让_dwMCRNumber的值加1,这样到循环结束时他的值回是循环的次数,同时也是地址范围描述符结构的个数。

在保护模式下的32位代码段添加显示内存信息的过程。


305	DispMemSize:
306		push	esi
307		push	edi
308		push	ecx
309
310		mov	esi, MemChkBuf
311		mov	ecx, [dwMCRNumber];for(int i=0;i<[MCRNumber];i++)//每次得到一个ARDS
312	.loop:				  ;{
313		mov	edx, 5		  ;  for(int j=0;j<5;j++) //每次得到一个ARDS中的成员
314		mov	edi, ARDStruct	  ;  {//依次显示BaseAddrLow,BaseAddrHigh,LengthLow,
315	.1:				  ;             LengthHigh,Type
316		push	dword [esi]	  ;
317		call	DispInt		  ;    DispInt(MemChkBuf[j*4]); //显示一个成员
318		pop	eax		  ;
319		stosd			  ;    ARDStruct[j*4] = MemChkBuf[j*4];
320		add	esi, 4		  ;
321		dec	edx		  ;
322		cmp	edx, 0		  ;
323		jnz	.1		  ;  }
324		call	DispReturn	  ;  printf("\n");
325		cmp	dword [dwType], 1 ;  if(Type == AddressRangeMemory)
326		jne	.2		  ;  {
327		mov	eax, [dwBaseAddrLow];
328		add	eax, [dwLengthLow];
329		cmp	eax, [dwMemSize]  ;    if(BaseAddrLow + LengthLow > MemSize)
330		jb	.2		  ;
331		mov	[dwMemSize], eax  ;    MemSize = BaseAddrLow + LengthLow;
332	.2:				  ;  }
333		loop	.loop		  ;}
334					  ;
335		call	DispReturn	  ;printf("\n");
336		push	szRAMSize	  ;
337		call	DispStr		  ;printf("RAM size:");
338		add	esp, 4		  ;
339					  ;
340		push	dword [dwMemSize] ;
341		call	DispInt		  ;DispInt(MemSize);
342		add	esp, 4		  ;
343
344		pop	ecx
345		pop	edi
346		pop	esi
347		ret


书上把注释直接用c语言表达了!!

程序的主题是一个循环,循环的次数为地址范围描述符结构(上下文用ARDStruct代替)的个数,每次循环将会读取一个ARDStruct。

首先打印其中每一个成员的各项,然后根据当前结构的类型,得到可以被操作系统使用的内存的上限。结果会被存放在变量dwMemSize中,并在此模块的最后打印到屏幕。

注释里面的DispInit 和 DIspStr等函数。是显示整形数字和字符串。 它们连同函数DispAL、DispReturn被放在了lib.inc中,使用%include “lib.inc”

lib.inc 代码如下:


1  	;; lib.inc
2  
3  	;; 显示 AL 中的数字
4  	DispAL:
5  		push	ecx
6  		push	edx
7  		push	edi
8  
9  		mov	edi, [dwDispPos]
10 
11 		mov	ah, 0Fh			; 0000b: 黑底    1111b: 白字
12 		mov	dl, al
13 		shr	al, 4
14 		mov	ecx, 2
15 	.begin:
16 		and	al, 01111b
17 		cmp	al, 9
18 		ja	.1
19 		add	al, '0'
20 		jmp	.2
21 	.1:
22 		sub	al, 0Ah
23 		add	al, 'A'
24 	.2:
25 		mov	[gs:edi], ax
26 		add	edi, 2
27 
28 		mov	al, dl
29 		loop	.begin
30 		;add	edi, 2
31 
32 		mov	[dwDispPos], edi
33 
34 		pop	edi
35 		pop	edx
36 		pop	ecx
37 
38 		ret
39 	;; DispAL 结束
40 
41 
42 	;; 显示一个整型数
43 	DispInt:
44 		mov	eax, [esp + 4]
45 		shr	eax, 24
46 		call	DispAL
47 
48 		mov	eax, [esp + 4]
49 		shr	eax, 16
50 		call	DispAL
51 
52 		mov	eax, [esp + 4]
53 		shr	eax, 8
54 		call	DispAL
55 
56 		mov	eax, [esp + 4]
57 		call	DispAL
58 
59 		mov	ah, 07h			; 0000b: 黑底    0111b: 灰字
60 		mov	al, 'h'
61 		push	edi
62 		mov	edi, [dwDispPos]
63 		mov	[gs:edi], ax
64 		add	edi, 4
65 		mov	[dwDispPos], edi
66 		pop	edi
67 
68 		ret
69 	;; DispInt 结束
70 
71 	;; 显示一个字符串
72 	DispStr:
73 		push	ebp
74 		mov	ebp, esp
75 		push	ebx
76 		push	esi
77 		push	edi
78 
79 		mov	esi, [ebp + 8]	; pszInfo
80 		mov	edi, [dwDispPos]
81 		mov	ah, 0Fh
82 	.1:
83 		lodsb
84 		test	al, al
85 		jz	.2
86 		cmp	al, 0Ah	; 是回车吗?
87 		jnz	.3
88 		push	eax
89 		mov	eax, edi
90 		mov	bl, 160
91 		div	bl
92 		and	eax, 0FFh
93 		inc	eax
94 		mov	bl, 160
95 		mul	bl
96 		mov	edi, eax
97 		pop	eax
98 		jmp	.1
99 	.3:
100		mov	[gs:edi], ax
101		add	edi, 2
102		jmp	.1
103
104	.2:
105		mov	[dwDispPos], edi
106
107		pop	edi
108		pop	esi
109		pop	ebx
110		pop	ebp
111		ret
112	;; DispStr 结束
113
114	;; 换行
115	DispReturn:
116		push	szReturn
117		call	DispStr			;printf("\n");
118		add	esp, 4
119
120		ret
121	;; DispReturn 结束
122
123

在DispInt中,[esp+4]即为已经入栈的参数,函数通过4次对DispAL的调用显示了一个整数,并且最后显示一个灰色的字母“h”。

函数DispStr通过一个循环来显示字符串,每一次复制一个字符入显存,遇到\0则结束循环。

同时,DispStr加入了对回车的处理,遇到0Ah就会从下一行的开始处继续显示。由于这一点,DispReturn也做了简化,通过DispStr来处理回车。

以前我们用edi来保存当前的显示位置,现在我们要改用变量dwDispPos来保存。这样我们放心的使用edi寄存器了!

需要提醒的是,在数据段中,几乎每个变量都有类似的符号

_dwMemSize: dd 0dwMemSize equ _dwMemSize - $$

在实模式下应使用_dwMemSize,在保护模式下使用dwMemSize。

因为程序是在实模式下编译的,地址只适用于实模式,在保护模式下,数据的地址应该是其相对于段基址的偏移!

代码调用了DispMemSize,在调用之前还显示了一个字符串要打印内存信息的表格头!


238		push	szMemChkTitle
239		call	DispStr
240		add	esp, 4
241
242		call	DispMemSize		; 显示内存信息

编译运行!

从图中,可以看出,总共5段内存被列出来了!

我们可以看到,操作系统能使用的最大内存地址为01FFFFFFh,所以此机器有32MB的内存。所以我们制定的256字节的内存MemChkBuf是够用的!虚拟机可以设置32MB 内存!

得到内存容量除了大小,还有可用内存的分布信息。

因为历史原因,估计是兼容吧,系统可用的内存分布并不连续,所以在使用的时候,我们要根据得到的信息小心行事!

内存容量得到了,我们得到内存是为了节约使用,不再初始化所有的PDE和所有页表了,我们可以根据内存的大小计算应该初始化多少PDE 以及 多少页表!

修改SetupPaging 代码:


249	; 启动分页机制 --------------------------------------------------------------
250	SetupPaging:
251		; 根据内存大小计算应初始化多少PDE以及多少页表
252		xor	edx, edx
253		mov	eax, [dwMemSize]
254		mov	ebx, 400000h	; 400000h = 4M = 4096 * 1024, 一个页表对应的内存大小
255		div	ebx
256		mov	ecx, eax	; 此时 ecx 为页表的个数,也即 PDE 应该的个数
257		test	edx, edx
258		jz	.no_remainder
259		inc	ecx		; 如果余数不为 0 就需增加一个页表
260	.no_remainder:
261		push	ecx		; 暂存页表个数
262
263		; 为简化处理, 所有线性地址对应相等的物理地址. 并且不考虑内存空洞.
264
265		; 首先初始化页目录
266		mov	ax, SelectorPageDir	; 此段首地址为 PageDirBase
267		mov	es, ax
268		xor	edi, edi
269		xor	eax, eax
270		mov	eax, PageTblBase | PG_P  | PG_USU | PG_RWW
271	.1:
272		stosd
273		add	eax, 4096		; 为了简化, 所有页表在内存中是连续的.
274		loop	.1
275
276		; 再初始化所有页表
277		mov	ax, SelectorPageTbl	; 此段首地址为 PageTblBase
278		mov	es, ax
279		pop	eax			; 页表个数
280		mov	ebx, 1024		; 每个页表 1024 个 PTE
281		mul	ebx
282		mov	ecx, eax		; PTE个数 = 页表个数 * 1024
283		xor	edi, edi
284		xor	eax, eax
285		mov	eax, PG_P  | PG_USU | PG_RWW
286	.2:
287		stosd
288		add	eax, 4096		; 每一页指向 4K 的空间
289		loop	.2
290
291		mov	eax, PageDirBase
292		mov	cr3, eax
293		mov	eax, cr0
294		or	eax, 80000000h
295		mov	cr0, eax
296		jmp	short .3
297	.3:
298		nop
299
300		ret
301	; 分页机制启动完毕 ----------------------------------------------------------
302

在函数开头,我们用内存的大小除以4MB 得到应初始化的PDE的个数(同时也是页表的个数)。在初始化页表的时候,通过刚刚计算出的页表个数诚意1024(每个页表含1024个PTE)得出要填充的PTE个数,然后通过循环完成对它的初始化。

这样一来,页表所占的空间就小的多,在本例子中,32MB的内存实际上只要32Kb的页表就够了,所以在GDT中,这样初始化页表段:

20 LABEL_DESC_PAGE_TBL: Descriptor PageTblBase, 4096 * 8 - 1, DA_DRW ; Page Tables

这样程序所需的内存空间就小了很多!