克勤克俭用内存
前面程序中,用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
这样程序所需的内存空间就小了很多!