8.2.2 操作ELF格式文件的方法
综合以上的描述,总结执行ELF格式文件的方法,步骤如下:
(1)从文件起始位置读取一个struct elf32_ehdr结构体,验证文件的正确性以及文件与操作系统是否匹配。
(2)找到该结构体中e_entry成员,从系统中获得这个值所指向的内存地址。
(3)读出struct elf32_ehdr结构体中的e_phoff、e_phextsize以及e_phnum三个成员。根据这三个值,利用struct elf32_phdr结构体遍历文件中每一个Program头。
(4)在遍历的过程中,检查struct elf32_phdr结构体中的p_type成员,如果为1,则调用存储设备的相关函数,将文件内偏移为p_offset、大小为p_filesz的一段数据从存储器中读取到p_vaddr所指向的内存位置。
(5)调用执行函数,使程序从e_entry内存处开始执行。
这样,一个ELF格式文件的执行过程就可以顺利完成了,将上述步骤用程序来实现,如下:
代码8-6
- void plat_boot(void){
- int i;
- for(i=0;init[i];i++){
- init[i]();
- }
- init_sys_mmu();
- start_mmu();
- // timer_init();
- init_page_map();
- kmalloc_init();
- ramdisk_driver_init();
- romfs_init();
- struct inode *node;
- struct elf32_phdr *phdr;
- struct elf32_ehdr *ehdr;
- int phnum,pos,dpos;
- char *buf;
- if((buf=kmalloc(1024))==(void *)0){
- printk("get free pages error\n");
- goto HALT;
- }
- if((node=fs_type[ROMFS]->namei(fs_type[ROMFS],"main"\
- ))==(void *)0){
- printk("inode read eror\n");
- goto HALT;
- }
- if(fs_type[ROMFS]->device->dout(fs_type[ROMFS]->device,buf,\
- fs_type[ROMFS]->get_daddr(node),node->dsize)){
- printk("dout error\n");
- goto HALT;
- }
- ehdr=(struct elf32_ehdr *)buf;
- phdr=(struct elf32_phdr *)((char *)buf+ehdr->e_phoff);
- for(i=0;i<ehdr->e_phnum;i++){
- if(CHECK_PT_TYPE_LOAD(phdr)){
- if(fs_type[ROMFS]->device->dout (fs_type[ROMFS]->device,\
- (char *)phdr->p_vaddr,\
- fs_type[ROMFS]->get_daddr(node)+\
- phdr->p_offset,phdr->p_filesz)<0){
- printk("dout error\n");
- goto HALT;
- }
- }
- phdr++;
- }
- exec(ehdr->e_entry);
- HALT:
- while(1);
- }
在代码8-6中,程序首先通过romfs文件系统的namei函数读取RAM盘上的main文件,得到代表该文件的inode结构体。不同于代码8-3中的"main.bin"文件,这里的main文件是直接由编译器编译生成的ELF格式文件。于是,我们可以依据执行ELF程序的一般步骤对它进行处理。
首先要做的就是通过存储设备的dout函数从文件系统中读出main文件的内容,并保存到buf中。为了保证程序简单直观,这里我们没有验证缓冲区的大小是否足够装下ELF和Program头信息,程序仅仅通过kmalloc函数申请了一个1K大小的内存来存储文件开头的部分。这样做,运行本书的例子至少是没有问题的。
接下来,我们需要找到描述ELF头信息的结构体struct elf32_ehdr 的位置,并通过读取它的e_phoff成员找到第一个struct elf32_phdr结构体。这两个结构体的地址,分别被保存在变量ehdr和phdr中。
然后,程序从phdr变量开始,通过循环读取每一个Program头信息,依次判断struct elf32_phdr结构体的p_type成员是否为1。如果该成员为1,则表示此Program头所描述的正是代码段或数据段。此时需要再次调用dout函数,将用户应用程序的代码或数据从存储设备中复制到内存里。这两个信息分别记录在了struct elf32_phdr结构体的p_offset和p_vaddr结构体的成员中。
当所有的代码和数据最终都被加载到内存的正确位置后,就可以调用exec来运行用户应用程序了。用户程序的入口地址可从struct elf32_ehdr的e_entry成员中得到。
有了这样的方法,操作系统在运行用户程序时,便不再需要了解用户程序的细节。而仅需从应用程序中读出与程序运行有关的信息,根据这些信息的提示准备好程序运行的环境。这样,理论上就实现了运行任意二进制应用程序的可能。
然而从另一个角度来看,这种运行应用程序的方法还存在一个致命的缺陷,那就是我们无法保证用户应用程序的运行地址恰好是有效的。
这需要分两种情况去分析。第一种情况是,用户应用程序的运行地址已经超出了物理内存的范围,例如,一个ELF格式的用户应用程序需要运行在内存为0x40000000的地址中,但我们的虚拟系统,不计算RAM盘所占用的内存空间,一共只有8M的可用内存,0x40000000这个地址远远超过了硬件原有内存的范围。第二种情况是,用户应用程序的运行地址虽然落在了物理内存的范围内,但这个地址却恰好被别的应用程序提前占用了。无论哪一种情况发生,都会使我们的操作系统将一个原本可以正常运行的用户应用程序拒之门外。
想要解决这个问题,可以通过虚拟内存映射将原本无效的内存地址映射到有效的空间中。于是,我们只需要将应用程序的代码和数据保存到任意一个没有被使用的有效内存中,然后修改页表,将这个内存地址映射到用户应用程序规定的地址。由此,读者也可以深入地体会一下虚拟地址映射对于一个功能强大的操作系统来说是多么的重要。
当然,想要实现这样的功能,我们的操作系统代码需要进行很大的调整,这其中至少包含内存管理部分和MMU部分两个子结构。为了不占用过多的篇幅,这段代码我们就不去具体实现了。读者如果感兴趣的话,可以尝试自行实现。
下面我们来实践一下这段代码。
操作ELF文件的方法