您的位置:首页 > 其它

一个操作系统的实现(7)-获取机器内存并进行合理分页

2016-05-29 18:36 302 查看
在前面的程序中,我们用了4MB的空间来存放页表,并用它映射了4GB的内存空间,而我们的物理内存不见得有这么大,这显然是太浪费了。如果我们的内存总数只有16MB的话,只是页表就占用了25%的内存空间。而实际上,如果仅仅是对等映射的话,16MB的内存只要4个页表就够了。所以,我们有必要知道内存有多大,然后根据内存大小确定多少页表是够用的。而且,一个操作系统也必须知道内存的容量,以便进行内存管理。


克勤克俭用内存

这里利用中断
15h
来获取计算机的内存。

在调用中断15h之前,我们需要填充下列寄存器:

e a x
 int 15h可完成许多工作,主要由ax的值决定,我们想要获取内存信息,需要将ax赋值为0E820h。

e b x
 放置着“后续值(continuation value)”,第一次调用时ebx必须为0。

e s : d i
 指向一个地址范围描述符结构ARDS(Address Range Descriptor Structure),BIOS将会填充此结构。

e c x
 es:di所指向的地址范围描述符结构的大小,以字节为单位。无论es:di所指向的结构如何设置,BIOS最多将会填充ecx个字节。不过,通常情况下无论ecx为多大,BIOS只填充20字节,有些BIOS忽略ecx的值,总是填充20字节。

e d x
 0534D4150h(‘SMAP’)──BIOS将会使用此标志,对调用者将要请求的系统映像信息进行校验,这些信息会被BIOS放置到es:di所指向的结构中。

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

C F
 CF=0表示没有错误,否则存在错误。

e a x
 0534D4150h(‘SMAP’)。

e s : d i
 返回的地址范围描述符结构指针,和输入值相同。

e c x
 BIOS填充在地址范围描述符中的字节数量,被BIOS所返回的最小值是20字节。

e b x
 这里放置着为等到下一个地址描述符所需要的后续值,这个值的实际形势依赖于具体的BIOS的实现,调用者不必关心它的具体形式,只需在下次迭代时将其原封不动地放置到ebx中,就可以通过它获取下一个地址范围描述符。如果它的值为0,并且CF没有进位,表示它是最后一个地址范围描述符。

上面提到的地址范围描述符结构(Address Range Descriptor Structure)如下表所示:
偏移名称意义
0BaseAddrLow基地址的低32位
4BaseAddrHigh基地址的高32位
8LengthLow长度(字节)的低32位
12LengthHigh长度(字节)的高32位
16Type这个地址范围的地址类型
其中,Type的取值及其意义如下表所示:
取值名称意义
1AddressRangeMemory这个内存段是一段可以被OS使用的RAM
2AddressRangeReserved这个地址段正在被使用,或者被系统保留,
所以一定不要被OS使用
其他未定义保留,为未来使用,任何其他置都必须被OS
认为是AddressRangeReserved
由上面的说明我们看出,ax=0E820h时调用int 15h得到的不仅仅是内存的大小,还包括对不同内存段的一些描述。而且,这些描述都被保存在一个缓冲区中。所以,在我们调用int 15h之前,必须先有缓冲区。我们可以在每得到一次内存描述时都使用同一个缓冲区,然后对缓冲区里的数据进行处理,也可以将每次得到的数据放进不同的位置,比如一块连续的内存,然后在想要处理它们时再读取。后一种方式可能更方便一些,所以在这里定义了一块256字节的缓冲区(代码第65行),它最多可以存放12个20字节大小的结构体。我们现在还不知道它到底够不够用,这个大小仅仅是凭猜测设定。我们将把每次得到的内存信息连续写入这块缓冲区,形成一个结构体数组。然后在保护模式下把它们读出来,显示在屏幕上,并且凭借它们得到内存的容量。

下面是调用中断15h的代码:


可以看到,代码使用了一个循环,一旦CF被置位或者ebx为零,循环将结束。在第一次循环开始之前,eax为0000E820h,ebx为0,ecx为20,edx为0534D4150h,es:di指向_MemChkBuf的开始处。在每一次循环进行时,寄存器di的值将会递增,每次的增量为20字节。另外,eax、ecx和edx的值都不会变,ebx的值我们置之不理。同时,每次循环我们让_dwMCRNumber的值加1,这样到循环结

束时它的值会是循环的次数,同时也是地址范围描述符结构的个数。

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


对照右边注释中的C代码,可以很容易了解这段代码的目的:程序的主题是一个循环,循环的次数为地址范围描述符结构(下文用ARDStruct代替)的个数,每次循环将会读取一个ARDStruct。首先打印其中每一个成员的各项,然后根据当前结构的类型,得到可以被操作系统使用的内存的上限。结果会被存放在变量dwMemSize中,并在此模块的最后打印到屏幕。

其中,

DispInt
函数定义如下:


DispStr
定义如下:


DispInt和DispStr函数连同DispAL、DispReturn被放在了
lib.inc
中,并且通过如下语句包含进pmtest7.asm中:


这与直接把代码写进这个位置的效果是一样的,把他们单独放到一个文件有利于阅读。

在DispInt中,[esp+4]即为已经入栈的参数,函数通过4次对DispAL的调用显示了一个整数,并且最后显示一个灰色的字母“h”。函数DispStr通过一个循环来显示字符串,每一次复制一个字符入显存,遇到\0则结束循环。同时,DispStr加入了对回车的处理,遇到0Ah就会从下一行的开始处继续显示。由于这一点,DispReturn也做了简化,通过DispStr来处理回车。

在以前的程序中,我们用edi保存当前的显示位置,从这个程序开始,我们改为用变量dwDispPos来保存。这样我们就可以放心地使用edi这个寄存器。

至此,我们新增的内容已经准备得差不多了,另外还需要提到的一点是,在数据段中,几乎每个变量都有类似的两个符号,比如:





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

接下来就是调用DispMemSize来显示内存信息啦:


在调用DispMemSize之前,我们显示了一个字符串作为将要打印的内存信息的表格头。现在来看看结果:





上面的结果图,总共有六段内存被列出来,对列出的内存情况解释如下表:
内存段属性是否可被OS使用
00000000h~0009EFFFhAddressRangeMemory
0009F000h~0009FFFFhAddressRangeReserved不可
000E8000h~000FFFFFhAddressRangeReserved不可
00100000h~01FEFFFFhAddressRangeMemory
01FF0000h~01FFFFFFh未定义不可
FFFC0000h~FFFFFFFFhAddressRangeReserved不可
从上面可以看出,操作系统能够使用的最大内存地址是01FEFFFFh,所以此极其拥有32MB-16KB的内存。而且幸运的是,我们指定的256字节的内存MemChkBuf是够用的。

你可能没有想到,得到内存容量还要这么多代码,不过,实际上我们除了得到了内存的大小,还得到了可用内存的分布信息。由于历史原因,系统可用内存分布得并不连续,所以在使用的时候,我们要根据得到的信息小心行事。

内存容量得到了,你是否还记得我们为什么要得到内存?我们是为了节约使用,不再初始化所有PDE和所有页表。现在,我们已经可以根据内存大小计算应初始化多少PDE以及多少页表,下面来修改一下函数SetupPaging。


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

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


这样,程序所需的内存空间就小了许多。


源代码



lib.inc
:

                                            
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息