【Linux】加载elf文件

发布时间 2023-06-02 15:16:51作者: 王磊明

Linux中程序的加载

load_elf_binary,主要负责对可执行文件中elf文件读取,并提前做好相关的内存布局

关于elf文件

  elf文件的全称——Executable and Linking Format。一般来说elf文件规定了二进制程序的组织规范。
  elf文件的信息分布视图

一般而言,其排布顺序是elf header->program header->section header。但是内存信息的排布,并不是唯一的,其内容地址可变。Section Header Table 描述了所有节信息表,而 Program Header Table 其实描述的是所有的 段信息表 。其中一个段是由一个或多个节信息组成。

加载过程中的相关结构体

  • Linux中,会有一个loc结构体

      struct {
      	struct elfhdr elf_ex;//(可执行文件)该结构体通常用于表示可执行程序或共享库文件中的ELF头部信息
      	struct elfhdr interp_elf_ex;//(动态链接器)该结构体通常用于表示动态链接器(即解释器)文件的ELF头部信息。
    	struct exec interp_ex;//该结构体通常用于表示动态链接器的可执行文件格式信息。
          } *loc;
    
    • struct elfhdr包含了头部字段
    struct elfhdr {
          unsigned char e_ident[EI_NIDENT]; //ELF 文件头标识符数组
      	Elf32_Half e_type;            //ELF 文件类型(例如可执行文件、共享库等)
      	Elf32_Half e_machine;         //目标机器类型(例如 x86、ARM 等)
      	Elf32_Word e_version;         //文件版本号  
      	Elf32_Addr e_entry;           //程序入口点地址
      	Elf32_Off e_phoff;            //程序头表(Program Header Table)的偏移量  
      	Elf32_Off e_shoff;            //节头表(Section Header Table)的偏移量    
      	Elf32_Word e_flags;           //处理器相关标志
      	Elf32_Half e_ehsize;          //ELF 文件头的大小    
      	Elf32_Half e_phentsize;       //程序头表中每个条目的大小
      	Elf32_Half e_phnum;           //程序头表中条目的数量  
      	Elf32_Half e_shentsize;       //节头表中每个条目的大小  
      	Elf32_Half e_shnum;           //节头表中条目的数量    
      	Elf32_Half e_shstrndx;        //节名字符串表的索引 
    };
    
    • struct exec 结构体包含了 a.out 文件头部的各个字段,例如魔数、文本段长度、数据段长度、BSS 段长度、符号表等信息。但后来普遍使用elf文件格式,struct exec不常用
  • struct linux_binprm

    当在 Linux 系统上执行可执行文件时,内核需要通过 struct linux_binprm 结构体描述正在被加载和执行的可执行文件的相关信息。下面是该结构体中一些重要的字段的详细解释

    • 相关字段的描述
      字段 描述
      const char *filename 表示可执行文件的路径名,即在命令行中输入的文件名
      struct file *file 表示指向代表可执行文件的 struct file 结构的指针。该结构描述了文件的属性、状态和调用它的进程等信息。
      int argc, char **argv, envc 分别表示命令行参数的个数、指向参数列表的指针数组以及环境变量的个数。
      unsigned long loader 表示用于加载可执行文件的系统调用函数的地址。在 Linux 中,这个系统调用是由 execve 函数实现的。
      unsigned long exec 表示用于执行可执行文件的系统调用函数的地址。在 Linux 中,这个系统调用是由 do_execve_common 函数实现的。
      unsigned long bprm_mm 指向可执行文件的内存映射区域的指针。在执行可执行文件之前,内核需要把可执行文件映射到当前进程的虚拟地址空间中,而 bprm_mm 就记录了该映射区域的信息。
      flags 表示文件打开标志
      cred 可执行文件的用户凭证
  • 内核分配标识

    标识 描述
    GFP_KERNEL 表示内核执行普通内存分配的请求,并且允许在分配期间睡眠
    GFP_ATOMIC 表示内核执行不可中断的、高优先级的内存分配请求。这种类型的请求不能睡眠,因此必须在原子上下文中完成。
    GFP_USER 表示该内存分配请求来自用户空间程序,与 GFP_KERNEL 相比,内核会使用更严格的验证策略来确保分配的内存区域不会被恶意代码滥用。
    GFP_HIGHUSER 类似于 GFP_USER,但是用于大型内存分配请求,可以从用户空间程序中进行。
    GFP_DMA 表示要求内核将分配的内存区域物理地址限制为适合 DMA 使用的范围
    GFP_NOIO 表示内核正在执行一个与 I/O 操作无关的任务,因此应该尽量避免使用可能引起 I/O 操作的内存分配方法。
  • elf文件中的段类型描述

    字段 描述
    PT_NULL 无效类型,表示该条目为空
    PT_LOAD 加载类型,表示该程序头表条目对应的段需要被加载到内存中
    PT_DYNAMIC 动态链接类型,表示该程序头表条目对应的段包含了动态链接信息
    PT_INTERP 解释器类型,表示该程序头表条目对应的段包含了程序解释器路径名
    PT_NOTE 注释类型,表示该程序头表条目对应的段包含了某些调试或其他附加信息
    PT_SHLIB 保留类型,暂未使用
    PT_PHDR 程序头表类型,表示该程序头表条目对应的段包含了整个程序头表本身

load_elf_binary

  • 初步类型检查

    • 通过bprm获取相关头部信息字段
      • bprm->buf是一个指向二进制程序映像的缓冲区的指针。这个缓冲区包含了即将被执行的程序的代码和数据。这里应该就是elf文件中头信息
      struct {
      	struct elfhdr elf_ex;//存储程序的头信息
      	struct elfhdr interp_elf_ex;//存储的解释器的头信息?
      	struct exec interp_ex;//存放了解释器的可执行文件?
      } *loc;
      loc = kmalloc(sizeof(*loc), GFP_KERNEL);//从内核空间中获取空间
      loc->elf_ex = *((struct elfhdr *)bprm->buf);
      
      • 对elf头文件进行相关类型检查
        if (memcmp(loc->elf_ex.e_ident, ELFMAG, SELFMAG) != 0)
          goto out;
          //既不是可执行文件,又不是动态库文件就退出
        if (loc->elf_ex.e_type != ET_EXEC && loc->elf_ex.e_type != ET_DYN)
          	goto out;
          //对elf文件进行架构文件检查,判断该架构是否和当前系统的架构符合,如果一致就进行加载
        if (!elf_check_arch(&loc->elf_ex))
          	goto out;
          //这里判断该可执行文件对应的函数指针是否存在||是否在用户空间和内核空间建立映射关系(两个都要满足)
        if (!bprm->file->f_op||!bprm->file->f_op->mmap)
          	goto out;
        
        • 检测是否可执行文件或动态库文件。不是就out
        • 对架构进行检测,判断是否符合一致
        • 判断bprm的文件是否已分配指针,是否已分配内存地址.都需要满足
  • 段信息的获取

    /* 
     应该这里是对段信息的获取
    */	
    //1. elf可执行文件中程序头表中的一个条目的大小是否正确(应该是每个section的大小?),这里是检测单个的大小
    if (loc->elf_ex.e_phentsize != sizeof(struct elf_phdr))
    	goto out;
    //2. (这里是检测数量)elf文件中程序头条目的数量不能太少,也不能太多
    if (loc->elf_ex.e_phnum < 1 ||
     	loc->elf_ex.e_phnum > 65536U / sizeof(struct elf_phdr))
    	goto out;
    //统一计算段信息的大小
    size = loc->elf_ex.e_phnum * sizeof(struct elf_phdr);
    retval = -ENOMEM;
    //elf_phdata 是一个指向存储读取数据的缓冲区的指针。
    //我理解,就是创建的一个段表
    elf_phdata = kmalloc(size, GFP_KERNEL);//按要求分配内核内存
    if (!elf_phdata)
    	goto out;
    //(bprm->file中存放的是elf文件的指针)通过bprm将相关信息读取到elf_phdata
    retval = kernel_read(bprm->file, loc->elf_ex.e_phoff,
    		     (char *)elf_phdata, size);
    if (retval != size) {
    	if (retval >= 0)
    		retval = -EIO;
    	goto out_free_ph;
    }
    
    • 通过程序头文件,检测每个条目(应该是section)中的大小是否合法
    • 通过程序头文件,检测elf文件中的section数目
    • 根据获取的数量和大小,建立一个段表
    • 通过bprm->file,获取elf文件中的段表,装载到elf_phdata中
  • 保持文件独立性&对文件描述符管理

    //对文件描述符做一些管理,本进程的文件描述独立管理
      files = current->files;	/* Refcounted so ok */
      retval = unshare_files();
      if (retval < 0)
      	goto out_free_ph;
      if (files == current->files) {
      	put_files_struct(files);//这里好像是把files对应的文件进行释放了
      	files = NULL;
      }
      //获取一个新的文件描述符
      retval = get_unused_fd();
      if (retval < 0)
      	goto out_free_fh;
      //增加bprm->file的文件引用次数
      get_file(bprm->file);
      //并将bprm->file加载到进程的文教描述表中去
      fd_install(elf_exec_fileno = retval, bprm->file);
    
    • 对files处理,保证当前进程的文件的文件描述符独立性
    • 将bprm->file加入到本进程的文件描述符表中
  • 找到PT_INTERP,确定解释器路径

    一个被编译为ELF格式的可执行文件在链接时,可以指定其需要依赖某个动态链接库或者使用某种解释器来执行。此时,在ELF文件头中就会有一项PT_INTERP,记录了程序的解释器路径。

    //如果该段信息内容是解释器类型
    if (elf_ppnt->p_type == PT_INTERP) {
        retval = -ENOEXEC;
        //elf_ppnt->p_filesz 表示该段中文件长度的大小
        //对长度有效性进行检测
        if (elf_ppnt->p_filesz > PATH_MAX || 
           elf_ppnt->p_filesz < 2)
              goto out_free_file;
    
        retval = -ENOMEM;
        //根据大小,先获取空间
        elf_interpreter = kmalloc(elf_ppnt->p_filesz,
        		  GFP_KERNEL);
        if (!elf_interpreter)
          goto out_free_file;
      //(elf_ppnt->p_offset给定了偏移)bprm->file中读取解释器数据到elf_interpreter中
        retval = kernel_read(bprm->file, elf_ppnt->p_offset,
        	     elf_interpreter,
        	     elf_ppnt->p_filesz);
        if (retval != elf_ppnt->p_filesz) {
        if (retval >= 0)
        	retval = -EIO;
          goto out_free_interp;
        }
        retval = -ENOEXEC;
        if (elf_interpreter[elf_ppnt->p_filesz - 1] != '\0')
              goto out_free_interp;
      //这里主要是针对一些遗留问题
      //如果满足下面这两个判断,则其是iBCS2镜像文件
        if (strcmp(elf_interpreter,"/usr/lib/libc.so.1") == 0 ||
           strcmp(elf_interpreter,"/usr/lib/ld.so.1") == 0)
              ibcs2_interpreter = 1;
      //如果是iBCS2的镜像文件,则要做一些处理
        SET_PERSONALITY(loc->elf_ex, ibcs2_interpreter);
        //打开ELF解释器,成功则返回相应的文件描述符
        interpreter = open_exec(elf_interpreter);
        //PTR_ERR会检测错误码,判断是否成功读取
        retval = PTR_ERR(interpreter);
        if (IS_ERR(interpreter))
              goto out_free_interp;
      //如果解释器内容不可读,则修改相关标志符
        if (file_permission(interpreter, MAY_READ) < 0)
              bprm->interp_flags |=BINPRM_FLAGS_ENFORCE_NONDUMP;
        //将解释器的信息内容写到bprm->buf中
        retval = kernel_read(interpreter, 0, bprm->buf,
        	     BINPRM_BUF_SIZE);
        if (retval != BINPRM_BUF_SIZE) {
        if (retval >= 0)
        	retval = -EIO;
            goto out_free_dentry;
        }
        //将解释器内容,放置到interp_ex和interp_elf_ex中
        loc->interp_ex = *((struct exec *)bprm->buf);
        loc->interp_elf_ex = *((struct elfhdr *)bprm->buf);
    

栈标志

找到段信息中为栈类型的,根据其标志,来调整相关标志变量

for (i = 0; i < loc->elf_ex.e_phnum; i++, elf_ppnt++)
  if (elf_ppnt->p_type == PT_GNU_STACK) {
      if (elf_ppnt->p_flags & PF_X)//这里表明,该段包含可执行代码段,则需要分配可执行内存空间
  	    executable_stack = EXSTACK_ENABLE_X;
      else
  	    executable_stack = EXSTACK_DISABLE_X;
      break;
}

对解释器进行一致性验证

根据loc中信息,确定解释器的类型

if (elf_interpreter) {//获取到有解释器,进行一致性检测
  static int warn;
  //设置解释器文件的格式:elf或者aout格式
  interpreter_type = INTERPRETER_ELF | INTERPRETER_AOUT;
  //通过loc->interp_ex判断类型,如果不是这些旧有类型,则直接标记为elf文件
  if ((N_MAGIC(loc->interp_ex) != OMAGIC) &&
      (N_MAGIC(loc->interp_ex) != ZMAGIC) &&
      (N_MAGIC(loc->interp_ex) != QMAGIC))
  	interpreter_type = INTERPRETER_ELF;

  //e_ident是ELF头部的标识字段,其大小固定为16个字节。
  //它包含一些重要的信息,如ELF的类别、数据编码方式、ELF版本等
  if (memcmp(loc->interp_elf_ex.e_ident, ELFMAG, SELFMAG) != 0)
  	interpreter_type &= ~INTERPRETER_ELF;
  // 如果类型是aotu类型,就++
  if (interpreter_type == INTERPRETER_AOUT && warn < 10) {
  	printk(KERN_WARNING "a.out ELF interpreter %s is "
  		"deprecated and will not be supported "
  		"after Linux 2.6.25\n", elf_interpreter);
  	warn++;
  }

  retval = -ELIBBAD;
  if (!interpreter_type)
  	goto out_free_dentry;

  /* Make sure only one type was selected */
  if ((interpreter_type & INTERPRETER_ELF) &&
       interpreter_type != INTERPRETER_ELF) {
      		// FIXME - ratelimit this before re-enabling
  	// printk(KERN_WARNING "ELF: Ambiguous type, using ELF\n");
  	interpreter_type = INTERPRETER_ELF;
  }
  /* Verify the interpreter has a valid arch */
  if ((interpreter_type == INTERPRETER_ELF) &&
      !elf_check_arch(&loc->interp_elf_ex))
  	goto out_free_dentry;
  }
  • 这里根据loc->interp_elf中的信息,来确定一下interpreter_type的类型(但作用具体是什么,还没搞懂)

内存布局的设置

  • 布局设置——1
    current->flags &= ~PF_FORKNOEXEC;//允许子进程继承父进程的执行文件标志
    	//mm 表示进程的内存映像区结构体,def_flags 是内存映像区结构体中的一个成员变量
    current->mm->def_flags = def_flags;//
    ......	
    //将当前进程mm的空闲区域的起始地址指向可用于映射文件或共享内存的起始地址
    current->mm->free_area_cache = current->mm->mmap_base;
    current->mm->cached_hole_size = 0;
    retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP),
      				 executable_stack);
    if (retval < 0) {
      	send_sig(SIGKILL, current, 0);
      	goto out_free_dentry;
        }
    //将新进程的堆栈指针设置为bprm可执行文件的程序入口地址
    current->mm->start_stack = bprm->p;
    
    • 设置空闲区域的起始地址--指向用于共享内存的起始地址
    • 进程的栈的起始地址,设置为可执行文件的程序入口地址
  • 布局设置-2

    对进行的段、堆、代码段、数据段地址进行设置

    • 布局的分布
    +----------------------+ <- 0xFFFFFFFF (4GB)
    |        Stack         |
    |                      |
    |                      |
    |                      |
    +----------------------+
    |       Heap           |
    |                      |
    |                      |
    |                      |
    +----------------------+
    |         BSS          |
    +----------------------+
    |        Data          |
    +----------------------+
    |        Code          |
    +----------------------+ <- 0x00000000
    
    
    • 初步得到终止地址的位置
      • 代码段的位置
        • 起始地址为最小的虚址
        • 结束地址为可执行文件中(虚址+文件大小)的最大范围
      • 数据段的位置
        • 起始地址为最大虚址
        • 结束地址为文件在磁盘上的最大范围
      • bss的位置
        • 起始位置:代码中没有明确看到,应该是数据段的终止位置
        • 结束地址:文件在磁盘上的最大范围
      • 堆的位置
        • 起始地址:应该是bss上的终止位置
        • 结束地址:程序加上预留下来的位置信息
      • tips:
        • 不清楚数据段和代码段中以虚址划分的标准是什么
        • 同时按代码逻辑上看的化,end_code可能存在大于start_data
      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;
      	//unlikely这里的意思是:
          //堆的结束地址超出了BSS段的结束地址,那么表达式的结果是真,unlikely()将返回0,反之为1.所以当堆地址小于了段地址,就需要重置了,但一般不会发生
      	if (unlikely (elf_brk > elf_bss)) {
      		unsigned long nbyte;
      		retval = set_brk (elf_bss + load_bias,
      				  elf_brk + load_bias);
      		if (retval) {
      			send_sig(SIGKILL, current, 0);
      			goto out_free_dentry;
      		}
      		nbyte = ELF_PAGEOFFSET(elf_bss);
      		if (nbyte) {
      			nbyte = ELF_MIN_ALIGN - nbyte;
      			if (nbyte > elf_brk - elf_bss)
      				nbyte = elf_brk - elf_bss;
      			if (clear_user((void __user *)elf_bss +
      						load_bias, nbyte)) {
      				/*
      				 * This bss-zeroing can fail if the ELF
      				 * file specifies odd protections. So
      				 * we don't check the return value
      				 */
      			}
      		}
      	}
          //这里对该段数据的读写权限进行设置
      	if (elf_ppnt->p_flags & PF_R)//具有可读权限
      		elf_prot |= PROT_READ;
      	if (elf_ppnt->p_flags & PF_W)//具有写权限
      		elf_prot |= PROT_WRITE;
      	if (elf_ppnt->p_flags & PF_X)//具有可执行权限
      		elf_prot |= PROT_EXEC;
      
      	elf_flags = MAP_PRIVATE | MAP_DENYWRITE | MAP_EXECUTABLE;
      
      	vaddr = elf_ppnt->p_vaddr;//获取该程序段的虚拟起始地址
      	//如果是可执行文件类型
      	if (loc->elf_ex.e_type == ET_EXEC || load_addr_set) {
      		elf_flags |= MAP_FIXED;//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.  */
      		//就是动态链接库会导致程序映射到非默认的地址空间,则要确保程序段和brk指针之间
      		//有足够的空间,避免内存碎片化导致的问题
      		load_bias = ELF_PAGESTART(ELF_ET_DYN_BASE - vaddr);
      	}
      	/*
          进行内存映射
      	bprm->file:可执行文件的文件描述符
      	load_bias + vaddr:表示加载偏移量加上程序段的虚拟内存地址
      	elf_ppnt:程序段头信息的指针
      	elf_prot:段的保护属性
      	elf_flags:包含内存映射的标志
      	load_bias是一个地址偏移量,用于修正动态链接器和程序之间的地址差异。
      	vaddr是程序段的虚拟内存地址,而elf_prot和elf_flags则用于设置程序段的权限和其他属性。
      	*/
      	//将ELF(可执行与链接格式)文件中的程序段映射到进程的虚拟地址空间中
      	error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,
      			elf_prot, elf_flags);//差不多就是将可执行文件中的程序段进行内存映射
      	if (BAD_ADDR(error)) {
      		send_sig(SIGKILL, current, 0);
      		retval = IS_ERR((void *)error) ?
      			PTR_ERR((void*)error) : -EINVAL;
      		goto out_free_dentry;
      	}
      
      	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;
      		}
      	}
          //start_code:最小虚址
      	k = elf_ppnt->p_vaddr;
      	if (k < start_code)
      		start_code = k;
          //start_data:最大虚址
      	if (start_data < k)
      		start_data = k;
              ...
              /*进行内存检查*/
              ...
      	//elf_ppnt->p_filesz表示该段在磁盘上占用的字节数(空间大小)
          //段的结束地址,就是磁盘文件大小的最大位置(不断更新)
      	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;
          //数据段的终止位置
      	//和elf_bss同理
      	if (end_data < k)
      		end_data = k;
      	//elf_ppnt->p_memsz表示程序段的总大小,
      	//与p_filesz相比p_memsz包含了需要在内存中为该段预留的额外空间,空间更大
          //堆的范围,就加上了预留的范围
      	k = elf_ppnt->p_vaddr + elf_ppnt->p_memsz;
      	if (k > elf_brk)
      		elf_brk = k;
      }
      	//统一增加一个偏移量
          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;
          //扩展堆-set_brk会对current->mm->start_brk设置第一个参数是起始地址,第二个参数是结束地址
          retval = set_brk(elf_bss, elf_brk);//堆的范围(elf_bss, elf_brk)
        ...
        ...
          current->mm->end_code = end_code;
          current->mm->start_code = start_code;
          current->mm->start_data = start_data;
          current->mm->end_data = end_data;
          current->mm->start_stack = bprm->p;