X86汇编语言-从实模式到保护模式—笔记(33)-第13章 程序的动态加载和执行(11)
本帖最后由 兰陵月 于 2017-12-5 21:50 编辑三、用户程序段的重定位和创建描述符既然用户程序已经全部读入内存,现在的任务就是根据它的头部信息来创建段描述符。首先我们读取头部信息,第433行,从栈弹出一个双字到寄存器EDI中,栈中弹出来的是什么,那我们就要看最近一次它压入的是什么?第417行,push ebx,看程序上下文可以得知,EBX的值为申请到的内存首地址,也就是后来我们的用户程序加载到内存中的起始地址。因此,第433行,执行完pop EDI后,EDI中就有了用户程序在内存中的起始地址;第434行,将程序起始地址给寄存器EAX,因为用户程序的起始部分是头部段,所以程序起始地址也是程序头部的起始地址;第435行,EDI+0x04处放着用户程序头部的长度数据,将该数据传送给寄存器EBX;第436行,将EBX自减1,这样EBX中就是头部段的段界限值了;第437行,将0x00409200传送给ECX,这个头部段的属性值,它表示头部段是一个字节粒度的数据段。现在我们已经得到了用户程序头部段的三个数据:头部段起始线性地址、头部段的段界限值、头部段的段属性。利用这三个数据,我们就可以构造头部段的段描述符了。第438行,调用过程make_seg_descriptor构造头部段的段描述符。该过程在第308行,过程说明为作用为构造存储器和系统的段描述符,输入参数为EAX=线性基地址、EBX=段界限、ECX=属性。各属性位都在原始位置,无关的位清零。最后过程返回EDX:EAX=构造成功的描述符。这个过程已经在上一章详细学习过了,不再重复。要在GDT中安装描述符,必须知道它的物理地址和大小。而要知道这些信息,可以使用指令sgdt,它用于将GDTR寄存器的基地址和边界信息保存到指定的内存位置。sgdt指令的格式为:sgdt m。m是一个6字节内存区域的首地址,该指令不影响任何标志位。第439行,调用一个过程set_up_gdt_descriptor安装刚才构造好了的段描述符。该过程在第263行定义。该过程的功能是在GDT内安装一个新的描述符,输入参数为EDX:EAX=描述符,输出为CX=描述符的选择子。进入程序第266行,第266~271行,将过程中要使用的寄存器压栈保护;第273、274行,切换DS到内核的数据段;第276行,sgdt 。标号pgdt在第332行进行了声明。一个字和一个双字,总共6个字节,低2字节用于保存GDT的界限(大小);高4字节用于保存GDT的32位物理地址,本行执行之后,GDTR的内容被保存到此处。第278、279行,切换ES寄存器指向0~4GB数据段,以便用ES寄存器操作全局描述符表(GDT)。下面的工作是计算描述符的安装地址。这个地址可以这样计算:先得到描述符表的界限值,将它加一,得到描述符表的总字节数,这实际上也是新描述符在GDT内的偏移量。然后,用GDT的线性地址加上这个偏移量,就是用于安装新描述符的线性地址。新指令学习:movzx,带零扩展的传送。如下图图13-021。 movsx,带符号扩展的传送,如下图图13-022,指令格式为:第281行,用关键字word指定了标号pgdt处的一个字数据,将其零扩展传送到寄存器EBX中。虽然段界限是16位的,允许64KB的大小,即8192个描述符,似乎不需要使用32位的寄存器EBX。事实上,还是需要的,因为后面要用它来计算新描述符的32位线性地址,加法指令add要求的是两个32位操作数。第282行,将GDT的界限值加1,就是GDT的总字节数,也是新描述符在GDT内的偏移量。这里用的inc bx,而不是inc ebx,这是有原因的。一般情况来说,在这里用这两条指令的哪一条,都没有问题。但是,如果这是启动计算机以来,第一次在GDT中安装描述符,可能就会产生问题。在初始状态下,也就是计算机启动之后,这是还没有使用GDT,GDTR寄存器中的基地址为0x00000000,界限为0xFFFF。当GDTR寄存器的界限部分是0xFFFF时,表明GDT中还没有描述符。因此,将此指加1,结果是0x10000,由于该寄存器的界限分只有16位,所以只能容纳16位的结果,即0x0000,这就是第一个描述符在表内的偏移量。同样的道理,因为EBX寄存器中的内容是GDT的界限值0x0000FFFF,如果执行的是指令inc ebx,那么EBX寄存器中的内容将使0x00010000,以它作为第一个描述符的偏移量显然是不对的。相反,如果执行的是指令inc bx,那么,因为BX寄存器只有16位,所以结果为0x0000,进位被丢弃(绝不会影响EBX寄存器的高16位)。此指令执行之后,EBX寄存器的内容是0x00000000。第283行,标号pgdt+2处指向的是GDT的线性基地址,将其与新描述符在GDT内的偏移量相加,结果是新的要安装的描述符的线性地址。第285行,将描述符的低32位(放在EAX,由参数传入)放到段基地址为0x0000000,偏移量为EBX的位置;第286行,将描述符的高32位(放在EDX中,由参数传入)放到ebx+4的位置;第288行,新的描述符添加之后,GDT的大小发生了变化,界限也要跟着修改,将原来的界限值加上8。GDTR寄存器中的界限值总是单数(8的整数倍减1),包括它的初始值0xFFFF,所以,每次只要加上新描述符的实际大小就能得到正确的界限值;第290行,用lgdt指令重新加载GDTR,使修改生效。第292~297行,根据GDT的新界限值,来生成相应的段选择子。具体的算法是,取得GDT的当前界限值,除以8,余数丢弃。描述符的索引是从0开始编号的,界限值总是比GDT的总字节数小1。因此,界限值除以8,一定会有余数(余7,丢弃不用),商就是我们所要得到的描述符索引号。最后,将索引号左移3次,留出TI位和RPL位(TI=0,指向GDT,RPL=00),这就是要生成的选择子。第299~306行,恢复调用之前的现场,返回调用者。返回时用了retf,说明这个过程只能通过远过程调用的方式进入。第440行,将头部段的选择子回填到edi+0x04处,从用户程序头部段我们可以看到,原来这里是头部段长度的数据,用户程序加载后,这个长度数据不再需要,因此直接用头部段的选择子数据回填。第443~460行,用同样的方法构造并安装用户程序代码段、数据段的段描述符,并分别回填到edi+0x14、edi+0x1c处。原来这两个地方分别是用户程序代码段、数据段的起始汇编地址。用户程序的栈段描述符创建有所不同。栈所用的空间不需要用户程序提供,而是由内核动态分配。内核分配栈空间时,是以4KB为单位的,也就是说,每次分配至少是4KB的倍数。至于到底分配多少,用户程序应该根据自己的实际需求提出建议。第463行,从用户程序头部偏移为0x0C的地方获得一个建议的栈大小。这是一个倍率,至少应当为1,说明用户程序希望分配4KB。如果为2,说明希望分配8KB;为3则表明希望分配12KB,以此类推。将取得的倍率数值传送到寄存器ECX。第464、465行,计算栈段的界限。如果栈段的粒度是4KB,那么,用0xFFFFF减去倍率,就是用来创建描述符的段界限。举例来说,如果用户程序建议的倍率是2,那么,意味着他想创建的栈空间为8KB。因此,段的界限值为0xFFFFF-2=0xFFFFD。那么,当处理器访问该栈段时,实际使用的段界限为0xFFFFD×0x1000+0xFFF=0xFFFFDFFF。栈是向下扩展的,访问32位的栈,要使用栈指针寄存器ESP,其最大值是0xFFFFFFFF。因此,ESP的值只允许在0xFFFFDFFF和0xFFFFFFFF之间变化,共8KB。第466~469行,用4096(4KB)乘以倍率,得到所需要的栈大小,然后,用这个值去申请内存。这是一个32位无符号数乘法,指令格式为:mul r/m32,这里,用EAX寄存器的值,乘以另一个32位的数(可以在通用寄存器或者内存单元里),在EDX:EAX得到64位的乘法结果。注意,allocate_memory过程返回所分配内存的低端地址。和一般的数据段不同,栈描述符中的基地址,应当是栈空间的高端地址。所以,第470行,用allocate_memory返回的低端地址,加上栈的大小,得到栈空间的高端地址。第471~473行,再次调用两个例程,生成和安装栈段的描述符。注意栈的属性值,它指明了这是一个32位的栈段,粒度为4KB。第474行,将栈段的选择子回填到用户程序头部,供用户程序接管处理器控制权之后使用。
页:
[1]