X86汇编语言-从实模式到保护模式—笔记(48)-第16章 分页机制和动态页面分配(3)
16.4 创建内核任务【16.4.1 内核的虚拟内存分配】
接下来的工作是使内核的一部分成为任务,并为创建用户任务和实施任务切换做准备。
先是创建内核任务的任务状态段(TSS)。内核的主题部分占据着从线性地址0x80000000开始的1MB内存空间,即0x80000000~
0x800FFFFF,在此之后的空间,即0x80100000~0xFFFFFFFF,是可以自由分配的。
为了连续地、动态地分配内核的空间,内核需要记住下一个可用于分配的线性地址。为此,第529行,专门声明了标号core_next_laddr,并初始化了一个双字0x80100000,这就是初始的可分配线性地址。每当分配了新的内存空间后,该双字将修正为下一个可分配的地址。
在分页机制下,内存的分配既要在虚拟内存空间中进行,还要在页目录表和页表中进行。因为线性地址最终要通过页目录表和页表转换成物理地址,如果没有分配一个物理页,对任何内存的访问都是无效的,会引发处理器异常中断。
第1026行,先访问内核数据段,从标号处取得当前可用的线性地址,将来作为内核TSS的起始线性地址。然后,将EBX寄存器中的线性地址作为参数,调用过程alloc_inst_a_page去申请一个物理页。过程转入第358行,它的功能是在可用的物理内存中搜索空闲的页,并将它安装在当前的层次化分页结构中(页目录和页表)。简单地说,就是寻找一个可用的页,然后,根据线性地址来创建页目录项和页表项,并将页的地址填写在页表项中。【DS指向0-4GB段】过程传入的参数由寄存器EBX传递,EBX=0x80100000。二进制形式 EBX=1000 0000 0001 0000 0000 0000 0000 0000;第370行,将寄存器EBX的值传送给寄存器ESI,ESI=0x80100000,EBX留下来作为备份;第371行,将寄存器ESI中的高10位保留,其余位清零。二进制形式ESI=1000 0000 0000 0000 0000 0000 0000 0000;第372行,寄存器ESI中的值逻辑右移22位,再逻辑左移2位,实际上就是逻辑右移20位,意思就是得到高10位的索引值,再乘以4,这就是页目录表中与线性地址对应的那个页目录项所在的偏移。ESI=0000 0000 0000 0000 0000 1000 0000 0000,0x800。第373行,页目录自身的线性地址0xFFFFF000加上偏移值,指向对应的页目录项。最终ESI的值为:0xFFFFF800;第375行,以ESI为偏移,以线性地址0x00000000位基地址,也就是—处取出页目录项的值,用test指令检测第1位,也就是P位是否为1,如果P位为1,则说明该页目录项对应的页表位于内存中。只需要找到页表并在其中添加一项即可。否则,表示页表不在内存中,必须先予以创建,或者从磁盘调入内存,并填写页目录项后方可使用。在这里要注意:尽管我们给出的就是线性地址,但是,那不是处理器段部件产生的线性地址,第366、367行,我们令段寄存器DS指向0-4GB的内存段(基地址是x00000000),此后,当我们用给出的“线性地址”作为段内偏移量访问内存,段部件才会输出真正的线性地址,尽管两者是相同的。总之,处理器的段管理机制是始终存在的,没有任何一种方法可以关闭它。
第376行,判断第375行的结果是否为零,如果为零,则说明页表不存在,需要创建页表;如果不为零,则说明页表存在,只需要找到页表,并在其中添加相关项目即可。
【新知识点:】
尽管每个任务都拥有4GB虚拟内存空间,也可以自由分配这些空间,但是,物理内存是有限的,或者用页的视角来书,物理页的数量是有限的。
操作系统必须在刚刚获得计算机控制权的时候,就检测实际的物理内存数量,并建立一张表格,标明页的物理地址及其是否空闲。当有程序申请内存时,就寻找这样的空闲页,并将其标记为已分配。
如果你的计算机真的有4GB物理内存,那么,它可以划分为1048576(2^20)个页。如果每个表项占一字节,则需要1MB内存来创建该表。显然,这有些不划算。为了简单,可以使用位串来指示页的分配情况。可以用一个长的比特串,叫做页映射位串,来指示每个页的位置及其分配情况。取决于你所拥有的实际内存数量(页数),该串最多可以有1048576比特,由于每字节包含8个比特,所以,共需要131072字节,也就是128KB。
比特在位串中的位置,决定了它所映射的页在哪里。位0对应的是物理地址为0x00000000的页,位1对应的是物理地址0x00010000的页,位2对应的是0x00020000的页,……,最后一个比特对应的是最后一个页,即物理地址为0xFFFFF000的页。
除了用比特所在的位置决定页的位置外,比特的值决定页的分配情况。当某比特为“0”时,表示它所对应的页未分配,是可以分配的空闲页;否则,就表明那个页已经被占用了,不能再分配给任何程序。
本章假设只有2MB物理内存可用。2MB的内存,可分为512个页,需要512个比特的位串,也就是64个字节。在实际的程序中,没有声明位串的方法,只有声明字节、字、双字。因此,只能用连续的字节或字数据来形成位串。
第465行,声明了标号page_bit_map,并初始化了64字节的数据。这64字节首尾相连,形成一个512比特的位串。前32字节的值差不多都是0xFF。这并不奇怪,它们对应着最低端1MB内存的那些页(256个页),它们已经整体上划归内核使用了,没有被内核占用的部分多数也被外围硬件占用了,比如ROM-BIOS。当然,也会发现其中混杂了两字节的0x55。这是因为在物理地址0x00030000—0x00040000之间,是一段较为连续的空闲区,共64KB,可划分16个页,页的物理地址为0x00030000—0x00040000,就对应着这两字节。本来,这两字节都应当是0x00,以表明是可以分配的空闲页。不过,为了表明大的、连续的线性地址空间不必对应着连续的页,我们有意将空闲的页在物理上分开,因为0x55的二进制形式为01010101。同样的做法也出现在后面的64个页中。【这么做的前提是已经保证了这16个页内核是不会用到的】
进入过程allocate_a_4k_page。
第327-330行,压栈保护相关寄存器的值;第332、333行,令DS指向内核数据段;第335行,寄存器EAX清零,表示从第0个索引开始搜索。第337行,以EAX为序号(从0开始),将起始地址为标号page_bit_map处的页映射位串从第0位开始进行搜索测试,使用的是bts指令,该指令将指定位置处(由寄存器EAX提供)的比特位送到标志寄存器的CF位后,再将该比特位置1。第338行,根据CF的值决定是否进行跳转。如果CF=1,则说明刚测试的比特位的值为1,也就说明该比特位代表的页已经被使用,那么,则跳转条件不成立,顺序往下执行,第339行,寄存器EAX自加1,指向要测试的下一个位置;第340行,将寄存器EAX的值与页映射位串的总长度向比较,这个页映射位置的总长度由标号page_map_len给出,因为它是以字节为单位的,而寄存器EAX中给出的是以比特为单位计数的,因此要将标号处的值乘以8。第341行,根据比较的结果决定是否跳转,如果EAX中的值小于页映射位串的总长度(最后一个比特总是总长度乘以8,再减去1,因为计数是从零开始的,所以如果当EAX的值等于或者大于总长度数值的时候,那就越界了),代表没有越界,程序跳转循环执行。如果一直都没有找到可以使用的页(也就是说CF的值一直为1),那么程序来到第343行,第343、344行,显示标号message_3处的信息:“********No more pages********”,意思是没有更多的页,然后第345行,处理器停机。程序到此就结束了。
刚才我们一直讨论的是没有找到空闲页,也就是测试的比特位一直都为1的情况,现在我们再回到第338行,如果CF=0,则说明刚测试的比特位的值为0,也就说明该比特位代表的页是空闲页,那么,程序跳转条件成立,跳转到第348行,将EAX这个索引值左移12位,也就是乘以0x1000,这就是该比特位代表的空闲的页。过程最终将返回分配好的空闲页的起始物理地址。第350—353行,过程恢复现场,第355行,程序返回,这是一个段内过程,短返回,仅限段内其他过程调用。
【16.4.3 创建页表并登记分配的页】
程序返回到第380行,此时寄存器EAX中保存的刚刚分配好的页的物理地址。这个页将作为一个页表。页表的地址要登记在页目录表内,仅高20位有效,对应着页表物理地址的高20位,页表地址的低12位是页表属性。从第380行可以看出,我们给出的页表的属性是0x00000007,最低3位均为1,也就是说P=1,说明页已经位于内存中,可以使用;RW=1,页是可读可写的;US=1,特权级别为3的程序也可以访问。内核的页表为什么允许特权级别为3的程序访问呢?当然了,原则上是不允许的,但是,这个例程既要用于为内核分配页面,也要用于为用户任务分配页面。对于前者,要求将所分配页面的U/S位清零;对于后者,要求将所分配页面的U/S位置“1”,这两者难以兼顾。为了不把事情搞复杂而又能说明问题,用当前过程所分配的页面,US位一概设置成“1”。刚分配的页是作为页表使用的,它应当登记在页目录表内,作为目录项存在。现在ESI寄存器中的内容就是该目录项的线性地址。第381行,将刚分配的页(作为页目录项-即页表)的物理地址填入ESI指向的页目录项内。
不管页表是原来就有的(也就是说程序第376行跳转条件成立),还是我们刚刚分配并创建的(也就是说程序第376行跳转条件不成立,顺序执行而来的),程序的执行流程最终会到达第385行,过程有一个输入参数,那就是寄存器EBX的值,它代表拟分配的页的线性地址,因为等下来要对EBX中的值做一些处理,所以这里拷贝一个副本到寄存器ESI中,此时寄存器EBX的值仍然是过程开始时由过程外部传入的值,未发生过变化。
因为下面我们是要修改页表内的页表项,所以,无论如何,必须要知道该页表项的线性地址才行,可以分几个步骤来完成:
首先,ESI寄存器中的线性地址,其高10位决定了页表在页目录表中的登记位置;中间10位,决定页在页表中登记位置。很显然,要访问页表,就得把页表当成普通页来访问。如此一来,那个页表项在页表中的位置,就相当于数据在页中的位置。为此,应当把ESI寄存器的中间10位乘以4之后,挪到该寄存器的低12位,作为页内偏移量;
其次,既然把页表作为普通的页来对待,那么,页部件势必要先访问该“页”的“页表”。页表的物理地址是登记在页目录表中的,它在页目录表中的位置由ESI寄存器的高10位指定,因此,就得把页目录表当成该“页”的“页表”来用,并把ESI寄存器的高10位挪到中间10位上,作为页表项的索引号。
最后,为了将页目录表作为页表来用,要将ESI寄存器的高10位置成0x3FF。这意味着,页目录表内最后一个目录项就是页表的物理地址。又因为该目录项指向页表自身,故,等于是又把页目录表当成页表来用。至此,任务完成,ESI寄存器中得到的新值,就是要修改的那个页表项的线性地址。
第386行,将寄存器ESI中的值逻辑右移10位。这样高10位就到了中间10位了;第387行,将高10位、低12位清零,这样就将线性地址的原来的高10位保留下来并移到中间10位,用来作为访问“页表”内的页项。第388行,将ESI中的高10位全部赋值1。这样本行执行完之后,ESI二进制形式为:1111111111_1000000000_000000000000。这个数值指向了页表,也就是页表的线性地址,只不过这里把这个页表当成了页了。高10位的值为0x3FF,乘以4后等于0xFFC,它指向页目录表内最后一个目录项,而这个目录项又指向页目录表自己,因此此时页目录表又变成了页表。然后用中间10位—值为0x200,乘以4得到0x800,这是页目录表内(此时被当做页表)偏移0x800的目录项,该目录项在前面我们已经填入了页表的物理地址0x00021000,也就是是说此时处理器已经找到那个页表,现在我们要找到页表中的某个项,并在其中填入一个页的物理地址,因为我们现在是把页表当成页来用,所以要找的那个页表的项的偏移,就是本过程传入的线性地址的中间10位乘以4。所以,第391行,将传入的线性地址的中间10位保留,其余位全部清零;第392行,将其右移10位(相当于右移12位后,再乘以4),这个就是真正的偏移值。第393行,将刚刚得到的页表的线性基地址加上这个偏移量,就是我们要修改的页表内的页项的位置。第394行,调用过程allocate_a_4k_page,申请到一个页面后,返回该页面的物理地址,该物理地址在寄存器EAX中;第395行,为刚刚申请的页添加属性值0x007;第396行,将该页的物理地址填入页表项内。至此,给出一个起始的线性地址,分配一个页,并登记在层次化的分页结构中,任务完成。第403行,retf返回到调用者,远返回。
【16.4.4 创建内核任务的TSS】
在为系统内核的任务状态段TSS分配了虚拟地址空间和页之后,第1028行,将标号core_next_laddr处的数据修改为下一个可分配的起始线性地址。下一次在内核的虚拟地址空间里分配内存时,将使用这个新值作为起始的线性地址。
第1031—1038行,填写和初始化TSS中的静态部分,有些内容,比如CR3寄存器域,对任务的执行来说很关键,必须事先予以填写。一旦分配了物理页,并填写了页目录项和页表项,就立即可以用那个线性地址来访问内存。此时,整个过程是相反的,页部件用那个线性地址访问页目录表和页表,生成物理地址。还有,尽管你在指令中给出的确实是线性地址,但并非是由段部件生成的线性地址。在Intel处理器上,段机制是无法关闭的,因此,你必须使用0-4GB的段,加上你的线性地址,才得使段部件生成真正的线性地址,尽管两个线性地址在数值上没有任何不同。因此,要访问TSS,必须通过段寄存器ES所指向的0-4GB数据段。第1031行,此时寄存器EBX的值是线性地址数值0x80100000,通过ES段寄存器后,它变成了真正的线性地址0x80100000,此时它才真正成为TSS的起始地址。第1031行,将反向链设置为0,没有前面的任务;第1033行,将CR3的值传送给寄存器EAX,注:CR3寄存器的值我们已经在第962行给它赋值了,它的值就是0x00020000,是页目录表的起始物理地址;第1034行,将CR3的值登记到TSS内偏移为28的地方,也就是存放PDBR的地方;第1036行,TSS内偏移96的地方登记LDT段选择子,这里登记为0,表示没有LDT。处理器允许没有LDT的任务;第1037行,TSS内偏移100的地方,是T位—软件调试位,此处设置为0;第1038行,TSS内偏移102的地方是登记I/O映射基地址的位置,此处登记为103,因此该字单元的值小于等于TSS的段界限,表明没有I/O映射(或者叫I/O位图)。当然,本来这里就是内核的任务,就是0特权级,也需要所谓I/O位图。
登记完内核任务状态段TSS中的必要项目后,就要开始创建TSS的段描述符。调用创建段描述符的过程需要三个参数,第1041行,将TSS的起始线性地址(在EBX中)传送给寄存器EAX;第1042行,将TSS的段界限(段界限=段的实际长度减1,也就是104-1=103,即从0开始最后一个字节的偏移值)给寄存器EBX;第1043行,将TSS段描述符的属性值给寄存器ECX,这里任务状态段TSS描述符的属性值为0x00408900,这是一个0特权级的TSS描述符;第1044行,调用过程生成TSS描述符,返回值在EDX:EAX中,EDX中放置TSS描述符的高32位,EAX中放置描述符的低32位;第1045行,调用过程在GDT中安装TSS描述符,返回值在CX中,为TSS的选择子;第1046行,将安装好的TSS的选择子的值填入内核数据段标号program_man_tss的高16位处;第1050行,加载TSS的选择子到任务寄存器TR中,到这里就可以认为程序管理器的任务正在执行了,由于当前任务事实上正处于运行中,因此,只要后补手续即可使其完全合法。
16.5 用户任务的创建和切换
【16.5.1 多段模型和段页式内存管理】
在保护模式下,首先按程序的结构分段,创建各个段的描述符,用描述符指向物理内存中的各个段。描述符中的基地址给出了段的起始物理地址,界限值给出了段的长度(边界),属性值指示了段的类型和特权级别等性质。
开启了页功能后,段是在自己的虚拟地址空间内分配的,而不是在物理内存中分配的。因此,段描述符中的基地址是段的线性地址,或者说是虚拟地址。再经过页部件转换成物理地址。
分段主要是从8086处理器开始的,因为当时只有16根地址线,为了访问1M内存地址,所以而采取的分段功能。
现在,32位的处理器拥有32根地址线和32/64根数据线,这使得它不用将4GB或多于4GB的内存空间划分成多个段,就能完全控制它。如此一来,软件设计者就会倾向于不分段。当然,程序的浮动和重定位将不可能再依赖于分段机制,但并不是没有其他的办法。
【16.5.2 平坦模型和用户程序的结构】
不分段的内存管理模型称为平坦模型(Flat Model)。尽管说是不分段,但我们千万不要信以为真,分段是Intel处理器的固有机制,处理器总是按“段地址+偏移量”来形成线性地址,不可能绕开这种工作机制。
因此,所谓的平坦模型,就是将全部4GB内存整体上作为一个大段来处理,而不是分成小的区块。在这种模型下,所有段都是4GB,每个段的描述符都指向4GB的段,段的基地址都是0x00000000,段界限都是0xFFFFF,粒度为4KB。
在这种基本的平坦模式下,程序在编写的时候不分段,即,只保留一个段,代码和数据都在这个段内,相互邻接,但一般并不交叉。很显然,在这种模式下,不能享受到段保护机制的好处,段界限和数据访问的检查仍然进行,但从不会产生违例的情况。原因很简单,每个段描述符的基地址都是0,实际使用的段界限都是0xFFFFFFFF,就任务内的地址空间而言,对任何内存位置的访问都是合法的。
【16.5.3 用户任务的虚拟地址空间分配】
一般来说,所有任务的TCB都应当占用内核的地址空间,在内核的虚拟地址空间里分配。任务都是由内核负责管理和调度的,如果TCB位于任务自己的地址空间里,而不是内核的地址空间里,那么,这同时也意味着,在内核的页目录表和页表中,没有指向TCB所在页的表项,内核不可能访问到它。
第1055-1057行,用于在内核的虚拟地址空间里分配4KB的内存(页),这和上一次在内核中分配内存的做法是一样的。
第1059-1062行,初始化TCB,位某些域赋初值。任务控制块TCB的结构和上一章相比大大简化,只保留了少数项目。在TCB中,有两个项目应该在创建用户任务前就予以填写和初始化,它们是LDT当前界限值和下一个可用的线性地址。 LDT当前界限值应该被初始化为0xFFFF,这是计算机启动时,LDTR寄存器中的默认界限值,LDTR中的界限部分只有16位。LDT的界限是LDT的长度减1,LDT的初始长度为0,因此,其界限值是0xFFFF。
每个任务都有自己的4GB虚拟内存空间,线性地址范围是0x00000000—0xFFFFFFFF,它是任务自己的空间,可以任意分配和使用。当然,实际可以使用的空间是前2GB,后2GB被任务的全局部分占用,映射并指向内核的页表。一般来说,第一个可以分配的线性地址是0x00000000,要把这个数值填写到TCB的“下一个可用线性地址”域中。
第1062行,按照本章的加载过程来设置任务控制块的内容,较上一章较为简单了。注意,在TCB中,有两个项目应该在创建用户任务前就予以填写和初始化,它们是LDT当前界限和下一个可用的线性地址。LDT当前界限值应该被初始化为0xFFFF,这是计算机启动时,LDTR寄存器中的默认值,LDTR中的界限部分只有16位。LDT的界限是LDT的长度减1,LDT的初始长度为0,因此,其界限值是0xFFFF。
【16.5.4 用户程序的加载】
在内核页目录表和页表中的低端部分,建立用户程序的页目录表和页表,然后将整个也目标拷贝到用户程序的页目录表中,这样切换到任务的时候,用户程序就有了自己的页目录表,这样程序就能正常运行。【这是中心思想】
【16.5.5 段描述符的创建(平坦模型)】
平坦模型下的段描述符创建简单得多,段的基地址都是0x00000000,段界限值都是0xFFFFF,粒度都是4KB。
用户程序要单独创建0、1、2特权级的栈,可以在数据段内创建,向上扩展和向下扩展并不影响栈的使用,只是影响特权级的检查,但这里采用平坦模式后,特权级的检查都会通过,因此可以直接在数据段里面使用虚拟内存创建栈(当然要分配具体的物理页,并安装到页机制的层级结构中)
【16.5.6 重定位U-SALT并复制页目录表】
除了地址使用虚拟的线性地址外,其他部分都是一样的,最后是完善TSS的各个部分的数据,为任务切换做好相关准备。
【16.5.7 切换到用户任务执行】
这个相对简单,用CALL进行切换,返回时判断是用CALL还是JMP发起的,再区别返回。
页:
[1]