• Linux中ELF文件启动过程


    linux注册支持运行的文件类型

    struct linux_binfmt {
        struct list_head lh;
        struct module *module;
        int (*load_binary)(struct linux_binprm *);    
        int (*load_shlib)(struct file *);
        int (*core_dump)(struct coredump_params *cprm);
        unsigned long min_coredump;     /* minimal dump size */
     };
    

    linux主要的可执行文件类型为ELF,当然linux实际是并不止可以运行ELF文件这一种文件格式,实际其还可以运行coff等文件格式的可执行文件。
    Linux中所有支持的可执行文件类型在内核中都有一个对应的linux_binfmt类型的对象。所有的Linux_binfmt对象都保存在一个双向链表中,此链表第一个元素的地址保存在formatis内核全局变量中。(可以通过register_fmt和unregister_fmt从添加中插入和删除一个Linux_binfmt对象)

    load_binary      //装入并执行新程序
    load_shlib       //装入共享库
    core_dump        //崩溃时进行内存转储
    

    每一个linux_binfmt对象都需要包含3个函数指针,用于对此类型的可执行文件进行进行相关操作,供操作系统调用。load_binary()负责装载此文件类型的二进制可执行文件并执行,load_shlib()用来装入这种文件类型的共享库,core_dump()则会在崩溃时对此文件类型进行转储。

    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可执行文件类型对应的linux_binfmt如上,其中其.load_binary函数指针为load_elf_binary()函数。当我们运行ELF文件时就是由load_elf_binary()加载并启动此ELF文件。

    ELF文件启动过程

    在linux中运行一个ELF可执行文件通常通过shell命令行。而shell命令行程序实际会先调用fork() 复制一个当前进程的副本为新的进程,fork()从内核中返回会返回两次,分别在父进程和子进程中各返回一次(子进程返回值为1,父进程中返回值为子进程PID)。子进程fock()返回后会继续调用exec()进入到内核中, exec()对应的系统调用为do_execve()。总的来说exec()作用就是清空新创建的进程的.text,.data,.bss段等,然后装载新进程并运行。

    int do_execve(struct filename *filename,
    	const char __user *const __user *__argv,
    	const char __user *const __user *__envp)
    {
    	struct user_arg_ptr argv = { .ptr.native = __argv };
    	struct user_arg_ptr envp = { .ptr.native = __envp };
    	return do_execveat_common(AT_FDCWD, filename, argv, envp, 0);
    }
    
    
    static int do_execveat_common(int fd, struct filename *filename,
    			      struct user_arg_ptr argv,
    			      struct user_arg_ptr envp,
    			      int flags)
    {
    	return __do_execve_file(fd, filename, argv, envp, flags, NULL);
    }
    
    

    do_execve函数会调用do_execveat_common函数,而do_execveat_common()又会进一步调用__do_execve_file()函数去加载ELF并执行。
    AT_FDCWD是一个宏,标识当前目录的文件描述符,所以__do_execve_file()去搜索ELF文件就是相对于当前路径。

    int do_execve_file(struct file *file, void *__argv, void *__envp)
    {
    	struct user_arg_ptr argv = { .ptr.native = __argv };
    	struct user_arg_ptr envp = { .ptr.native = __envp };
    
    	return __do_execve_file(AT_FDCWD, NULL, argv, envp, 0, file);
    }
    

    除了do_execveat_common()函数会调用__do_execve_file()函数去加载ELF并执行外,do_execve_file()函数也会调用__do_execve_file(),其用于User Mode Helper,是内核主动执行应用程序的一种机制。

    struct linux_binprm {
    	char buf[BINPRM_BUF_SIZE];        //用于保存ELF文件的前128个字节
    #ifdef CONFIG_MMU
    	struct vm_area_struct *vma;       //新进程默认栈空间的线性区间地址描述符(相当于windows中的VAD)
    	unsigned long vma_pages;
    #else
    # define MAX_ARG_PAGES	32
    	struct page *page[MAX_ARG_PAGES];
    #endif
    	struct mm_struct *mm;            //指向新进程内存地址描述符
    	unsigned long p;                 //默认栈的栈顶地址
    	unsigned long argmin; 
    	unsigned int
    		called_set_creds:1,
    		cap_elevated:1,
    		secureexec:1;
    #ifdef __alpha__
    	unsigned int taso:1;
    #endif
    	unsigned int recursion_depth; 
    	struct file * file;              //ELF文件的文件指针
    	struct cred *cred;	
    	int unsafe;		
    	unsigned int per_clear;	
    	int argc, envc;                  //程序的argc和envc
    	const char * filename;	         //ELF文件的路径
    	const char * interp;	         //链接器的路径
    	unsigned interp_flags;
    	unsigned interp_data;
    	unsigned long loader, exec;
    
    	struct rlimit rlim_stack; 
    } __randomize_layout;
    

    linux_binprm 结构体用于在加载ELF文件之前保存文件的一些基本信息

    static int __do_execve_file(int fd, struct filename *filename,
    			    struct user_arg_ptr argv,
    			    struct user_arg_ptr envp,
    			    int flags, struct file *file)
    {
            int retval;
    	char *pathbuf = NULL;
    	struct linux_binprm *bprm;
    	struct files_struct *displaced;          //当前进程task_struct的files字段
                                                     //(PCB进程控制块task_struct的files字段指向当前进程打开的文件表)
    	
    
            //将父进程task_struct->files中保存的打开文件的描述符复制一份到当前进程(子进程)的task_struct->files中
    	retval = unshare_files(&displaced);
            //为bprm申请内存用于保存加载的二进制可执行文件的信息到linux_binprm结构体中
    	bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);
            //当前进程的in_execve标志,表示do_execve()已经调用
    	current->in_execve = 1;
            
            //打开ELF文件
            file = do_open_execat(fd, filename, flags);
    	
    	
    	//将ELF文件文件指针file,文件路径filename保存到bprm中
    	bprm->file = file;
    	if (!filename) {
    		bprm->filename = "none";
    	} else if (fd == AT_FDCWD || filename->name[0] == '/') {
    		bprm->filename = filename->name;
    	} else {
    		if (filename->name[0] == '\0')
    			pathbuf = kasprintf(GFP_KERNEL, "/dev/fd/%d", fd);
    		else
    			pathbuf = kasprintf(GFP_KERNEL, "/dev/fd/%d/%s",
    					    fd, filename->name);
    		
    		bprm->filename = pathbuf;
    	}
            //interp链接器的文件路径先暂时设置为ELF文件的路径
    	bprm->interp = bprm->filename;
    
    
            //bprm_mm_init调用mm_alloc()分配一个内存描述符mm_struct并赋值给bprm->mm, 用来描述新进程的整个0-3G的内存空间信息。 
            //调用vm_area_alloc()分配一个vm_area_struct线性区间描述符并赋值给bprm->vma中,用来描述新进程初始栈的内存空间信息。(相当于windows中的虚拟地址描述符VAD)
            //调用instert_vm_struct将新进程初始栈的vm_area_struct插入到进程内存描述符mm->mmap链表中(此链表是一个红黑树,相当于windows中的VAD树)
    	retval = bprm_mm_init(bprm);
    	
    
            //设置bprm->argv和envp字段
    	retval = prepare_arg_pages(bprm, argv, envp);
    	
            //调用bprm_fill_uid()设置进程的用户ID(UID),用户组ID(GID)等信息
            //调用kernel_read()将ELF文件头前128个字节读取到bprm->buf中
    	retval = prepare_binprm(bprm);
    	
            //将ELF文件路径,envp和argv拷贝到bprm->p指向的栈顶中,bprm->p会发生变化
    	retval = copy_strings_kernel(1, &bprm->filename, bprm);
    	retval = copy_strings(bprm->envc, envp, bprm);
    	retval = copy_strings(bprm->argc, argv, bprm);
    	
    
            //内部会加载并执行ELF文件
    	retval = exec_binprm(bprm);
    	
    
            //新进程创建完成,释放无关内存
    	/* execve succeeded */
    	current->fs->in_exec = 0;
    	current->in_execve = 0;
    	task_numa_free(current);
    	free_bprm(bprm);
    	kfree(pathbuf);
            return retval;
    }
    

    __do_execve_file的主要核心代码如上。

    • unshare_files:将父进程task_struct->files中保存的打开文件的描述符复制一份到当前进程(子进程)的task_struct->files中
    • kzalloc:为bprm申请内存用于保存加载的二进制可执行文件的信息到linux_binprm结构体中
    • 将ELF文件文件指针file,文件路径filename保存到bprm中,interp链接器的文件路径先暂时设置为ELF文件的路径
    • bprm_mm_init(bprm):1. bprm_mm_init调用mm_alloc()分配一个内存描述符mm_struct并赋值给bprm->mm, 用来描述新进程的整个0-3G的内存空间信息。
      调用vm_area_alloc()分配一个vm_area_struct线性区间描述符并赋值给bprm->vma中,用来描述新进程初始栈的内存空间信息。(相当于windows中的虚拟地址描述符VAD)
      调用instert_vm_struct将新进程初始栈的vm_area_struct插入到进程内存描述符mm->mmap链表中(此链表是一个红黑树,相当于windows中的VAD树)
    • prepare_arg_pages(bprm, argv, envp):设置bprm->argv和envp字段
    • prepare_binprm(bprm):调用kernel_read()将ELF文件头前128个字节读取到bprm->buf
    • exec_binprm(bprm):内部会调用search_binary_handler(),search_binary_handler会进一步加载并执行ELF文件

    exec_binprm(bprm) 主要就是调用 search_binary_handler()

    • search_binary_handler会调用list_for_each_entry枚举formatis链表中的linux_binfmt对象,
    • 调用linux_binfmt.load_binary加载ELF文件知道能加载成功。对于ELF文件来说linux_binfmt.load_binary就是函数load_elf_binary()

    调用load_elf_binary

    load_elf_binary()会先检查加载的文件头是否为ELF magic,文件类型是否为ET_EXEC(可执行文件)或者ET_DYN(共享文件)类型。

    接着会获取各个program header table并得到PT_INTERP类型。

    利用readelf -l查看elf文件的program信息,PT_INTERP类型program保存的是此ELF文件需要加载的加载器路径。
    只要ELF文件需要进行动态链接其他库就需要PT_INTERP类型的program,如果在链接时传入链接参数-static则会静态链接所有的库也就不需要此program了(因为其不需要动态链接,所以不需要加载对应的linker加载器)。

    得到ELF待执行文件的PT_INTERP类型的program后,其会将对应路径的加载器的ELF文件头加载到内存中保存在loc->interp_elf_ex中

    接着会判断加载的interpreter是否是标准的ELF文件,并将其e_phoff程序头的文件偏移保存在interp_elf_phdata中

    接着遍历待执行ELF文件的所有program,并加载所有PT_LOAD类型的program。对于ET_EXEC类型的文件采用默认的加载基地址0,而对于ET_DYN需要计算的到其随机加载基地址。

            //遍历ELF待执行文件的program程序段
    	for(i = 0, elf_ppnt = elf_phdata;
    	    i < loc->elf_ex.e_phnum; i++, elf_ppnt++) {
    		int elf_prot = 0, elf_flags, elf_fixed = MAP_FIXED_NOREPLACE;
    		unsigned long k, vaddr;
    		unsigned long total_size = 0;
                    //如果程序段不是PT_LOAD类型的就不加载到内存中
    		if (elf_ppnt->p_type != PT_LOAD)
    			continue;
    
    		//获得对应PT_LOAD程序段的内存属性
    		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;
    
                    //load_bias为ELF加载到内存的基地址,初始化为0
                    //对于ET_EXEC可执行的文件类型直接使用默认的加载基地址load_bias = 0
    		if (loc->elf_ex.e_type == ET_EXEC || load_addr_set) {
    			elf_flags |= elf_fixed;
    		}
                    //对于ET_DYN可重定位的文件类型(动态库文件),需要计算获得当前程序的实际内存加载基地址load_bias
                    else if (loc->elf_ex.e_type == ET_DYN) {
    			if (elf_interpreter) {
    				load_bias = ELF_ET_DYN_BASE;
    				if (current->flags & PF_RANDOMIZE)
    					load_bias += arch_mmap_rnd();
    				elf_flags |= elf_fixed;
    			} else
    				load_bias = 0;
    
    			load_bias = ELF_PAGESTART(load_bias - vaddr);
    			total_size = total_mapping_size(elf_phdata,
    							loc->elf_ex.e_phnum);
    			if (!total_size) {
    				retval = -EINVAL;
    				goto out_free_dentry;
    			}
    		}
    
                    //将当前段MAP到进程地址空间中,实际内存加载地址为load_bias + vaddr,设置内存属性为elf_port,
    		error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,
    				elf_prot, elf_flags, total_size);
    		
                    
                    //k此时为当前段默认的虚拟地址
    		k = elf_ppnt->p_vaddr;
    		if (k < start_code)
    			start_code = k;        //代码段的起始为最小的段地址
    		if (start_data < k)
    			start_data = k;        //数据段的起始为最大的段地址
    
    
                    //k此时为当前段的文件段尾默认的虚拟地址(也就是说一个PT_LOAD类型的段在磁盘文件中的大小和加载到内存中的大小是不相等的,不是整个段都加在到内存中)        
    		k = elf_ppnt->p_vaddr + elf_ppnt->p_filesz;      
    		if (k > elf_bss)
    			elf_bss = k;          //bss段的起始地址为最大的段的文件段尾
    		if ((elf_ppnt->p_flags & PF_X) && end_code < k)
    			end_code = k;         //代码段的结束地址为最大可执行的段的文件段尾
    		if (end_data < k)
    			end_data = k;         //数据段的结束地址为最大的段的文件段尾
    
                    //k当前为当前段的内存段尾默认的虚拟地址
    		k = elf_ppnt->p_vaddr + elf_ppnt->p_memsz;
    		if (k > elf_brk) {
    			bss_prot = elf_prot;  //bss段的结束地址为最大的段的内存段尾
    			elf_brk = k;
    		}
    	}
    

    刚才计算出的各种段(代码段,数据段,bss段)都是默认的虚拟地址(默认加载基地址为0),所以其对应的实际内存地址都需要加上load_bias实际的内存加载基地址。

            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;
    
    • 继续判断ELF文件是否采用动态链接,也就是含有PT_INTERP段并需要加载interpreter。如果需要就调用load_elf_interp()加载interpreter(其对应的加载流程和ELF上方的PT_LOAD类型的段的加载相似),并返回interpreter的程序入口点作为新进程应用层的入口点。
    • 如果判断ELF文件采用静态链接(编译器参数使用-static),那么则直接返回此ELF文件的程序入口点作为新进程应用层的入口点。

    最后的最后调用start_thread并设置regs寄存器,elf_entry应用层的入口点,bprm->p默认栈的栈顶。

    start_thread()->compat_start_thread()->start_thread_common()设置应用层的ip和sp,以及默认的cs,ds(x86架构而言)

    • android
      对于android而言,如果存在PT_INTERP类型的program,函数就会将其加载到内存中并将应用层入口设置为/system/lib/linker程序的入口(__dl_start)。如果不存在PT_INTERP类型的program也就是编译时采用静态链接,则将应用层入口点设置为ELF文件的入口点,一般为__start(ELF被UPX加壳之后就是这样的)。

    linux版本为5.0

  • 相关阅读:
    2单表CRUD综合样例开发教程
    1代码表样例开发教程
    ProcessOn
    UniEAP Platform V5.0 Unable to compile class for JSP
    Message Unable to compile class for JSP
    UniEAP Platform V5.0建库
    UML_2_浅谈UML的概念和模型之UML九种图
    PPT鼠绘必须掌握的PPT绘图三大核心功能
    PPT添加节
    remap.config文件配置模板
  • 原文地址:https://www.cnblogs.com/revercc/p/16294051.html
Copyright © 2020-2023  润新知