ELF映像的装入

ELF映像的装入(一)   毛德操


   现在我们来看看ELF映像的装入和启动。

一般而言,应用软件的编程不可能是“一竿子到底”、所有的代码都自己写的,程序员不可避免地、也许是不自觉地、都会使用一些现成的程序库。对于C语言的编程,至少C程序库是一定会用到的。从编译/连接和运行的角度看,应用程序和库程序的连接有两种方法。一种是固定的、静态的连接,就是把需要用到的库函数的目标(二进制)代码从程序库中抽取出来,连接进应用软件的目标映像中,或者甚至干脆把整个程序库都连接进应用软件的映像中。这里所谓的连接包括两方面的操作,一是把库函数的目标代码“定位”在应用软件目标映像中的某个位置上。由于不同应用软件本身的大小和结构都可能不同,库函数在目标映像中的位置是无法预先确定的。为此,程序库中的代码必须是可以浮动的,即“与位置无关”的,在编译时必须加上-fPIC选项,这里PIC是“Position-Independent Code”的缩写。一旦一个库函数在映像中的位置确定以后,就要使应用软件中所有对此函数的调用都指向这个函数。早期的软件都采用这种静态的连接方法,好处是连接的过程只发生在编译/连接阶段,而且用到的技术也比较简单。但是也有缺点,那就是具体库函数的代码往往重复出现在许多应用软件的目标映像中,从而造成运行时的资源浪费。另一方面,这也不利于软件的发展,因为即使某个程序库有了更新更好的版本,已经与老版本静态连接的应用软件也享受不到好处,而重新连接往往又不现实。再说,这也不利于将程序库作为商品独立发展的前景。于是就发展起了第二种连接方法,那就是动态连接。所谓动态连接,是指库函数的代码并不进入应用软件的目标映像,应用软件在编译/连接阶段并不完成跟库函数的连接;而是把函数库的映像也交给用户,到启动应用软件目标映像运行时才把程序库的映像也装入用户空间(并加以定位)、再完成应用软件与库函数的连接。说到程序库,最基本、最重要的当然是C语言库、即libc或glibc。

这样,就有了两种不同的ELF格式映像。一种是静态连接的,在装入/启动其运行时无需装入函数库映像、也无需进行动态连接。另一种是动态连接的,需要在装入/启动其运行时同时装入函数库映像并进行动态连接。显然,Linux内核应该既支持静态连接的ELF映像、也支持动态连接的ELF映像。进一步的分析表明:装入/启动ELF映像必需由内核完成,而动态连接的实现则既可以在内核中完成,也可在用户空间完成。因此,GNU把对于动态连接ELF映像的支持作了分工:把ELF映像的装入/启动放在Linux内核中;而把动态连接的实现放在用户空间,并为此提供一个称为“解释器”的工具软件,而解释器的装入/启动也由内核负责。

大家知道,在Linux系统中,目标映像的装入/启动是由系统调用execve()完成的,但是可以在Linux内核上运行的二进制映像有a.out和ELF两种。由于篇幅的关系,在“情景分析”一书中对于二进制映像只讲了a.out格式映像的装入/启动,而没有讲ELF格式映像的装入/启动。这是因为如果讲了ELF映像就不可避免地要讲到动态连接、讲到“解释器”,那样一来篇幅就大了。从对于装入/启动可执行映像的过程的一般了解而言,光讲a.out也许就够了;可是考虑到ELF映像(以及Windows软件的PE映像)对于兼容内核开发的重要意义,还是有必要补上这一课。

本文先介绍装入/启动一个ELF映像时发生于Linux内核中的操作,下一篇漫谈则介绍发生于用户空间的操作、即“解释器”对于共享库的操作。



1.系统空间的操作



       内核中实际执行execv()或execve()系统调用的程序是do_execve(),这个函数先打开目标映像文件,并从目标文件的头部(从第一个字节开始)读入若干(12 字节,然后调用另一个函数search_binary_handler(),在那里面让各种可执行程序的处理程序前来认领和处理。内核所支持的每种可执行程序都有个struct linux_binfmt数据结构,通过向内核登记挂入一个队列。而search_binary_handler(),则扫描这个队列,让各个数据结构所提供的处理程序、即各种映像格式、逐一前来认领。如果某个格式的处理程序发现特征相符而,便执行该格式映像的装入和启动。

       我们从ELF格式映像的linux_binfmt数据结构开始:



#define load_elf_binary load_elf32_binary



static struct linux_binfmt elf_format = {

              .module          = THIS_MODULE,

              .load_binary    = load_elf_binary,

              .load_shlib      = load_elf_library,

              .core_dump    = elf_core_dump,

              .min_coredump      = ELF_EXEC_PAGESIZE

};



       这个数据结构表明:ELF格式的二进制映像的认领、装入和启动是由load_elf_binary()完成的。而“共享库”、即动态连接库映像的装入则由load_elf_library()完成。实际上共享库的映像也是二进制的,但是一般说“二进制”映像是指带有main()函数的、可以独立运行并构成一个进程主体的可执行程序的二进制映像。另一方面,尽管装入/启动二进制映像的过程中蕴含了共享库的装入(否则无法运行),但是在此过程中却并没有调用load_elf_library(),而是通过别的函数进行,这个函数只是在sys_uselib()、即系统调用uselib()中通过函数指针load_shlib受到调用。所以,load_elf_library()所处理的是应用软件在运行时对于共享库的动态装入,而不是启动进程时的静态装入。

       下面我们就来看load_elf_binary()代码,这个函数在fs/binfmt_elf.c中。由于篇幅的关系,本文只能以近似于伪代码的形式列出经过简化整理的代码(下同),有需要或兴趣的读者不妨结合源文件中的原始代码阅读。由于load_elf_binary()是个比较大的函数,我们分段阅读。



[sys_execve() > do_execve() > search_binary_handler() > load_elf_binary()]



static int load_elf_binary(struct linux_binprm * bprm, struct pt_regs * regs)

{

       . . . . . .

       struct {

              struct elfhdr elf_ex;

              struct elfhdr interp_elf_ex;

             struct exec interp_ex;

       } *loc;



       loc = kmalloc(sizeof(*loc), GFP_KERNEL);

       . . . . . .

      

       /* Get the exec-header */

       loc->elf_ex = *((struct elfhdr *) bprm->buf);



       . . . . . .

       /* First of all, some simple consistency checks */

       if (memcmp(loc->elf_ex.e_ident, ELFMAG, SELFMAG) != 0)

              goto out;           //比对四个字符,必须是0x7f、‘E’、‘L’、和‘F’。



       if (loc->elf_ex.e_type != ET_EXEC && loc->elf_ex.e_type != ET_DYN)

              goto out;           //映像类型必须是ET_EXEC或ET_DYN。

       if (!elf_check_arch(&loc->elf_ex))

              goto out;           //机器(CPU)类型必须相符。

       . . . . . .



       首先是认领。ELF映像文件的头部应该是个struct elfhdr数据结构,对于32位映像这实际上是struct elf32_hdr数据结构、即Elf32_Ehdr,其定义如下所示:



#define elfhdr         elf32_hdr



typedef struct elf32_hdr{

  unsigned char     e_ident[EI_NIDENT];           // EI_NIDENT = 16

  Elf32_Half   e_type;                                     // 即unsigned shout

  Elf32_Half   e_machine;                               // 即 unsigned int

  Elf32_Word e_version;

  Elf32_Addr e_entry;  /* Entry point */

  Elf32_Off   e_phoff;

  Elf32_Off   e_shoff;

  Elf32_Word e_flags;

  Elf32_Half   e_ehsize;

  Elf32_Half   e_phentsize;

  Elf32_Half   e_phnum;

  Elf32_Half   e_shentsize;

  Elf32_Half   e_shnum;

  Elf32_Half   e_shstrndx;

} Elf32_Ehdr;



       这个数据结构的前16个字节是ELF映像的标志e_ident[ ],其中开头的4个字节就是所谓“Magic Number”,应该是“\177ELF”。除这4个字符比对相符以外,还要看映像的类型是否ET_EXEC和ET_DYN之一;前者表示可执行映像,后者表示共享库(此外还有ET_REL和ET_CORE,分别表示浮动地址模块和dump映像)。同时,映像所适用的CPU类型(如x86或PPC)也须相符。如果这些条件都满足,就算认领成功,下面就是进一步的处理了。进一步的处理当然需要更多的信息,在Elf32_Ehdr中提供了两个指针,或者说两个(文件内的)位移量,即e_phoff和e_shoff。如果非0的话,前者指向“程序头(Program Header)”数组的起点;后者指向“区段头(Section Header)”数组的起点。两个数组的大小(元素的个数)分别由e_phnum和e_shnum提供,而每个数组元素(表项)的大小由e_phentsize和e_shentsize提供。至于e_ehsize,则是映像头部本身的大小。还有个值得特别说明的成分是e_entry,那就是该映像的程序入口,一般是_start()的起点。

人们常常提到二进制代码映像中有所谓“程序段”“数据段”等等,那都属于映像中的“区段”即“Section”。但是区段的种类远远不止这些而有很多,例如“符号表”就是一个区段,再如用于动态连接的信息、用于Debug的信息等等,都属于不同的区段。而区段头数组、或曰区段头表,则为映像中的每一个区段都提供一个描述性的数据结构。

而程序头数组或曰程序头表中的每一个表项,则是对一个“部(Segment)”的描述。一个部可以包含若干个区段,也可以只是一个简单的数据结构。整个ELF映像就是由文件头、区段头表、程序头表、一定数量的区段、以及一定数量的部构成。而ELF映像的装入/启动过程,则就是在各种头部信息的指引下将某些部或区段装入一个进程的用户空间,并为其运行做好准备(例如装入所需的共享库),最后(在目标进程首次受调度运行时)让CPU进入其程序入口的过程。读者将会看到,这个过程很可能是嵌套的,因为在装入一个映像的过程中很可能需要装入另一个或另几个别的映像。

我们继续往下看:



[sys_execve() > do_execve() > search_binary_handler() > load_elf_binary()]



       /* Now read in all of the header information */

       . . . . . .

       size = loc->elf_ex.e_phnum * sizeof(struct elf_phdr);

       retval = -ENOMEM;

       elf_phdata = (struct elf_phdr *) kmalloc(size, GFP_KERNEL);

       if (!elf_phdata)

              goto out;



       retval = kernel_read(bprm->file, loc->elf_ex.e_phoff, (char *) elf_phdata, size);

       . . . . . .

       files = current->files;            /* Refcounted so ok */

       . . . . . .

       retval = get_unused_fd();

       . . . . . .

       get_file(bprm->file);

       fd_install(elf_exec_fileno = retval, bprm->file);



       elf_ppnt = elf_phdata;

       elf_bss = 0;

       elf_brk = 0;



       start_code = ~0UL;

       end_code = 0;

       start_data = 0;

       end_data = 0;



       这里通过kernel_read()读入的是目标映像的整个程序头表,这是一个struct elf_phdr、实际上是struct elf32_phdr结构数组。这种数据结构的定义为:



typedef struct elf32_phdr{

  Elf32_Word       p_type;

  Elf32_Off          p_offset;

  Elf32_Addr        p_vaddr;

  Elf32_Addr        p_paddr;

  Elf32_Word        p_filesz;

  Elf32_Word        p_memsz;

  Elf32_Word        p_flags;

  Elf32_Word        p_align;

} Elf32_Phdr;



这里的p_type表示部的类型。

同时,这里还为已打开的目标映像文件在当前进程的打开文件表中另外分配一个表项,类似于执行了一次dup(),目的在于为目标文件维持两个不同的上下文,以便从不同的位置上读出。

接着是对elf_bss 、elf_brk、start_code、end_code等等变量的初始化。这些变量分别纪录着当前(到此刻为止)目标映像的bss段、代码段、数据段、以及动态分配“堆” 在用户空间的位置。除start_code的初始值为0xffffffff外,其余均为0。随着映像内容的装入,这些变量也会逐步得到调整,读者不妨自己留意这些变量在整个过程中的变化。

读入了程序头表,并对start_code等变量进行初始化以后,下面的第一步就是在程序头表中寻找“解释器”部、并加以处理的过程。



[sys_execve() > do_execve() > search_binary_handler() > load_elf_binary()]



       for (i = 0; i < loc->elf_ex.e_phnum; i++) {

              if (elf_ppnt->p_type == PT_INTERP) {

                     . . . . . .

                     retval = -ENOMEM;

                     elf_interpreter = (char *) kmalloc(elf_ppnt->p_filesz, GFP_KERNEL);

                     . . . . . .

                     retval = kernel_read(bprm->file, elf_ppnt->p_offset,

elf_interpreter, elf_ppnt->p_filesz);

                     . . . . . .

                     interpreter = open_exec(elf_interpreter);

                     retval = PTR_ERR(interpreter);

                     if (IS_ERR(interpreter))

                            goto out_free_interp;

                     retval = kernel_read(interpreter, 0, bprm->buf, BINPRM_BUF_SIZE);

                     . . . . . .

                     /* Get the exec headers */

                     loc->interp_ex = *((struct exec *) bprm->buf);

                     loc->interp_elf_ex = *((struct elfhdr *) bprm->buf);

                     break;

              }

              elf_ppnt++;

       }



       显然,这个for循环的目的仅在于寻找和处理目标映像的“解释器”部。ELF格式的二进制映像在装入和启动的过程中需要得到一个工具软件的协助,其主要的目的在于为目标映像建立起跟共享库的动态连接。这个工具称为“解释器”。一个ELF映像在装入时需要用什么解释器是在编译/连接是就决定好了的,这信息就保存在映像的“解释器”部中。“解释器”部的类型为PT_INTERP,找到后就根据其位置p_offset和大小p_filesz把整个“解释器”部读入缓冲区。整个“解释器”部实际上只是一个字符串,即解释器的文件名,例如“/lib/ld-linux.so.2”。有了解释器的文件名以后,就通过open_exec()打开这个文件,再通过kernel_read()读入其开头128个字节,这就是映像的头部。早期的解释器映像是a.out格式的,现在已经都是ELF格式的了,/lib/ld-linux.so.2就是个ELF映像。

下面是对解释器映像头部的处理,首先要确认其为ELF格式还是a.out格式。



[sys_execve() > do_execve() > search_binary_handler() > load_elf_binary()]



       . . . . . .

       /* Some simple consistency checks for the interpreter */

       if (elf_interpreter) {

              interpreter_type = INTERPRETER_ELF | INTERPRETER_AOUT;



              /* Now figure out which format our binary is */

              if ((N_MAGIC(loc->interp_ex) != OMAGIC) &&

                  (N_MAGIC(loc->interp_ex) != ZMAGIC) &&

                  (N_MAGIC(loc->interp_ex) != QMAGIC))

                     interpreter_type = INTERPRETER_ELF;



              if (memcmp(loc->interp_elf_ex.e_ident, ELFMAG, SELFMAG) != 0)

                     interpreter_type &= ~INTERPRETER_ELF;

              . . . . . .

       } else {

              . . . . . .

       }



       /* OK, we are done with that, now set up the arg stuff,

          and then start this sucker up */



       至此,我们已为目标映像和解释器映像的装入作好了准备。可以让当前进程(线程)与其父进程分道扬镳,转化成真正意义上的进程,走自己的路了。



[sys_execve() > do_execve() > search_binary_handler() > load_elf_binary()]



       /* Flush all traces of the currently running executable */

       retval = flush_old_exec(bprm);

              . . . . . .

       /* OK, This is the point of no return */

       current->mm->start_data = 0;

       current->mm->end_data = 0;

       current->mm->end_code = 0;

       current->mm->mmap = NULL;

       current->flags &= ~PF_FORKNOEXEC;

       current->mm->def_flags = def_flags;

       . . . . . .

       /* Do this so that we can load the interpreter, if need be.  We will

          change some of these later */

       retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP), executable_stack);

       . . . . . .



可想而知,flush_old_exec()把当前进程用户空间的页面都释放了。这么一来,当前进程的用户空间是“一片白茫茫大地真干净”,什么也没有了,原有的物理页面映射都已释放。

现在要来重建用户空间的映射了。一个新的映像要能运行,用户空间堆栈是必须的,所以首先要把用户空间的一个虚拟地址区间划出来用于堆栈。进一步,当CPU进入新映像的程序入口时,堆栈上应该有argc、argv[]、envc、envp[]等参数。这些参数来自老的程序,需要通过堆栈把它们传递给新的映像。实际上,argv[]和envp[]中是一些字符串指针,光把指针传给新映像,而不把相应的字符串传递给新映像,那是毫无意义的。为此,在进入search_binary_handler()、从而进入load_elf_binary()之前,do_execve()已经为这些字符串分配了若干页面,并通过copy_strings()从用户空间把这些字符串拷贝到了这些页面中。现在则要把这些页面再映射回用户空间(当然是在不同的地址上),这就是这里setup_arg_pages()要做的事。这些页面映射的地址是在用户空间堆栈的最顶部。对于x86处理器,用户空间堆栈是从3GB边界开始向下伸展的,首先就是存放着这些字符串的页面,再往下才是真正意义上的用户空间堆栈。而argc、argv[]这些参数,则就在这真正意义上的用户空间堆栈上。

下面就可以装入新映像了。所谓“装入”,实际上就是将映像的(部分)内容映射到用户(虚拟地址)空间的某些区间中去。在MMU的swap机制的作用下,这个过程甚至并不需要真的把映像的内容读入物理页面,而把实际的读入留待将来的缺页中断。

       首先装入的是目标映像本身。



[sys_execve() > do_execve() > search_binary_handler() > load_elf_binary()]



              /* Now we do a little grungy work by mmaping the ELF image into

             the correct location in memory.  At this point, we assume that

             the image should be loaded at fixed address, not at a variable address. */



       for(i = 0, elf_ppnt = elf_phdata; i < loc->elf_ex.e_phnum; i++, elf_ppnt++) {

              int elf_prot = 0, elf_flags;

              unsigned long k, vaddr;



              if (elf_ppnt->p_type != PT_LOAD)

                     continue;

              . . . . . .

              vaddr = elf_ppnt->p_vaddr;

              if (loc->elf_ex.e_type == ET_EXEC || load_addr_set) {

                     elf_flags |= MAP_FIXED;

              } else if (loc->elf_ex.e_type == ET_DYN) {

                     /* Try and get dynamic programs out of the way of the default mmap

                        base, as well as whatever program they might try to exec.  This

                        is because the brk will follow the loader, and is not movable.  */

                     load_bias = ELF_PAGESTART(ELF_ET_DYN_BASE - vaddr);

              }



              error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt, elf_prot, elf_flags);

              . . . . . .



              if (!load_addr_set) {

                     load_addr_set = 1;

                     load_addr = (elf_ppnt->p_vaddr - elf_ppnt->p_offset);

                     if (loc->elf_ex.e_type == ET_DYN) {

                            load_bias += error -

                                         ELF_PAGESTART(load_bias + vaddr);

                            load_addr += load_bias;

                            reloc_func_desc = load_bias;

                     }

              }

              k = elf_ppnt->p_vaddr;

              if (k < start_code) start_code = k;

              if (start_data < k) start_data = k;

               . . . . . .

              k = elf_ppnt->p_vaddr + elf_ppnt->p_filesz;



              if (k > elf_bss)

                     elf_bss = k;

              if ((elf_ppnt->p_flags & PF_X) && end_code < k)

                     end_code = k;

              if (end_data < k)

                     end_data = k;

              k = elf_ppnt->p_vaddr + elf_ppnt->p_memsz;

              if (k > elf_brk)

                     elf_brk = k;

       }                                                      //end for() loop



       loc->elf_ex.e_entry += load_bias;

       elf_bss += load_bias;

       elf_brk += load_bias;

       start_code += load_bias;

       end_code += load_bias;

       start_data += load_bias;

       end_data += load_bias;



       /* Calling set_brk effectively mmaps the pages that we need

        * for the bss and break sections.  We must do this before

        * mapping in the interpreter, to make sure it doesn't wind

        * up getting placed where the bss needs to go.

        */

       retval = set_brk(elf_bss, elf_brk);

       . . . . . .



       还是从目标映像的程序头表中搜索,这一次是寻找类型为PT_LOAD的部(Segment)。在二进制映像中,只有类型为PT_LOAD的部才是需要装入的。

       找到一个PT_LOAD片以后,先要确定其装入地址。正如代码前面的注释所述,这里先假定装入地址是固定的,然后再根据映像是否允许浮动而作出调整。具体片头数据结构中的p_vaddr提供了映像在连接时确定的装入地址vaddr。如果映像的类型为ET_EXEC,(或者load_addr_set已经被设置成1,见下)那么装入地址就是固定的。而若类型为ET_DYN、即共享库,那么即使装入地址固定也要加上一个偏移量,代码中给出了计算方法,其中ELF_ET_DYN_BASE对于x86定义为(TASK_SIZE / 3 * 2),所以这是2GB边界,而ELF_PAGESTART表示按页面边界对齐。

       确定了装入地址以后,就通过elf_map()、实际上是elf32_map()、建立用户空间虚存区间与目标映像文件中某个连续区间之间的映射。这个函数基本上就是do_mmap(),其返回值就是实际映射的(起始)地址。对于类型为ET_EXEC的可执行程序映像而言,代码中的load_bias是0,所以装入的起点就是映像自己提供的地址vaddr。另一方面,对于ET_EXEC,由于参数中的elf_flags中的MAP_FIXED标志位为1,所以给定的映射地址是刚性的而不容许变通,如果与已经映射的区间有冲突就以失败告终。不过,目标映像的映射是从一片空白开始的,所以实际上不可能失败。顺便提一下,现在又多了一种ELF格式的目标映像,称为FDPIC,其装入地址就是可浮动的。

       即使总的装入地址是浮动的,一旦装入了第一个Segment以后,下一个Segment的装入地址就应该是固定的了,所以这里一方面把load_addr_set设置成1
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值