X86汇编语言-从实模式到保护模式—笔记(31)-第13章 程序的动态加载和执行(9)
本帖最后由 兰陵月 于 2017-12-5 21:50 编辑13.4用户程序的加载和重定位一、加载用户程序前显示友好信息为了表示程序的友好性、交互性,在用户程序加载之前,我们先显示一段信息,意思就是要加载用户程序了。第567、568行,处理器将调用公用例程段的过程put_string显示标号message_5处的字符信息。二、加载用户程序(一)调用过程load_relocate_program信息显示完了后,下面就要正式开始加载用户程序。第569行,将用户程序在硬盘上的存储位置即扇区号50给寄存器ESI。第570行,调用一个过程load_relocate_program,从指令中我们看出,这时一个段内调用,该过程在内核当前的代码段内,在程序的第387行。我们来到第387行,该过程有解释:功能是加载并重定位用户程序;参数是ESI,用户程序在硬盘上的起始逻辑扇区号;返回值是ax,它指向用户程序头部的选择子。(二)进入过程load_relocate_program,并做相关前期工作第390行~第397行,压栈保存在过程内部要用到的通用寄存器和段寄存器;第399、400行,切换DS到内核数据段,此前DS也是指向内核数据段的,为什么这里还要这样做,因为我们的这个过程有可能被很多代码调用,而不仅仅是第570行那里调用,因为那些代码执行的时候DS是否指向内核数据段我们不得而知,所以这里为了确保我们的过程能够正常运行,我们必须在这里切换DS指向内核数据段。(三)加载用户程序的第一个扇区加载用户程序的过程是read_hard_disk_0,它位于公用例程段内第144行,其功能与主引导程序的同名过程是一样的,即读取硬盘上指定的一个扇区。它需要的参数是:1、EAX,需要读取的硬盘的逻辑扇区号;2、DS:EBX,加载到内存的目标地址。它的返回值是:EBX=EBX+512,在传入参数的基础上加512,指向下一个要加载到内存的目标地址。该过程具体的实现过程已经重温多次,不再重复。第402行,将ESI的值(ESI在调用load_relocate_program前已经作为参数给值50)给EAX,EAX将作为过程read_hard_disk_0的参数。第403行,将标号core_buf的值给EBX,DS:EBX将作为过程read_hard_disk_0的另一个参数,它是用户程序准备加载到内存的目标地址,该地址位于内核数据段中,是在第376行声明的一个2048字节长的缓冲区。因为在内核中开辟出一段固定的空间,对于分析、加工和中转数据都比较方便。第404行,调用内核公用例程段的read_hard_disk_0过程读取用户程序的第一个扇区到缓冲区。(四)计算用户程序实际占用的扇区数量用户程序必须符合规定的格式,才能被内核程序识别和加载。通常情况下,流行的操作系统会规定自己的可执行文件格式,一般都比较复杂,这种复杂性和操作系统自身的复杂性是息息相关的。1、首先分析用户程序头部的相关数据所有操作系统的可执行文件都包括文件头,这里也不例外。事实上,这也是我们熟悉的、一贯的做法。用户程序头部在用户程序的第7行~第38行。(1)偏移0x00,双字长,用户程序总长度,字节为单位。(2)偏移0x04,双字长,用户程序头部长度,字节为单位。(3)偏移0x08,双字长,为栈保留,用于接收堆栈段选择子。和早先的做法不同,内核不要求用户程序提供栈空间,而改由内核动态分配,以减轻用户程序编写的负担。当内核分配了栈空间后,会把栈段的选择子填写到这里,用户程序开始执行时,可以从这里取得该选择子以初始化自己的栈。(4)偏移0x0C,双字长,供用户程序使用,编写者建议的栈大小,以4KB为单位。如果是1,就是希望分配4KB的栈空间;如果是2,就是希望分配8KB的栈空间,依次类推。(5)偏移0x10,双字长,用户程序入口点的32位偏移地址。(6)偏移0x14,双字长,用户程序代码段的起始汇编地址。当内核完成对用户程序的加载和重定位后,将把该段的选择子回填到这里(仅占用低字部分)。这样一来,它和0x10处的双字一起,共同组成一个6字节的入口点,内核从这里转移控制到用户程序。(7)偏移0x18,双字长,用户程序代码段的长度,字节为单位。(8)偏移0x1C,双字长,用户程序数据段起始汇编地址,当内核完成用户程序的加载和重定位后,将把该段的选择子回填到这里(仅占用低字部分)。(9)偏移0x20,双字长,用户程序数据段长度,字节为单位。2、根据读取到的用户程序长度数据确定实际需要的扇区数量用户程序的长度数据在其头部偏移0x00处的一个双字内,前面我们把用户程序的第一个扇区长度的数据加载到了目标缓冲区,该缓冲区由标号core_buf指向。也就是说,现在用户程序的第一个扇区起始地址就是标号core_buf所在的位置。第407行,把core_buf开始的偏移量为0x00的位置处的双字传送给寄存器EAX,这个位置处的数据就是用户程序的长度,因此,实际上就是把用户程序的长度数据给了寄存器EAX。用户程序的大小(总字节数)不一定恰好是512的整数倍。也就是说,最后一个扇区未必是满的。因此,如果直接除以512,可能会使结果(除法的商)比实际的扇区数少1。通常情况下,需要判断除法的余数,根据余数是否为零,来决定实际的扇区总数,这不可避免地要使用判断和条件转移指令。早先的处理器中,转移指令是影响处理器速度的重大因素之一,因为它会使流水线中那些已经预取和译码的指令失效。在较晚的处理器中,虽然普遍采用了分支预测技术,但并不总能保证预测是准的。因此,为了减少转移指令对处理器的影响,最好的办法就是尽量不使用转移指令。为了帮助程序员部分地戒掉使用转移指令的欲望,处理器引入了条件传送指令cmovcc。cmovcc指令是从P6处理器开始引入的,因为并非所有处理器都支持它。要检测处理器是否支持cmovcc指令,可以以1号功能执行cpuid指令(mov eax,1; cpuid)当处理器执行这两条指令后,会在EBX、ECX和EDX寄存器返回丰富的信息,以指示各种详尽的处理器特性。此时,检查EDX寄存器的第16位(位15),当它是“1”时,表明处理器支持cmovcc指令。comvcc指令是条件转移指令和传送指令相结合的产物,既有条件转移指令的多样性,又执行的是传送操作。但是,和mov指令不同的是,它的目的操作数只允许是16位或者32位通用寄存器,源操作数只能是相同宽度的通用寄存器和内存单元。条件传送指令支持所有的条件转移指令的条件。只需要相应的把条件转移指令的“j”换成“cmov”即可。该指令不影响EFLAGS寄存器中的任何标志位。相反地,它的执行过程要依赖于这些标志,就像条件转移指令一样。回到程序,关于一个数能否被512整除,我们可以找到一个规律,所有能被512整除的数,其最低端的9个比特都是“0”。掌握了这个规律,我们很多事情就好做了。第408行,将EAX值传送给EBX,等于是做个备份,因为还要用到。第409、410行,(先假设了EBX不能被512整除)先用and指令将长度数据的最低9个比特清零,等于是去掉那些不足512的零头,然后,再将其加上512,等于是将那些零头凑整。这样EBX中就是实际占用的整数个扇区长度数值。但是,如果人家原本就是512的整数倍,这样做无疑是多加了一个扇区。因此,第411行,先测试EAX(同样保存着用户程序长度数据)寄存器的最低9个比特,如果测试的结果是它们不全为零,则将EBX中内容传送至EAX,采用凑整的结果;如果为全零,则cmovnz指令什么也不做,依然采用用户程序原本的长度。
页:
[1]