兰陵月 发表于 2017-12-6 22:00:59

X86汇编语言-从实模式到保护模式—笔记(34)-第13章 程序的动态加载和执行(12)

本帖最后由 兰陵月 于 2017-12-6 22:06 编辑

四、重定位用户程序内的符号地址为了使用内核提供的例程,用户程序需要建立一个符号-地址对照表(SALT)。这样,当用户程序加载后,内核应该根据这些符号名来回填它们对应的入口地址,这称为符号地址的重定位。显然,重定位的过程就是字符串匹配和比较的过程。为了对用户程序内的符号名进行匹配,内核也必须建立一张符号-地址对照表(SALT)。内核的符号地址对照表在第338~360行。这个表是可以根据需要进行扩展的。用户程序内也有一张对照表,在用户程序第29~36行。用户程序内的SALT表,每个条目是256字节,用于容纳符号名,不足256字节的,用零进行填充。内核中的SALT表,每个条目则包括两部分,第一部分也是256字节的符号名;第二部分有6字节,用于容纳4字节的偏移地址和2字节的段选择子,因为符号名是用来描述例程,这6字节就是例程的入口。比如内核SALT表的第一个条目。它初始化了一个256字节的符号名,该名称的前12个字符是“@PrintString”,因为不足256字节,后面填充244个0x00。在该条目的后面,先是一个双字,初始化为put_string例程的偏移地址。这就是说PrintString其实就是put_string的别名,调用PrintString,其实就是调用put_string例程。在用户程序内,只能通过远过程用来进入该例程,所以,该条目的最后是一个字,用公共例程段的选择子来初始化,因为put_string例程位于公共例程段内。内核SALT表最后一个条目“TerminateProgram”,终止程序。当用户程序调用过程时,意味着结束用户程序,将控制返回到内核。当用户程序终止并返回时,返回点位于内核程序中标号return_point所在的位置,该标号位于第582行,属于内核代码。在这一行之前,是内核将控制权交给用户程序的指令。内核的SALT表是静态的,适用于所有要加载的用户程序,理所当然要比用户程序的SALT表大,因为它要提供所有可被用户程序调用的过程列表。至于用户程序,根据需要,它只会列出自己用到的那些。在用户程序加载时,内核的任务是比对着两张SALT表,并将用户程序SALT表中的符号名替换成相应的入口地址。为了便于说明,用户程序的SALT表简称为U-SALT,内核的SALT表简称C-SALT。基本的算法是使用内外层循环,外循环依次从U-SALT表中取出条目,每取出一个条目,就进入内循环进行比对;内循环遍历C-SALT中的每一个条目,同外循环输入的条目进行比对。比对的过程就是两个字符串的比较过程,可以使用cmps指令。该指令有3种基本的形式,分别用于字节、字和双字的比较,如下图图13-023、图13-024。

我们即将进行的比较源字符串地址位于ES:EDI中,目标字符串地址位于DS:ESI中。因此,第477行,从用户程序头部取出头部段的选择子。回忆一下,在第440行的时候,我们构造了用户程序头部段的段描述符,并将其选择子回填在edi+0x04处。本行再次取出头部段的选择子,因为等下要对访问头部段;第478行,切换ES寄存器指向用户程序头部段;第479、480行,切换DS指向内核数据段,因为要比较的目标字符串位于内核数据段中。第482行,清标志寄存器EFLAGS中的DF方向标志,使即将进行的cmps指令按正向推进进行比较。第484行,将用户程序头部段偏移0x24处的一个双字传送给ECX寄存器,此处存放着用户程序SALT表的条目数,将该值传送给ECX寄存器,是用来作为循环的次数。本程序中该值应该为0x00000003。第485行,0x28给EDI。0x28为用户程序SALT的起始偏移地址。这样源字符串地址ES:EDI确定完毕。外循环的基本工作原理:将ES:EDI指向的字符串与目标地址处的字符串每个条目逐一比较,比较结果相等或者不相等由内循环处理,外循环不管。比较结束之后,将EDI的值加上256(因为用户程序SALT表中的每个条目长度为256字节),这样ES:EDI就指向用户程序SALT表中的下一个条目,又进行上述循环操作,知道ECX循环次数结束。内循环的基本工作原理:外循环传来一个字符串地址(由ES:EDI传来),将该地址处字符串与DS:ESI处指向的字符串进行比较,比较的过程由repe cmps指令进行,它也是一个循环次数小于等于64的过程。如果repe cmps比较结果一致,则此时ESI肯定刚好指向内核SALT表某个条目后的入口地址处了,而ESI+4则指向条目的选择子;此时EDI肯定也加了256,指向用户程序SALT本次进行比较的条目的下一个条目了,那么将EDI减去256之后,则EDI就会再次指向U-SALT表中本次比较的条目起始偏移地址,EDI减去252之后,则EDI就会指向条目的拟填充的选择子处。因此将ESI指向的过程入口地址传入EDI-256处的地址,将ESI+4指向的选择子填入EDI-252处。本次填充完毕之后,再将ESI指向C-SALT表的下一个比较条目处,再重复这样的操作。直到C-SALT表中的所有条目都被比较一次。内循环结束,再回到外循环,外循环将EDI的值加上256,指向下一个拟比较的条目,再重复整个过程,直到最后U-SALT表中的所有条目与C-SALT中的所有条目都两两比较一次。这样整个工作结束。从这里程序开始第一次运行时,下面的推算的相关数据都够根据程序执行来推断。第487、488行,因为在内循环中repe cmps指令会影响ECX和EDI的值,因此这里将其压栈保存,内循环结束时再弹出,这样就不会影响相关操作结果。第490行,将内循环的循环次数给ECX,该循环次数再编译时给出,按本程序来看,应该是4。第491行,将要比较的目标地址的数据也就是标号salt的值给ESI。这样DS:ESI这个目标地址也确定了。确定这些相关数据,程序就正式进入内循环。第493~495行,同样时因为在循环中要用到用到这些寄存器,所以压栈进行保护。第497行,因为要用repe cmpsd指令比较长度为256字节的字符串,因此需要比较64次,这里把比较次数64给ecx;第498行,比较ES:EDI和DS:ESI分别指向的字符串,repe是如果相等则继续比较,如果不相等则退出比较。它会根据结果将标志寄存器EFLAGS的ZF位置0或者1。第499行,如果不等于零,则说明两个字符串不同,不同则没有必要进行后续操作,因此直接跳转至标号.b4处。如果等于零,则说明两个字符串相等,程序不跳转,顺序往下执行。同时cmpsd肯定也已经比较了64次。EDI和ESI的值肯定也已经增加了256。第500行,将ESI指向的地址处的双字传给EAX,这个双字前面我们说过,它是过程put_string的入口地址数据。第501行,将该入口地址数据给EDI-256,EDI-256我们前面也讲过了,是本次用来比较的源字符串的首地址,实际上就是用户程序第29行标号PrintString处的地址。第502行,将ESI+4处的一个字传给ax,ESI+4处的字前面我们说过了,它是过程put_string所在段的选择子。将该选择子填充到到EDI-252处,EDI-252的值前面也已经讲过,它是用户程序标号PrintString偏移加4处,也就是紧挨着已经回填的put_string入口地址后的一个字。经过这些操作后,用户程序第29行标号PrintString处开始的6个字依次就是32位入口地址和16位段选择子。不管比较结果匹配还是不匹配,程序总是会来到第506行,弹出一个双字给ECX,此时弹出的双字是第495行压入的双字,当时压入的值应该是4;第507行,再弹出一个双字给ESI,此时弹出的双字是第494行压入的双字,当时其值应该指向标号salt处,也即是第339行C-SALT表第一个条目的首地址。第508行,将ESI的值加上salt_item_len(这个值是编译时候计算的,应该是256+4+2=262),指向C-SALT表中的下一个条目首地址。第509行,从栈中弹出一个双字给EDI,此时弹出的值应该是第493行压入的寄存器EDI的值,该值指向U-SALT表中本次比较条目的首地址,这是因为还要将U-SALT表中的该条目与C-SALT表中的后续条目进行比较,当然应该将EDI的值恢复到源字符串的首地址处。第510行,将ECX的值减1,检查是否为零,这里第一次运行时,肯定不会为零,因此程序跳转到标号.b3处,继续进行U-SALT表中第一个条目与C-SALT表中第二个条目的比较,然后是第三个……直到U-SALT表中第一个条目与C-SALT表中所有条目比较完毕之后,第510行,ecx减1后肯定等于,loop循环条件不成立,程序不再继续循环,执行第506行。同时,程序执行到第509行,堆栈也处于平衡状态,也就是栈顶数据此时是第488行压入的数据。第512行,从栈中弹出一个双字给EDI,此时弹出的值是第488行压入的寄存器EDI中的数据,该数据指向源字符串的首地址。第513行,将该值加上256后,指向U-SALT表中的第二个条目,即用户程序第32行,标号TerminateProgram处,第514行,从栈中弹出一个双字给ECX,此时弹出的双字应该是第487行压入的双字,也就是U-SALT表的条目数,即3。第515行,将ECX值减1,判断其是否为零,此时程序第一次运行到这里,肯定不为零,因此loop循环跳转到标号.b2处继续执行。再次执行相关操作,只不过这次用用户程序第32行处的字符串与内核中的SALT表条目继续进行比较。【到现在进入保护模式为止,我们一直使用的栈都是主引导程序在第62、63行指定的栈,也就是GDT表中索引号为3的段描述符对应的栈】第515行,U-SALT表中所有条目都比较匹配完成之后,循环终止,执行第517行。第517行,es:0x04处为用户程序头部段的段选择子。将该给寄存器ax,主程序将用它来找到用户程序的入口,并从那里进入。第519~528行,恢复过程调用的现场,返回调用处,ret指令是近返回,表示可以用近转移调用。到这里,过程load_relocate_program全部执行完毕,程序返回。第572、573行,在屏幕上显示标号do_status处的信息,标号do_status在第369行定义。第575行,临时保存堆栈指针的值。esp_pointer在第378行有定义,长度为一个双字。第577行,将ax的值给ds,切换DS指向用户程序头部段。ax的值在过程load_relocate_program中第517有给值。第579行,用一个远程跳转指令,跳转到处执行,DS此时指向用户程序的头部段,其偏移量0x10处为一个双字,其值为标号start在用户程序code段内偏移地址。该双字后的位置(即偏移量0x14处)跟着一个16位的段选择子(内核程序加载用户程序的时候已将该处回填用户程序代码段的段选择子),该选择子指向用户程序的代码段,其在GDT中的索引号应该是9。
13.5执行用户程序,返回内核控制现在程序已经来到用户程序区域开始执行了。也就是代码段的标号start处,即用户程序第56行。第57行,把寄存器DS的值(某个段的选择子)给EAX。DS的值此时是那个段的选择子呢?内核程序第577行,将寄存器ax的值给段寄存器DS。那ax的值从哪里来的呢,它是过程load_relocate_program的返回值。过程内部第517行,它将用户程序头部段的选择子给了ax寄存器。我们回到用户程序第57行,这时我们就知道DS中的值就是用户程序头部段的选择子。第58行,切换FS寄存器指向用户程序头部段。第60行,将的值给eax,“”的内存寻址默认寄存器为DS,因此我们推断标号stack_seg在头部段。查找头部段,第13行,声明了标号stack_seg,经过内核程序加载后,它是用户程序堆栈段的选择子的数值;第61行,切换SS指向用户程序堆栈段;第62行,初始化栈指针值ESP为0;第64、65行,切换DS指向用户程序数据段。第67、68行,调用内核过程,显示用户程序数据段标号message_1处的信息字符串。第70~72行,调用内核过程,从硬盘读取一个扇区。起始逻辑扇区号为100,加载目标位置为用户程序数据段第43行声明的标号buffer处,长度为1024字节的缓冲区。第74、75行,显示一串信息;第77、78行,显示刚加载目标缓冲区的那个扇区长度的内容。第80行,用户程序事情处理完毕,用jmp指令远程跳转至内核代码段标号return_point处。程序再次回到内核。位于内核程序第582行。内核重新接管处理器的控制权后,第583、584行,再次切换DS指向内核数据段;第586~588行,切换SS指向内核自己的栈段,并恢复栈指针的值。第590、591行,显示一串信息,意思是我胡汉三又回来了。第596行,进入停机状态。
页: [1]
查看完整版本: X86汇编语言-从实模式到保护模式—笔记(34)-第13章 程序的动态加载和执行(12)