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文件启动过程

do_execve

在linux中运行一个ELF可执行文件通常通过shell命令行。而shell命令行程序实际会先调用fork() 复制一个当前进程的副本为新的进程,fork()从内核中返回会返回两次,分别在父进程和子进程中各返回一次(子进程返回值为0,父进程中返回值为子进程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,是内核主动执行应用程序的一种机制。

__do_execve_file

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;
	//当前进程task_struct的files字段
	//PCB进程控制块task_struct的files字段指向当前进程打开的文件表
	struct files_struct *displaced;          
                                                 
    //将父进程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树也就是EPROCESS的ActiveProcessLinks链表)
	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):
    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此时为当前段的文件段尾默认的虚拟地址        
	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

posted @ 2022-05-20 23:29  怎么可以吃突突  阅读(2498)  评论(0编辑  收藏  举报