兰陵月 发表于 2017-12-15 12:12:07

X86汇编语言-从实模式到保护模式—笔记(42)-第14章 任务和特权级保护(8)

本帖最后由 兰陵月 于 2017-12-15 12:19 编辑

14.4 加载用户程序并创建任务
【14.4.1 任务控制块和TCB链】第832、833行是以传统的方式调用内核例程显示字符串。即使不通过调用门,特权检查也是照常进行的,而且更为严格。把控制从较低的特权级转移到较高的特权级,通过调用门尚有可能,但直接控制转移则在任何时候都是不允许的。当然,这两行是从0特权级的内核代码段进入同样0特权级的公共例程段,能够通过特权级检查。按照处理器的要求标准,要使一个程序成为“任务”,并且能够参与任务切换和调度,那不是简简单单就行的,必须要有LDT和TSS。而为了创建这两样东西,又需要更多的东西。所以,加载和执行用户程序的活儿,比起从前是麻烦了不少。加载程序并创建一个任务,需要用到很多数据,比如程序的大小、加载的位置,等等。当任务执行结束,还要依据这些信息来回收它所占用的内存空间。还有,多任务系统是个多个任务同时运行的,特别是在一个单处理器(核)的系统中,为了在任务之间切换和轮转,必须能追踪到所有正在运行的的任务,记录它们的状态,或者根据它们的当前状态来采取适当的操作(比如切换和轮转)。为了满足以上的要求,内核应当为每一个任务创建一个内存区域,来记录任务的信息和状态,称为任务控制块(Task Control Block,TCB)。任务控制块不是处理器的要求,是我们自己为了方便而发明的。如图14-013,这是任务控制块的结构,很明显,这里两种大小的方格,较窄的格子代表16位的数据宽度,即1个字;而较宽的格式代表32位数据的宽度,即2个字。
为了能够追踪到所有任务,应当把每个任务控制块TCB串起来,形成一个链表(链表是一种数据结构)。代码清单第414行,声明了标号tcb_chain并初始化为一个双字,初始的数值为零。实际上,它是一个指针,用来指向第一个任务的TCB线性基地址。当它为零时,表示任务的数量为0,也就是没有任务。在创建了第一个任务后,应当把该任务的TCB线性基地址填写到这里。每个TCB的第一个双字,也是一个双字长度的指针,用于指向下一个任务的TCB。如果该位置是零,表示后面没有任务,这是链上的最后一个任务;否则,它的数值就是下一个任务的TCB线性基地址。如图14-014所示,所有任务都按照被创建的先后顺序链接在一起,从tcb_chain开始,可以依次找到每一个任务。

第836~838行,用于分配创建TCB所需要的内存空间,并将其挂在TCB链上。当前版本的TCB结构需要0x46字节的内存空间。将新TCB追加到链表上的工作是由过程append_to_tcb_link来完成的,代码清单位于第735~772行,属于内核代码段的内部(近)过程,过程append_to_tcb_link的工作思路是遍历整个链表,找到最后一个TCB,在它的TCB指针域里填写新TCB的首地址。它需要用ECX作为传入的参数,ECX的内容应当为新TCB的线性地址。有个问题值得注意,链首指针tcb_chain是在内核数据段声明并初始化的,只能知道它在段内的偏移地址,而不知道它的线性地址,因此,只能通过内核数据段访问,而无法通过线性地址来访问;相反地,链上的每个TCB,其空间都是动态分配的,只能通过线性地址来访问。因此,在将两个段寄存器和两个通用寄存器压栈保护之后,第742~745行,令段寄存器DS指向内核数据段以读写链首指针tcb_chain,而ES指向整个4GB内存空间,用于遍历和访问每一个TCB。第747行,ECX是当前准备要添加的TCB表的线性基地址,段选择子表示0~4GB的数据段,它的线性基地址为0x00000000,所以ES:ECX的值实际上就是ECX寄存器里面的,ES:ECX+0x00实际上也是TCB的线性地址的首地址,将值0给这个地址。因此,当前准备要添加的TCB表的偏移0x00处的双字被赋值“0”。为何要赋值为“0”,因为当前要追加的TCB表肯定是链表上最后一个TCB,故其用于指向下一个TCB的指针域必须清零,以表明自己是链上最后一个TCB。每个TCB的空间都是动态分配的,其首地址都是线性地址,只能由段寄存器ES指向的0~4GB段来访问。第750行,将标号tcb_chain处指向的双字值传送给寄存器EAX;第751行,将EAX寄存器自己进行or操作,如果EAX为零,则标志寄存器ZF位置“1”,否则置“0”;第752行,判断标志寄存器EFLAGS的ZF是否为1,如果为1,则说明EAX值为“0”,说明标号tcb_chain指向的双字为“0”,说明这是一个空表,程序跳转到第763行,将当前要添加的TCB的线性地址填入标号tcb_chain指向的双字,第一个TCB添加完成;第752行,如果标志寄存器EFLAGS的ZF位不为“0”,则说明EAX不为“0”,说明标号tcb_chain指向的双字不为“0”,有一个线性地址值,因此程序来到第754行;第754行~第758行,搜寻最后一个TCB。第755行,将寄存器EAX的值传送给寄存器EDX,作为0~4GB段使用时作为偏移量。如果这是代码第一次运行,则EAX是标号tcb_chain指向处的双字值,它在第750行被指令赋值,如果这不是代码第一次运行,则EAX是上一个链表的指针域里的值(这个值指向下一个TCB,是下一个TCB的线性地址);第756行,将EDX指向的TCB的指针域里第一个双字(指向下一个TCB)给寄存器EAX;第757行,将寄存器EAX作or操作;第758行,如果EAX为零,表明此次查到到的TCB就是最后一个TCB,如果EAX不为零,则会继续往下寻找,直到找到那个TCB的指针域为零才终止;第760行,将本次要添加的TCB线性地址放入上一个TCB表的指针域里。无论哪种情况,程序都会来到第767行;第767~770行,恢复现场;第772行,ret跳转返回调用者,这是一个近返回。

【14.4.2 使用栈传递过程参数】下面的工作是加载和重定位用户程序,依然是在过程load_relocate_program中进行。该过程需要传入两个参数,分别是用户程序的起始逻辑扇区号,以及它的任务控制块TCB线性地址。和上一章不同的是,参数不是用寄存器传入的,而是采用栈。事实上,这是更为流行和标准的做法。原因很简单,寄存器数量有限,况且还要再过程内部使用,当传入的参数很多时,栈是最好的选择。第840~843行,先以双字的长度将立即数50压入当前栈,这是用户程序的起始逻辑扇区号。在第10章里,我们已经知道push指令可以压入立即数。因此,在这里,压入到栈中的内容将是双字0x00000032(十进制数50)。接着,再压入当前任务控制块TCB的32位线性地址。最后进入过程load_relocate_program内部执行。过程位于第464行,是当前内核代码段的内部过程。第468行,将8个通用寄存器全部入栈保护;第470、471行,将段寄存器DS和ES入栈;第473行,栈顶指针ESP的值给EBP。相当于复制了ESP的值,这样就可以用EBP加偏移量来访问栈中的数据,也不会影响到栈;栈的访问有两种,一种是隐式的,由处理器在执行诸如push、pop、call、ret等指令时自动进行。隐式地访问栈需要使用栈指针寄存器ESP。另一种访问栈的方式不依赖于先进后出机制,而是把栈看成是一般的数据段,直接访问其中的任何内容。在这种方式下,需要使用栈基址寄存器EBP。这里有个例子,比如,从栈中读取一个双字,该数据在栈中的偏移量是由EBP寄存器指向的:mov edx,,在32位模式下,处理器执行这条指令时,用段寄存器SS描述符高速缓存器中的32位基地址,加上EBP寄存器提供的32位偏移量,形成32位线性地址,访问内存取得一个双字,传送到EDX寄存器。很显然,用EBP寄存器来寻址时,不需要使用段超越前缀“SS:”,因为EBP寄存器出现在指令中的地址部分时,默认使用段寄存器SS。如图14-015所示,这是用ESP寄存器的内容初始化EBP后,栈的状态。

当前栈顶的位置是SS:EBP,指向一个双字,是段寄存器ES的内容,因为最近一次的压栈操作是push es。在32位模式下,访问栈用的是栈指针寄存器ESP,而且,每次栈操作的默认操作数大小是双字。处理器在执行压栈指令时,如果发现指令的操作数是段寄存器(CS、SS、DS、ES、FS、GS),那么,将先执行一个内部的零扩展操作,将段寄存器中的16位值扩展成32位,高16位是全零,然后再执行压栈操作。当然,出栈指令pop会执行相反的操作,将32位的值截短为16位,并传送到相应的段寄存器。相应地,SS:EBP+4的位置是段寄存器DS的压栈值。因为栈是向下推进的,故较早压入的内容反而位于高地址方向,回溯它们需要增加EBP的值。从SS:EBP+8的位置开始,是pushad指令压入的8个双字,其中就包括EBP在压栈时的原始内容。再往上,是调用者的返回地址。因为load_relocate_program是一个内部过程,是用32位相对近调用(第843行)进入的,故只压入了EIP的内容,而没有压入段寄存器CS的内容。栈中的第11个双字(从0开始算起)是TCB线性地址,其在栈中的位置是SS:EBP+11×4。同样我们也可以看到,用户程序起始逻辑扇区号在栈中的位置是SS:EBP+12×4。

【14.4.3 加载用户程序】当用户程序被读入内存,并处于运行或者等待运行的状态时,就视为一个任务。任务有自己的代码段和数据段(包括栈),这些段必须通过描述符来引用,而这些描述符可以放在GDT中,也可以放在任务自己私有的LDT中,但最好是放在LDT中。GDT用于存放各个任务公有的描述符,比如公共的数据段和公共例程。每个任务都允许有自己的LDT,而且可以定义在任何内存位置。所以,我们现在要做三件事情:(1)分配一块内存,作为LDT来用,为创建用户程序各个段的描述符做准备;(2)将LDT的大小和起始线性地址登记在任务控制块TCB中;(3)分配内存并加载用户程序,并将它的大小和起始线性地址登记到TCB中。第475、476行,令段寄存器ES指向4GB内存段;第478行,从图14-015中我们可以看到EBP+11×4指向TCB的线性基地址。将该TCB的线性基地址的值给寄存器ESI;第481行,开始申请创建一个LDT所需要的内存,我们的用户程序很简单,不会划分太多的段,所以要创建的大小为160个字节,每个LDT描述符的长度为8个字节,所以160个字节可以放置20个描述符,应当足够了。将其数值传送给寄存器ECX;第482行,调用allocate_memory创建一个内存区域,创建完毕之后,寄存器ECX会返回所创建的内存区域的首地址;第483行,ESI的内容在第478行被赋值为TCB的线性基地址,因此ES:ESI指向TCB表的线性基地址,从图14-013可以看出,ES:ESI+0x0C指向LDT的线性基地址。ECX寄存器在第482行过程返回后被赋值LDT的线性基地址,此行指令将LDT的线性基地址值填入到TCB中;第484行,ES:ESI+0x0A指向LDT的当前界限值,这里将初始的界限值0xFFFF填入TCB表中该处。为何是0xFFFF呢?和GDT一样,界限值是表的总字节数减1,因为我们刚创建LDT,总字节数为0,所以,当前的界限值应当是16位的0-16位的1,结果就是0xFFFF。第487行~第500行,先将用户程序头部读入内核缓冲区中,根据它的大小决定分配多少内存。具体的方法和上一章一样,唯一不同的是调用过程sys_routine_seg_sel:read_hard_disk_0之前,用户程序的起始逻辑扇区号是从栈中取得的。第487、488行,令段寄存器DS指向内核数据段;第490行,从图14-015中可以看到,EBP+12×4指向压入的扇区号50,此处将其从栈中取出,传送给寄存器EAX;第491行,标号core_buff在程序第405行声明,是一个2M的缓冲区。标号core_buf指向缓冲区的首地址,将该首地址传送给寄存器EBX;第492行,近调用sys_routine_seg_sel:read_hard_disk_0过程,将用户程序的第一个扇区长度的内容加载到目标缓冲区。第495行,将标号core_buf指向的双字的值给寄存器EAX,此处的格式为:,这是用DS指向的段的基地址加上core_buf的汇编地址,得到最终的线性地址。DS寄存器在第488行已经指向内核数据段,所以标号core_buf的汇编地址即使缓冲区的首地址。对照用户程序文件,其第一个双字为程序总长度,此条执行之后,寄存器EAX中的值就是用户程序总长度数据;第496行,将该数据复制到寄存器EBX中;第497行,使寄存器EBX中的值与512对齐,即能够被512整除;第498行,EBX增加512,使EBX不仅能够被512整除,还能容纳原来的大小;第499行,测试EAX能否被512整除;第500行,如果EAX能够被512整除,则继续用原来EAX的值。如果不能被512整除,则将对齐了的能够被512整除的EBX中的值传送给寄存器EAX。第502行,经过上面的处理后,EAX中的值肯定能够被512整除,且肯定是加载用户程序实际需要申请的扇区数量长度,将EAX值传送给寄存器ECX,作为参数;第503行,远调用过程allocate_memory为用户程序分配内存,内存分配完毕后,返回的ECX中的值为分配好了的内存的首地址;第504行,将程序加载的基地址登记到TCB中,其偏移量为ES:ESI+0x06;第506行,将分配好了的内存首地址传送给寄存器EBX,第507行,寄存器EDX清零;第508行,ECX赋值512;第509行,ECX为被除数,EDX为零,EAX为能被512整除的字节数,进行32位除法,得到的商在EAX中,就是扇区数目,EDX中的值肯定是零;第510行,将商EAX的值(扇区总数)传送给寄存器ECX。第512、513行,令寄存器DS指向0~4GB数据段;第515行,从栈中取得用户程序的逻辑扇区号;第516~519行,读取全部用户程序,加载到内存中,加载的地址位置可以在TCB中偏移量为0x06地方找到。

【14.4.4 创建局部描述符表】用户程序已经被加载到内存中,现在该是在LDT中创建段描述符的时候了。第521行,从TCB中取得用户程序在内存中的基地址。第524~528行,构造用户程序头部段描述符,其中调用过程后,会在EDX:EAX中返回64位的段描述符。第531、532行用于调用另一个过程fill_descriptor_in_ldt把刚才创建的描述符安装到LDT中。fill_descriptor_in_ldt是当前内核代码段的内部(近)过程,位于第421行,用于在当前任务的LDT中安装描述符。它需要传入两个参数,一个是要安装的描述符,由EDX:EAX共同提供;另一个是当前任务控制块的基地址,由EBX寄存器提供。它用这个地址来访问TCB以获得LDT的基地址和当前的大小(界限值),并在安装描述符后更新LDT的界限值。第425~428行,执行例行的现场保护工作,将过程中用到的各个寄存器压栈保护;第430~433行,先使段寄存器DS指向4GB的内存段;然后,访问TCB,从中取出LDT的基地址传送到EDI寄存器。新描述符的线性地址可以用LDT的基地址加上LDT的总字节数得到。第435~440行,计算用于安装新描述符的线性地址,并把它安装到那里。在这里,ECX寄存器有两个相关联的用途,一个是在第439和440行寻址内存,以安装描述符;另一个是在第436、437行用于计算LDT的界限值是0xFFFF,加1之后,总大小是0x0000,进位部分要丢弃。对CX寄存器的操作不会影响到ECX寄存器的高16位。即使CX寄存器产生了进位,进位也会丢弃,而绝不会跑到ECX寄存器的高16位。
和GDT不同,LDT的0号槽位也是可用的。原因在于,其选择子的TI位是“1”,所以不可能会有一个全零的选择子指向LDT。这就是说,一个指向LDT的选择子代入段寄存器时,它不可能是因程序员粗心大意而未初始化的。第442、443行,将LDT的总大小(字节数)在原来的基础上增加8字节,再减去1,就是新的界限值。第445行,将这个新的界限值更新到TCB中。
第447~450行,将描述符的界限值除以8,余数丢弃不管,所得的商就是当前新描述符的索引号。第452~454行,将CX寄存器中的索引号逻辑左移3次,并将TI位置1,表示指向LDT,这就得到了当前描述符的选择子。第456~461行,恢复现场,返回调用者,回到第534行。过程fill_descriptor_in_ldt在LDT中安装描述符后,用CX寄存器返回一个选择子。第534~536行,用于将选择子的请求特权级RPL设置为3,登记到TCB,并回填到用户程序头部。在LDT中安装的描述符,通常只由用户程序自己使用,即,在请求访问这些段时,请求者是用户程序自己。因此,其选择子的RPL和用户程序的特权级始终一致。第539行,将程序加载基地址给EAX;第540行,将程序加载基地址加上用户程序代码段汇编地址,就得到了用户程序代码段的起始线性基地址,在EAX中;第541行,将代码段的长度值给EBX;第542行,段的长度值减去1,就是界限值。执行到这里,寄存器EBX中的值就是用户程序代码段的界限值;第543行,将段的属性值给ECX,可以看到,这是一个只允许执行的代码段,特权级3,字节为单位的32位段。第544行,调用过程构造描述符,返回在EDX:EAX中;第545行,将TCB的基地址给EBX;第546行,在LDT中再次安装本次构造好的描述符,返回选择子CX;第547行,把返回的选择子设置为特权级3;第548行,把选择子回填到用户程序头部偏移0x14处。第551~560行,构造用户程序数据段的描述符,并安装到LDT中,返回的选择子设置特权级3后,回填到用户程序头部偏移0x1C处;第563行,从用户程序头部0x0C处得到栈的倍率,传送给ECX;第564行,用0xFFFFF减去倍率,得到段界限值;第566行,将4096给EAX;第567行,乘以倍率,就是栈的实际大小(字节计算);第568行,将要创建的栈的实际大小数给ECX,作为参数;第569行,为栈分配内存,返回值ECX为栈的低地址处;第570行,得到栈的高端物理地址;第571行,将栈的属性值给寄存器ECX;第572行,构造栈段的描述符,返回在EDX:EAX中;第573行,将ESI寄存器中的值(也就是TCB的基址)给EBX;第574行,过程fill_descriptor_in_ldt有了EBX(TCB的基址)、EDX:EAX(64位段描述符),就可以将其安装在LDT中,返回值为特权级0的该段的段选择子;第575行,将返回的段选择子的特权级设置为3;第576行,将修改后的段选择子回填到用户程序头部偏移0x08处。到这里为止,运行用户程序前的各个段描述符都已经构造并在LDT中安装完毕,这些段选择子的请求特权级也都是3。

【14.4.5 重定位U-SALT表】从第579行开始,到第620行结束,用于重定位用户程序的U-SALT表。和上一章相比,绝大多数代码是相同的,具体的工作流程也几乎没有变化。当然,因为涉及特权级,个别的差异还是有的。U-SALT位于用户程序头部段。为了访问它,第13章的做法是先用段寄存器ES指向用户程序头部段,再访问该段内的U-SALT表。当然,前提是用户程序头部段的描述符已经安装并开始生效。在本章中,用户程序各个段的描述符位于LDT中,尽管已经安装,但还没有生效(还没有加载局部描述符表寄存器LDTR)。在这种情况下,只能通过4GB的段来访问U-SALT。所以,第579、580行用于令段寄存器ES指向4GB的内存段。第582、583行,令DS寄存器指向内核数据段,即为使用C-SALT表做准备。在前面的代码中,是令EDI寄存器指向用户程序起始加载地址的,这也就是用户程序头部段的起始线性地址。因为U-SALT的条目数位于头部段内偏移0x24处。故,程序中用以下指令来取得该条目数(第587行):mov ecx,。同样的道理,因为U-SALT表位于头部段内偏移0x28处,要想得到U-SALT表的线性基地址,使EDI寄存器指向它,程序中使用的是以下指令(第588行)add edi,0x28。具体的重定位过程在上一章已经讲得很清楚了,无非就是找到名字相同的C-SALT条目,把它的地址部分复制到U-SALT的对应条目中。在第13章里,复制的是16位的代码段选择子和32位的段内偏移。在本章中,这些地址不再是普通的段选择子和段内偏移,而是调用门选择子和段内偏移。当初,创建这些调用门时,选择子的RPL字段是0。也就是说,这些调用门选择子的请求特权级是0。当它们被复制到U-SALT中时,应当改为用户程序的特权级(3)。为此,第605、606行,因为ESI寄存器指向当前条目的地址部分,所以4字节之后的地方是该地址的选择子部分,需要首先传送到AX寄存器;紧接着,修改它的RPL字段,使该选择子的请求特权级为3,回填到用户程序相应位置处。
页: [1]
查看完整版本: X86汇编语言-从实模式到保护模式—笔记(42)-第14章 任务和特权级保护(8)