Linux内核装载和启动一个可执行程序

“平安的祝福 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000 ”

理解编译链接的过程和ELF可执行文件格式                                                

                                                   

a.out基本上已经不用了,分成了两种PE和ELF,linux主要用ELF格式。

COFF 目标文件的格式

                                                               

PE 目标文件的格式

ELF 目标文件的格式

    ELF: 可执行连接格式

可执行连接格式是UNIX系统实验室(USL)作为应用程序二进制接口(Application Binary Interface(ABI)而开发和发布的。工具接口标准委员会(TIS)选择了正在发展中的ELF标准作为工作在32位INTEL体系上不同操作系统 之间可移植的二进制文件格式。

在object文件中有三种主要的类型。

* 一个可重定位(relocatable)文件保存着代码和适当的数据,用来和其他的object文件一起来创建一个可执行文件或者是一个共享文件。
* 一个可执行(executable)文件保存着一个用来执行的程序;该文件指出了exec(BA_OS)如何来创建程序进程映象。
* 一个共享object文件保存着代码和合适的数据,用来被下面的两个链接器链接。第一个是连接编辑器,可以和其他的可重定位和共享object文件来创建其他的object。第二个是动态链接器,联合一个可执行文件和其他的共享object文件来创建一个进程映象。

Object文件格式

  Linking 视角                      Execution 视角
  ============                      ==============
  ELF header                        ELF header
  Program header table (optional)   Program header table
  Section 1                         Segment 1
  ...                               Segment 2
  Section n                         ...
  Section header table              Section header table (optional)

一个ELF头在文件的开始,保存了路线图(road map),描述了该文件的组织情况。
sections保存着object 文件的信息,从连接角度看:包括指令,数据,符号表,重定位信息等等。

注意: 虽然图显示出程序头表立刻出现在一个ELF头后,section头表跟着其他section部分出现,事实是的文件是可以不同的。此外,sections和段(segments)没有特别的顺序。只有ELF头(elf header)是在文件的固定位置。

ELF Header

  #define EI_NIDENT       16

  typedef struct {
      unsigned char       e_ident[EI_NIDENT];
      Elf32_Half          e_type;
      Elf32_Half          e_machine;
      Elf32_Word          e_version;
      Elf32_Addr          e_entry;
      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;

* e_entry

  该成员是系统第一个传输控制的虚拟地址,在那启动进程。假如文件没有如何关联的入口点,该成员就保持为0。

sections包含了在一个object文件中的所有信息,除了ELF报头,程序报头表(program header table),和section报头表(section header table)。
此外,object文件的sections满足几天条件:

* 每个在object文件中的section都有自己的一个section的报头来描述它。section头可能存在但section可以不存在。
* 每个section在文件中都占有一个连续顺序的空间(但可能是空的)。
* 文件中的Sections不可能重复。文件中没有一个字节既在这个section中又在另外的一个section中。
* object文件可以有"非活动的"空间。不同的报头和sections可以不覆盖到object文件中的每个字节。"非活动"数据内容是未指定的。

链接是一个收集、组织程序所需的不同代码和数据的过程,以便程序能被装入内存并被执行。
链接过程分为两步:
-空间与地址分配
    扫描所有的输入目标文件,获得它们的各个段的长度、属性和位置,并且将输入目标文件中的符号定义和符号引用收集起来,统一放到一个全局符号表。这一步 中,链接器将能获得所有输入目标文件的段长度,并且将它们合并,计算出输出文件中各个段合并后的长度与位置,并建立映射关系。
-符号解析与重定位
  使用上面第一步中收集到的所有信息,读取输入文件中段的数据、重定位信息,并且进行符号解析与重定位、调整代码中的地址等。事实上第二步是链接过程的核心,特别是重定位过程。

查看ELF文件的头部: readelf -h hello

编程使用exec*库函数加载一个可执行文件,动态链接分为可执行程序装载时动态链接和运行时动态链接

 execve系统调用

  • 命令行参数和shell环境,一般我们执行一个程序的Shell环境,我们的实验直接使用execve系统调用。

    • $ ls -l /usr/bin 列出/usr/bin下的目录信息

    • Shell本身不限制命令行参数的个数, 命令行参数的个数受限于命令自身

      • 例如,int main(int argc, char *argv[])

      • 又如, int main(int argc, char *argv[], char *envp[])

    • Shell会调用execve将命令行参数和环境参数传递给可执行程序的main函数

      • int execve(const char * filename,char * const argv[ ],char * const envp[ ]);

      • 库函数exec*都是execve的封装例程

    •     #include <stdio.h>
          #include <stdlib.h>
          #include <unistd.h>
          int main(int argc, char * argv[])
          {
              int pid;
              /* fork another process */
              pid = fork();
              if (pid<0) 
              { 
                  /* error occurred */
                  fprintf(stderr,"Fork Failed!");
                  exit(-1);
              } 
              else if (pid==0) 
              {
                  /*   child process   */
                  execlp("/bin/ls","ls",NULL);
              } 
              else 
              { 
                  /*     parent process  */
                  /* parent will wait for the child to complete*/
                  wait(NULL);
                  printf("Child Complete!\n");
                  exit(0);
              }
          }

      命令行参数和环境串都放在用户态堆栈中

    • 实验截图
  • 装载时动态链接和运行时动态链接应用举例

  • 动态链接分为可执行程序装载时动态链接和运行时动态链接,如下代码演示了这两种动态链接。

     文件在附件:https://files.cnblogs.com/files/pingandezhufu/SharedLibDynamicLink.zip

    • 准备.so文件

    shlibexample.h (1.3 KB) - Interface of Shared Lib Example

    shlibexample.c (1.2 KB) - Implement of Shared Lib Example

    编译成libshlibexample.so文件

  •  

        $ gcc -shared shlibexample.c -o libshlibexample.so -m32

    dllibexample.h (1.3 KB) - Interface of Dynamical Loading Lib Example

    dllibexample.c (1.3 KB) - Implement of Dynamical Loading Lib Example

    编译成libdllibexample.so文件

  •     $ gcc -shared dllibexample.c -o libdllibexample.so -m32
    • 分别以共享库和动态加载共享库的方式使用libshlibexample.so文件和libdllibexample.so文件

    main.c  (1.9 KB) - Main program

    编译main,注意这里只提供shlibexample的-L(库对应的接口头文件所在目录)和-l(库名,如libshlibexample.so去掉lib和.so的部分),并没有提供dllibexample的相关信息,只是指明了-ldl

  •     $ gcc main.c -o main -L/path/to/your/dir -lshlibexample -ldl -m32
        $ export LD_LIBRARY_PATH=$PWD #将当前目录加入默认路径,否则main找不到依赖的库文件,当然也可以将库文件copy到默认路径下。
        $ ./main
    

使用gdb跟踪分析一个execve系统调用内核处理函数sys_execve

可执行程序的装载

命令行参数和shell环境,一般我们执行一个程序的Shell环境,我们的实验直接使用execve系统调用。

  Shell本身不限制命令行参数的个数, 命令行参数的个数受限于命令自身

  例如,int main(int argc, char *argv[])

  又如, int main(int argc, char *argv[], char *envp[])

Shell会调用execve将命令行参数和环境参数传递给可执行程序的main函数

  int execve(const char * filename,char * const argv[ ],char * const envp[ ]);

  库函数exec*都是execve的封装例程

  sys_execve内部会解析可执行文件格式(在fs/exec.c)

SYSCALL_DEFINE3(execve,
        constchar __user *, filename,
        constchar __user *const __user *, argv,
        constchar __user *const __user *, envp)
{
    return do_execve(getname(filename), argv, envp);
}

从这个函数得知sys_execve调用do_execve,do_execve -> do_execve_common -> exec_binprm -> search_binary_handler;

exec_binprm对可执行文件处理;

search_binary_handler符合寻找文件格式对应的解析模块,如下:

    1369    list_for_each_entry(fmt, &formats, lh) {
    1370        if (!try_module_get(fmt->module))
    1371            continue;
    1372        read_unlock(&binfmt_lock);
    1373        bprm->recursion_depth++;
    1374        retval = fmt->load_binary(bprm);
    1375        read_lock(&binfmt_lock);

对于ELF格式的可执行文件fmt->load_binary(bprm);执行的应该是load_elf_binary其内部是和ELF文件格式解析的部分需要和ELF文件格式标准结合起来阅读。

load_elf_binary ->  start_thread;

Linux内核是如何支持多种不同的可执行文件格式的?(fs/binfmt_elf.c)

    82static struct linux_binfmt elf_format = {
    83  .module     = THIS_MODULE,
    84  .load_binary    = load_elf_binary,
    85  .load_shlib = load_elf_library,
    86  .core_dump  = elf_core_dump,
    87  .min_coredump   = ELF_EXEC_PAGESIZE,
    88};

在下面的函数中完成注册elf_format

    2198static int __init init_elf_binfmt(void)
    2199{
    2200    register_binfmt(&elf_format);
    2201    return 0;
    2202}

于是在fmt->load_binary(bprm);执行的应该是load_elf_binary,从而实现多态机制。

 实验截图:

刚开始执行exec测试:

设置断点:

运行调试:

特别关注新的可执行程序是从哪里开始执行的?为什么execve系统调用返回后新的可执行程序能顺利执行?

  • 庄生梦蝶 —— 醒来迷惑是庄周梦见了蝴蝶还是蝴蝶梦见了庄周?

  • 庄周(调用execve的可执行程序)入睡(调用execve陷入内核),醒来(系统调用execve返回用户态)发现自己是蝴蝶(被execve加载的可执行程序)

  • 修改int 0x80压入内核堆栈的EIP

  • load_elf_binary ->  start_thread

//修改int 0x80压入内核堆栈的EIP,并将内核的堆栈进行切换,方便返回用户态时能够开始执行新的这个程序
void
start_thread(struct pt_regs *regs, unsigned long new_ip, unsigned long new_sp) { set_user_gs(regs, 0); regs->fs = 0; regs->ds = __USER_DS; regs->es = __USER_DS; regs->ss = __USER_DS; regs->cs = __USER_CS; regs->ip = new_ip;//new_ip返回用户态的第一条指令,在linux下通常是0x804800a+偏移 regs->sp = new_sp; regs->flags = X86_EFLAGS_IF; /* * force it to the iret return path by making it look as if there was * some work pending. */ set_thread_flag(TIF_NOTIFY_RESUME); }

 在load_elf_binary 中查看执行动态链接时的代码:

if (elf_interpreter) {
        unsigned long interp_map_addr = 0;

        elf_entry = load_elf_interp(&loc->interp_elf_ex,
                        interpreter,
                        &interp_map_addr,
                        load_bias);//返回的是动态链接库的起点,负责解析当前的动态链接库,递归地广度遍历/加载依赖的动态链接库
        if (!IS_ERR((void *)elf_entry)) {
            /*
             * load_elf_interp() returns relocation
             * adjustment
             */
            interp_load_addr = elf_entry;
            elf_entry += loc->interp_elf_ex.e_entry;
        }
        if (BAD_ADDR(elf_entry)) {
            retval = IS_ERR((void *)elf_entry) ?
                    (int)elf_entry : -EINVAL;
            goto out_free_dentry;
        }
        reloc_func_desc = interp_load_addr;

        allow_write_access(interpreter);
        fput(interpreter);
        kfree(elf_interpreter);
    } else {
        elf_entry = loc->elf_ex.e_entry;//静态链接完的入口地址
        if (BAD_ADDR(elf_entry)) {
            retval = -EINVAL;
            goto out_free_dentry;
        }
    }

动态链接主要时用户态的动态连接器来负责完成的。内核态只是加载其可执行的程序。

 

参考文献:

http://jzhihui.iteye.com/blog/1447570

posted @ 2015-04-19 22:01  pingandezhufu  阅读(232)  评论(0编辑  收藏  举报