Linux系统下ELF文件装入与执行过程分析
平时在linux控制台下执行gcc编译过的文件时,经常在想:为什么下./hello后程序会自动运行?这个elf可执行文件是如何装载到内存中的? 运行的流程是什么样的呢? 建议作为一名程序员还是需要从原理上了解其执行过程,对于以后问题的排查一定的帮助作用。
Linux系统下ELF文件的装载和执行过程是一个比较漫长和复杂的过程,涉及到用户空间和内核空间的工作和切换,以下以在bash控制台下运行hello程序为为例具体分析其流程:
#./hello
1. bash进程启动shell做如下几件事:
---命令解析:Shell会解析你输入的命令,包括带的参数;
---程序查找:Shell会在文件系统中指定路径下查找对应的程序文件;
---权限检查:Shell会检查文件是否有执行该程序的权限。如果你没有执行权限,shell将不会启动该程序
2. 通过检查后,bash进程调用fork()系统调用创建一个新的进程;
可以查看bash源码execute_cmd.c中,流程如下:
execute_command -->execute_simple_command --->fork 创建新的子进程用于来处理程序;
3. 新的进程调用execve()系统调用执行指定路径的ELF文件
1) 新的进程调用execve()系统调用执行指定路径的ELF文件,原先的bash进程继续返回等待刚才启动的新进程结束,然后继续等待用户输入命令。
2)execve()系统调用相应的入口是sys_execve(),sys_execve()进行一些参数的检查复制后,调用do_execve(),最终会调用到通用函数do_execveat_comman。
3)大致的函数调用流程如下(先有个大概的了解):
4. 具体的系统调用流程: ----kernel/fs/exec.c
sys_execve -->do_execve() --->do_execveat_common 最终都会调用到这里:
---file = do_open_execat(fd, filename, flags); //打开指定路径的elf文件
---retval = bprm_mm_initibprm); //初始化二进制elf加载器
---retval = prepare_binprm(bprm); //填充bin的binprm参数,权限检查,读取文件头的128个字节
---retval = copy_strings_kernel(1, &bprm->filename, bprm); //将文件名从用户空间复制到内核空间
---retval = copy_strings(bprm->envc, envp, bprm); //将用户空间的环境变量复制到内核空间中
---retval = copy_strings(bprm->argc, argv, bprm); ////将用户空间的运行命令行参数复制到内核空间中
---would_dump(bprm, bprm->file); //不太清楚,应该与权限处理有关
---retval = exec_binprm(bprm); //执行可执行文件,并进行文件格式的遍历
5. 轮询所有注册过的文件格式(如:elf,a.out, flat,script等)加载适合的elf载器;
inux提供来了一种可执行文件类型的注册机制,核心数据结构是struct linux_binfmt :
1)a.out文件格式;
2)elf文件格式:
所有的可执行文件格式通过register函数注册后信息都存放在formats全局结构体中进行管理;
3)script脚本程序:
retval = fmt->load_binary(bprm);
search_binary_handle() 通过魔数确定文件格式,并调用相应的装载过程;如果魔数不匹配,直接返回尝试下一个;
6. ELF文件解析和加载(重点分析!!) load_elf_binary()----/fs/binfmt_elf.c中
具体的elf文件格式说明请参考另一篇文章介绍说明:
stepwalker:Linux系统下ELF文件格式学习笔记
---概括起来可以分以下几步:
1).读取并检查目标可执行程序的头信息,检查完成后加载目标程序的程序头表;
2).如果需要解释器则读取并检查解释器的头信息,检查完成后加载解释器的程序头表;
3).装入目标程序的段segment, 这是目标程序二进制代码中的真正可执行映像;
4).修改程序的入口地址,如果有解释器则填入解释器的入口地址, 否则直接填入可执行程序的入口地址;
5). 调用create_elf_tables填写目标文件的参数环境变量等必要信息;
6). start_kernel宏准备进入新的程序入口;
---详细分析如下:
1)先读取elf文件头数据:
loc->elf_ex = *((struct elfhdr *)bprm->buf);
2)检查ELF文件的前4个字节魔数是否匹配;
if (memcmp(loc->elf_ex.e_ident, ELFMAG, SELFMAG) != 0)
3)检查ELF文件类型是否为ET_EXEC和ET_DYN(可执行和动态库);
if (loc->elf_ex.e_type != ET_EXEC && loc->elf_ex.e_type != ET_DYN)
4)检查支持的系统架构:
if (!elf_check_arch(&loc->elf_ex))
5)检查文件内存映射的函数是否存在,如果文件属于ext文件系统,那么该函数指针最终指向generic_file_mmap()函数;
if (!bprm->file->f_op->mmap)
6)再读取程序头数据:
elf_phdata = load_elf_phdrs(&loc->elf_ex, bprm->file);
7)寻找和处理PT_INTERP解释器段(动态链接相关)
---分配虚拟内存空间,大小为动态库路径的字符长度;
elf_interpreter = kmalloc(elf_ppnt->p_filesz, GFP_KERNEL);
---从elf文件中读取对应的字段;
kernel_read(bprm->file, elf_ppnt->p_offset, elf_interpreter, elf_ppnt->p_filesz);
---打开动态库文件:
interpreter = open_exec(elf_interpreter);
---把动态库数据填充到loc结构体中的interp_elf_ex地址待用;
retval = kernel_read(interpreter, 0,
(void *)&loc->interp_elf_ex,
sizeof(loc->interp_elf_ex));
8)检查并读取解释器的程序表头:
9)设置虚拟内存空间中的内存映射区,并初始化栈地址,设置栈底;
至此我们已经把目标执行程序和其所需要的解释器加载初始化,并且完成检查工作,也加载了程序头表program header table,下面开始加载程序的段信息;
10)装入目标程序的段segment
这段代码以目标映像的程序头中搜索类型为PT LOAD的段Sement) ,在二进制像中,只有类型为PT LOAD的段才是需要装入的,当然在装入之前,需要确定装入的地址,只要考虑的就是页面对齐,还有该段的D vadd域的值(上面省路汶部分内容),确定了装入地后,就通过e ma 建立用户空拟地让空间与目标决文件中某个连续区间之间的映射,其返回值就是实际映射的起始地址。
---搜索需要装入的PT_LOAD段 :
---检查地址和页面的信息,包括虚拟地址和大小;
---虚拟地址空间与目标文件的映射确定了装入地址后,就通过elf map()建立用户空间虚拟地址空间与目标映像文件中某个连续区间之间的映射,其返回值就是实际映射的起始地址
error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt, elf_prot, elf_flags, total_size);
---检查代码段和数据段的大小 有没有超过内存大小,必须保证映射的内存大小大于合并的段大小;
----调用set_brk可以有效地映射我们需要的页面,用于BSS和break部分
retval = set_brk(elf_bss, elf_brk, bss_prot);
10)装载动态库:
---如果需要装入解释器,就通过load_elf_intep装入其映像,并把将来进入用户空间的入口地址设置成ad ef intep的返回值,即解释器映像的入口地址;
---若不装入解释器,那么这个入口地址就是目标映像本身的入口地址
以下即为整个文件装载到进程中的内存映射区的布局图:
11)填写目标文件的参数环境变量等必要信息:
在完成装入,启动用户空间的映像运行之前,还需要为目标映像和解释器准备好一些有关的信息,这些信息包括常规的agcEVC等等,还有一些”辅助向量(AuxilaryVector)"这些信息需要复制到用户空间, 使它们在CPU进入解释器或目标映像的程序入口时出现在用户空间堆找上,这里的就通过load_elf_intep就起着这作用;
12)启动start_thread宏准备进入新的程序入,
start_thread(regs, elf_entry, bprm->p);这个函数操作会将eip和esp改成新的地址,
就使得CPU在返回用户空间时就进入新的程序入口。如果存在动态库镜像,那么这就是动态库镜像的
入口地址,否则就是目标镜像文件的程序入口;
regs参数对应的内容及栈顺序如下图:
7. 调用结束,返回用户态:
上述步骤执行完,返回do_execve再返回至sys_execve()时,系统调用的返回地址改成了被装载的ELF程序的入口地址了,所以当sys_execve()系统调用从内核态返回到用户态时,EIP寄存器直接跳转到了ELF程序的入口地址,于是新的程序开始执行,ELF可执行文件装载完成。
补充说明:
1)bash进程:就是shell的进程,每一个已登录的用户都有bash这个进程,当用户在终端上面登录后,Linux系统就会给这个用户一个shell交互框,这个shell就是bash进程;
2)如何判断是否有动态链镜像?
如果目标镜像与各种库的链接是静态链接,就无需依靠动态共享库,那就不需要解释器镜像; 否则就一定要有解释器映像存在。也可通过ldd命令查看关联的动态库。