qemu中ELF文件的加载

1450阅读 0评论2015-05-20 embeddedlwp
分类:LINUX

    前段时间分析了qemu中ELF文件的加载过程,个人感觉通过这个分析不但可以加深对ELF文件格式的理解,而且能够从侧面了解操作系统加载器的工作过程。

一、ELF相关的背景知识

1. ELF格式文件相关概念
ELF格式文件主要包括以下三种类型的文件:
    从链接和执行的角度来讲,ELF文件存在两种视图:链接视图和执行视图。为了区分两种视图,只需记住链接视图由多个section组成,而执行视图由多个segment组成即可。另外,section是程序员可见的,是给链接器使用的概念,汇编文件中通常会显示的定义.text,.data等section,相反,segment是程序员不可见的,是给加载器使用的概念。下图形象的描述了ELF文件两种不同的视图的结构以及二者之间的联系。

    二者之间的联系在于:一个segment包含一个或多个section。
    注意:Section Header Table和Program Header Table并不是一定要位于文件的开头和结尾,其位置由ELF Header指出,上图这么画只是为了清晰。-- ELF文件每个部分的详细介绍参见《ELF 文件格式分析》。 TN05.ELF.Format.Summary.pdf  

2. ELF文件主要数据结构
    上面讲了ELF的相关概念,但是要想用计算机语言(C语言)来实现,必须对应相应的数据结构。linux下通过三个数据结构描述了ELF文件的相关概念。
(1) ELF Header
ELF Header描述了体系结构和操作系统等基本信息,并指出Section Header Table和Program Header Table在文件中的什么位置,每个成员的解释参见注释及附件。

  1. #define EI_NIDENT 16
  2. typedef struct{
  3. /*ELF的一些标识信息,固定值*/
  4. unsigned char e_ident[EI_NIDENT];
  5. /*目标文件类型:1-可重定位文件,2-可执行文件,3-共享目标文件等*/
  6. Elf32_Half e_type;
  7. /*文件的目标体系结构类型:3-intel 80386*/
  8. Elf32_Half e_machine;
  9. /*目标文件版本:1-当前版本*/
  10. Elf32_Word e_version;
  11. /*程序入口的虚拟地址,如果没有入口,可为0*/
  12. Elf32_Addr e_entry;
  13. /*程序头表(segment header table)的偏移量,如果没有,可为0*/
  14. Elf32_Off e_phoff;
  15. /*节区头表(section header table)的偏移量,没有可为0*/
  16. Elf32_Off e_shoff;
  17. /*与文件相关的,特定于处理器的标志*/
  18. Elf32_Word e_flags;
  19. /*ELF头部的大小,单位字节*/
  20. Elf32_Half e_ehsize;
  21. /*程序头表每个表项的大小,单位字节*/
  22. Elf32_Half e_phentsize;
  23. /*程序头表表项的个数*/
  24. Elf32_Half e_phnum;
  25. /*节区头表每个表项的大小,单位字节*/
  26. Elf32_Half e_shentsize;
  27. /*节区头表表项的数目*/
  28. Elf32_Half e_shnum;
  29. /**/
  30. Elf32_Half e_shstrndx;
  31. }Elf32_Ehdr;
下面通过一个具体实例来说明ELF header中每个数据成员对应的值,下面是hello world的ELF文件头,在linux下可以通过"readelf -h ELF文件名"来获得。

ELF Header用数据结构Elf32_Ehdr来表示,描述了操作系统是UNIX,体系结构是80386。Section Header Table中有30个Section Header,从文件地址4412开始,每个Section Header占40字节,Segment Header Table中有9个segment,每个segment header占32个字节,此ELF文件的类型是可执行文件(EXEC),入口地址是0x8048320。

(2) Section Header Table Entry
从ELF Header中可知,每个ELF文件有个Section Header Table,其中每一个表项对应一个section,由数据结构Elf32_Shdr来描述,每个成员的含义参见注释及附件。在linux下可以通过"readelf -S ELF文件名"来查看。
  1. typedef struct{
  2. /*节区名称*/
  3. Elf32_Word sh_name;
  4. /*节区类型:PROGBITS-程序定义的信息,NOBITS-不占用文件空间(bss),REL-重定位表项*/
  5. Elf32_Word sh_type;
  6. /*每一bit位代表一种信息,表示节区内的内容是否可以修改,是否可执行等信息*/
  7. Elf32_Word sh_flags;
  8. /*如果节区将出现在进程的内存影响中,此成员给出节区的第一个字节应处的位置*/
  9. Elf32_Addr sh_addr;
  10. /*节区的第一个字节与文件头之间的偏移*/
  11. Elf32_Off sh_offset;
  12. /*节区的长度,单位字节,NOBITS虽然这个值非0但不占文件中的空间*/
  13. Elf32_Word sh_size;
  14. /*节区头部表索引链接*/
  15. Elf32_Word sh_link;
  16. /*节区附加信息*/
  17. Elf32_Word sh_info;
  18. /*节区带有地址对齐的约束*/
  19. Elf32_Word sh_addralign;
  20. /*某些节区中包含固定大小的项目,如符号表,那么这个成员给出其固定大小*/
  21. Elf32_Word sh_entsize;
  22. }Elf32_Shdr;
(3) Program Header Table Entry
从ELF Header中可知,每个ELF文件有个Program Header Table,其中每一个表项对应一个segment,由数据结构Elf32_phdr来描述,每个成员的含义参见注释及附件。在linux下可以通过"readelf -l ELF文件名"来查看。
  1. typedef struct
  2. {
  3.     /*segment的类型:PT_LOAD = 1 可加载的段*/
  4.     Elf32_Word p_type;
  5.     /*从文件头到该段第一个字节的偏移*/
  6.     Elf32_Off p_offset;
  7.     /*该段第一个字节被放到内存中的虚拟地址*/
  8.     Elf32_Addr p_vaddr;
  9.     /*在linux中这个成员没有任何意义,值与p_vaddr相同*/
  10.     Elf32_Addr p_paddr;
  11.     /*该段在文件映像中所占的字节数*/
  12.     Elf32_Word p_filesz;
  13.     /*该段在内存映像中占用的字节数*/
  14.     Elf32_Word p_memsz;
  15.     /*段标志*/
  16.     Elf32_Word p_flags;
  17.     /*p_vaddr是否对齐*/
  18.     Elf32_Word p_align;
  19. } Elf32_phdr;
二、qemu中ELF文件的加载过程
    在了解了ELF文件的基本结构之后,大体可以想到ELF文件的加载过程就是一个查表的过程,即通过ELF Header得到ELF文件的基本信息-Section Header Table和program Header Table,然后再根据Section Header Table和program Header Table的信息加载ELF文件中的相应部分。上面也提到过,section是从链接器的角度来讲的概念,所以,ELF文件的加载过程中,只有segment是有效的,加载器根据program Header Table中的信息来负责ELF文件的加载。
    首先,从感性上认识一下segment,还是以上面的hello world为例,其对应的program header table如下。

    第一列type即每个segment的类型,每个类型的具体含义参见附件。通常我们之关心程序的代码段(.text section)和数据段(.date section),这两个section组成LOAD类型的segment。
    Offset:当前segment加载到的地址的偏移
    VirAddr:当前segment加载到的虚拟地址
    PhysAddr:当前segment加载到的物理地址(x86平台上,此值没有意义,并不指物理地址)
    FileSiz:当前segment在ELF文件中的偏移
    MemSiz:当前segment在内存页中的偏移
    Flg:segment的权限,R-可读,W-可写, E-可执行
    Align:x86平台内存页面的大小

    在了解了segment的相关信息后,分析下qemu代码中ELF文件的加载过程,印证下上面提到的ELF文件的加载的思想。

   
  1. ret = loader_exec(filename, target_argv, target_environ, regs,info, &bprm);
    filename:要加载的ELF文件的名称
    target_argv:qemu运行的参数,在这里即hello(hello是生成的可执行文件名, $qemu hello)
    target_environ:执行qemu的shell的环境变量
    regs,info,bprm是ELF文件加载过程中涉及的三个重要数据结构,下面会详细分析。

loader_exec函数的功能及含义参见代码注释。
  1. int loader_exec(const char * filename, char ** argv, char ** envp,
  2.              struct target_pt_regs * regs, struct image_info *infop,
  3.              struct linux_binprm *bprm)
  4. {
  5.     int retval;
  6.     int i;

  7.     bprm->p = TARGET_PAGE_SIZE*MAX_ARG_PAGES-sizeof(unsigned int);    /*MAX_ARG_PAGES = 33*/
  8.     memset(bprm->page, 0, sizeof(bprm->page));
  9.     retval = open(filename, O_RDONLY);                                /*返回打开文件的fd*/
  10.     if (retval < 0)
  11.         return retval;
  12.     bprm->fd = retval;
  13.     bprm->filename = (char *)filename;
  14.     bprm->argc = count(argv);
  15.     bprm->argv = argv;
  16.     bprm->envc = count(envp);
  17.     bprm->envp = envp;
  18.     /*1. 要加载文件的属性判断:是否常规文件,是否可执行文件,是否ELF文件; 2. 读取ELF文件的前1024个字节*/
  19.     retval = prepare_binprm(bprm);

  20.     if(retval>=0) {            /*prepare_binrpm函数已经读出了目标文件的前1024个字节,先判断下这个文件是否是ELF文件,即前4个字节*/
  21.         if (bprm->buf[0] == 0x7f
  22.                 && bprm->buf[1] == 'E'
  23.                 && bprm->buf[2] == 'L'
  24.                 && bprm->buf[3] == 'F') {
  25.             retval = load_elf_binary(bprm, regs, infop);
  26. #if defined(TARGET_HAS_BFLT)
  27.         } else if (bprm->buf[0] == 'b'
  28.                 && bprm->buf[1] == 'F'
  29.                 && bprm->buf[2] == 'L'
  30.                 && bprm->buf[3] == 'T') {
  31.             retval = load_flt_binary(bprm,regs,infop);
  32. #endif
  33.         } else {
  34.             fprintf(stderr, "Unknown binary format\n");
  35.             return -1;
  36.         }
  37.     }

  38.     if(retval>=0) {
  39.         /* success. Initialize important registers */
  40.         do_init_thread(regs, infop);
  41.         return retval;
  42.     }

  43.     /* Something went wrong, return the inode and free the argument pages*/
  44.     for (i=0 ; i<MAX_ARG_PAGES ; i++) {
  45.         g_free(bprm->page[i]);
  46.     }
  47.     return(retval);
  48. }

  1. int load_elf_binary(struct linux_binprm * bprm, struct target_pt_regs * regs,
  2.                     struct image_info * info)
  3. {
  4.     struct image_info interp_info;
  5.     struct elfhdr elf_ex;
  6.     char *elf_interpreter = NULL;

  7.     info->start_mmap = (abi_ulong)ELF_START_MMAP;        /*ELF_START_MMAP = 0x80000000*/
  8.     info->mmap = 0;
  9.     info->rss = 0;
  10.     /*主要工作就是初始化info,申请进程虚拟地址空间,将ELF文件映射到这段虚拟地址空间上*/
  11.     load_elf_image(bprm->filename, bprm->fd, info,
  12.                    &elf_interpreter, bprm->buf);

     ... ... ... ...

  1.   
  2.     return 0;
  3. }

  1. static void load_elf_image(const char *image_name, int image_fd,
  2.                            struct image_info *info, char **pinterp_name,
  3.                            char bprm_buf[BPRM_BUF_SIZE])
  4. {
  5.     struct elfhdr *ehdr = (struct elfhdr *)bprm_buf;
  6.     struct elf_phdr *phdr;
  7.     abi_ulong load_addr, load_bias, loaddr, hiaddr, error;
  8.     int i, retval;
  9.     const char *errmsg;

  10.     /* First of all, some simple consistency checks */
  11.     errmsg = "Invalid ELF image for this architecture";
  12.     if (!elf_check_ident(ehdr)) {/*ELF头检查*/
  13.         goto exit_errmsg;
  14.     }
  15.     bswap_ehdr(ehdr);    /*当前为空,是不是主机和目标机大小尾端不一致时才会swap*/
  16.     if (!elf_check_ehdr(ehdr)) {
  17.         goto exit_errmsg;
  18.     }
  19.     /*下面的代码即读出ELF文件的程序头表,首先判断下是否已经被完全读出*/
  20.     i = ehdr->e_phnum * sizeof(struct elf_phdr);    /*program header 表的大小*/
  21.     if (ehdr->e_phoff + i <= BPRM_BUF_SIZE) {
  22.         phdr = (struct elf_phdr *)(bprm_buf + ehdr->e_phoff);
  23.     } else {
  24.         phdr = (struct elf_phdr *) alloca(i);    /*申请i个程序头部*/
  25.         retval = pread(image_fd, phdr, i, ehdr->e_phoff);    /*从文件image_id的偏移为ehdr->e_phoff处读取i个字节到phdr中,即phdr存放program header*/
  26.         if (retval != i) {
  27.             goto exit_read;
  28.         }
  29.     }
  30.     bswap_phdr(phdr, ehdr->e_phnum);

  31. #ifdef CONFIG_USE_FDPIC
  32.     info->nsegs = 0;
  33.     info->pt_dynamic_addr = 0;
  34. #endif

  35.     /* Find the maximum size of the image and allocate an appropriate
  36.        amount of memory to handle that. */
  37.     loaddr = -1, hiaddr = 0;
  38.     for (i = 0; i < ehdr->e_phnum; ++i) {/*遍历每一个program header*/
  39.         if (phdr[i].p_type == PT_LOAD) {
  40.             abi_ulong a = phdr[i].p_vaddr;
  41.             if (a < loaddr) {        /*loaddr = -1而且是unsigned 类型的,所以loaddr是个很大的数*/
  42.                 loaddr = a;            /*loaddr记录segment的起始地址*/
  43.             }
  44.             a += phdr[i].p_memsz;    /*这个segment在内存中的偏移地址*/
  45.             if (a > hiaddr) {        /*hiaddr记录segment的结束地址*/
  46.                 hiaddr = a;
  47.             }
  48. #ifdef CONFIG_USE_FDPIC
  49.             ++info->nsegs;
  50. #endif
  51.         }
  52.     }

  53.     load_addr = loaddr;        /*计算出来的需要加载的起始地址*/
  54.     if (ehdr->e_type == ET_DYN) {    /*共享目标文件(.so)*/
  55.         /* The image indicates that it can be loaded anywhere. Find a
  56.            location that can hold the memory space required. If the
  57.            image is pre-linked, LOADDR will be non-zero. Since we do
  58.            not supply MAP_FIXED here we'll use that address if and
  59.            only if it remains available. */
  60.         load_addr = target_mmap(loaddr, hiaddr - loaddr, PROT_NONE,
  61.                                 MAP_PRIVATE | MAP_ANON | MAP_NORESERVE,
  62.                                 -1, 0);
  63.         if (load_addr == -1) {
  64.             goto exit_perror;
  65.         }
  66.     } else if (pinterp_name != NULL) {
  67.         /* This is the main executable. Make sure that the low
  68.            address does not conflict with MMAP_MIN_ADDR or the
  69.            QEMU application itself. */
  70.         probe_guest_base(image_name, loaddr, hiaddr);
  71.     }
  72.     load_bias = load_addr - loaddr;

  73. #ifdef CONFIG_USE_FDPIC
  74.     {
  75.         struct elf32_fdpic_loadseg *loadsegs = info->loadsegs =
  76.             g_malloc(sizeof(*loadsegs) * info->nsegs);

  77.         for (i = 0; i < ehdr->e_phnum; ++i) {
  78.             switch (phdr[i].p_type) {
  79.             case PT_DYNAMIC:
  80.                 info->pt_dynamic_addr = phdr[i].p_vaddr + load_bias;
  81.                 break;
  82.             case PT_LOAD:
  83.                 loadsegs->addr = phdr[i].p_vaddr + load_bias;
  84.                 loadsegs->p_vaddr = phdr[i].p_vaddr;
  85.                 loadsegs->p_memsz = phdr[i].p_memsz;
  86.                 ++loadsegs;
  87.                 break;
  88.             }
  89.         }
  90.     }
  91. #endif

  92.     info->load_bias = load_bias;    /*真实的加载地址和计算出来(读ELF头信息)的加载地址之差*/
  93.     info->load_addr = load_addr;    /*真实的加载地址*/
  94.     info->entry = ehdr->e_entry + load_bias;    /*重新调整下程序的入口*/
  95.     info->start_code = -1;
  96.     info->end_code = 0;
  97.     info->start_data = -1;
  98.     info->end_data = 0;
  99.     info->brk = 0;

  100.     for (i = 0; i < ehdr->e_phnum; i++) {
  101.         struct elf_phdr *eppnt = phdr + i;
  102.         if (eppnt->p_type == PT_LOAD) {
  103.             abi_ulong vaddr, vaddr_po, vaddr_ps, vaddr_ef, vaddr_em;
  104.             int elf_prot = 0;
  105.             /*记录PT_LOAD类型segment的权限:读//可执行*/
  106.             if (eppnt->p_flags & PF_R) elf_prot = PROT_READ;
  107.             if (eppnt->p_flags & PF_W) elf_prot |= PROT_WRITE;
  108.             if (eppnt->p_flags & PF_X) elf_prot |= PROT_EXEC;

  109.             vaddr = load_bias + eppnt->p_vaddr;
  110.             vaddr_po = TARGET_ELF_PAGEOFFSET(vaddr); /*((vaddr) & ((1 << 12)-1)),目的是取页内偏移*/
  111.             vaddr_ps = TARGET_ELF_PAGESTART(vaddr);     /*((vaddr) & ~(unsigned long)((1 << 12)-1)),向下页对齐,目的取页对齐的地址*/
  112.             /*将ELF文件映射到进程地址空间中*/
  113.             error = target_mmap(vaddr_ps, eppnt->p_filesz + vaddr_po,    /*映射的时候从页内偏移vaddr_po开始映射,即保持原来的偏移量*/
  114.                                 elf_prot, MAP_PRIVATE | MAP_FIXED,
  115.                                 image_fd, eppnt->p_offset - vaddr_po);
  116.             if (error == -1) {
  117.                 goto exit_perror;
  118.             }

  119.             vaddr_ef = vaddr + eppnt->p_filesz;
  120.             vaddr_em = vaddr + eppnt->p_memsz;

  121.             /* If the load segment requests extra zeros (e.g. bss), map it. */
  122.             if (vaddr_ef < vaddr_em) {
  123.                 zero_bss(vaddr_ef, vaddr_em, elf_prot);
  124.             }

  125.             /* Find the full program boundaries. */
  126.             if (elf_prot & PROT_EXEC) {
  127.                 if (vaddr < info->start_code) {
  128.                     info->start_code = vaddr;    /*代码段的起始虚拟地址(页对齐的地址)*/
  129.                 }
  130.                 if (vaddr_ef > info->end_code) {
  131.                     info->end_code = vaddr_ef;    /*代码段的结束虚拟地址(页对齐的地址)*/
  132.                 }
  133.             }
  134.             if (elf_prot & PROT_WRITE) {
  135.                 if (vaddr < info->start_data) {
  136.                     info->start_data = vaddr;    /*数据段的起始虚拟地址*/
  137.                 }
  138.                 if (vaddr_ef > info->end_data) {
  139.                     info->end_data = vaddr_ef;    /*数据段的起始虚拟地址(包括bss段的大小)*/
  140.                 }
  141.                 if (vaddr_em > info->brk) {
  142.                     info->brk = vaddr_em;    /*程序内存映像的顶端(代码段+数据段+bss段)*/
  143.                 }
  144.             }
  145.         } else if (eppnt->p_type == PT_INTERP && pinterp_name) {/*内部解释程序名称:/lib/ld-linux.so.2*/
  146.             char *interp_name;

  147.             if (*pinterp_name) {
  148.                 errmsg = "Multiple PT_INTERP entries";
  149.                 goto exit_errmsg;
  150.             }
  151.             interp_name = malloc(eppnt->p_filesz);
  152.             if (!interp_name) {
  153.                 goto exit_perror;
  154.             }

  155.             if (eppnt->p_offset + eppnt->p_filesz <= BPRM_BUF_SIZE) {
  156.                 memcpy(interp_name, bprm_buf + eppnt->p_offset,
  157.                        eppnt->p_filesz);
  158.             } else {
  159.                 retval = pread(image_fd, interp_name, eppnt->p_filesz,
  160.                                eppnt->p_offset);
  161.                 if (retval != eppnt->p_filesz) {
  162.                     goto exit_perror;
  163.                 }
  164.             }
  165.             if (interp_name[eppnt->p_filesz - 1] != 0) {
  166.                 errmsg = "Invalid PT_INTERP entry";
  167.                 goto exit_errmsg;
  168.             }
  169.             *pinterp_name = interp_name;
  170.         }
  171.     }

  172.     if (info->end_data == 0) {
  173.         info->start_data = info->end_code;
  174.         info->end_data = info->end_code;
  175.         info->brk = info->end_code;
  176.     }

  177.     if (qemu_log_enabled()) {
  178.         load_symbols(ehdr, image_fd, load_bias);
  179.     }

  180.     close(image_fd);
  181.     return;

  182.  exit_read:
  183.     if (retval >= 0) {
  184.         errmsg = "Incomplete read of file header";
  185.         goto exit_errmsg;
  186.     }
  187.  exit_perror:
  188.     errmsg = strerror(errno);
  189.  exit_errmsg:
  190.     fprintf(stderr, "%s: %s\n", image_name, errmsg);
  191.     exit(-1);
  192. }


上一篇:使用 libvirt创建和管理KVM虚拟机
下一篇:qemu源码分析之Makefile