克勤克俭用内存
分页的益处其实体现在多个方面,以下举例,先有个初步认识!
如果你写一个程序(在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 0
和 dwMemSize 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
这样程序所需的内存空间就小了很多!