一、目的
本文将讲述进程如何加载并执行一个二进制文件。
进程被创建完成后,如果需要执行自己的程序,需要做两件事情:从文件系统中读取二进制文件;识别该二进制文件的格式并加载到进程内存地址空间。
为什么需要识别二进制文件的格式呢?因为linux支持的二进制文件格式不止一种,例如:ELF、BASH等。不同的二进制文件格式,需要调用不同的解析程序识别并加载该二进制文件到内存中。
sys_execve()负责二进制文件的读取、识别和加载;sys_execve()的实现可以在fs/exec.c文件中找到。
二、读取二进制文件
定义structlinux_binprm结构体,记录二进制文件的重要信息:二进制头128字节、文件名、进程名、环境变量个数、命令行参数个数等。
sys_execve()->do_execve()->do_execve_common()负责加载二进制文件:
1、调用unshare_files()为进程复制一份文件表;
2、调用kzalloc()分配一份structlinux_binprm结构体;
3、调用open_exec()查找并打开二进制文件;
4、调用sched_exec()找到最小负载的CPU,用来执行该二进制文件;
5、根据获取的信息,填充structlinux_binprm结构体中的file、filename、interp成员;
6、调用bprm_mm_init()创建进程的内存地址空间,并调用init_new_context()检查当前进程是否使用自定义的局部描述符表;如果是,那么分配和准备一个新的LDT;
7、填充structlinux_binprm结构体中的argc、envc成员;
8、调用prepare_binprm()检查该二进制文件的可执行权限;最后,kernel_read()读取二进制文件的头128字节(这些字节用于识别二进制文件的格式及其他信息,后续会使用到);
9、调用copy_strings_kernel()从内核空间获取二进制文件的路径名称;
10、调用copy_string()从用户空间拷贝环境变量和命令行参数;
至此,二进制文件已经被打开,structlinux_binprm结构体中也记录了重要信息;下面需要识别该二进制文件的格式并最终运行该文件。
三、识别二进制文件
每种格式的二进制文件对应一个structlinux_binprm结构体,load_binary成员负责识别该二进制文件的格式;内核使用链表组织这些structlinux_binfmt结构体,链表头是formats。
接着do_execve_common()继续往下看:
11、调用search_binary_handler()在formats的链表中找到该二进制文件的处理函数,并执行该函数;(例如,ELF文件的处理函数是load_elf_binary())
12、释放structlinux_binprm结构体占用的内存空间。
四、ELF格式的二进制文件
通常加载的二进制文件都是ELF格式的,因此下面讲述ELF格式的二进制文件处理过程。(注:此处需要一些ELF文件格式的只是,可以参考ELF规范。)
1、从structlinux_binprm结构体的buf成员中拷贝ELF头到缓冲区,并检查该文件是否是可执行文件或共享文件;
2、调用elf_check_arch()检查ELF头记录的系统体系结构与当前系统的体系结构是否相同(例如,x86、arm等);
3、计算ELF文件structelf32_phdr结构数组的长度(即e_phensize* e_phnum = total program header)并分配内存;
4、调用kernel_read()从二进制文件中读取programheader到内存中;
5、如果该二进制文件中包含PT_INTER类型的programheader,那么打开该programheader指向的文件(ELF程序解析器,该文件也是二进制文件)并读取该解析程序的ELF头;
6、调用flush_old_exec()复制父进程的信号处理表、用户空间等资源;至此,新进程与父进程就没有什么共享资源了;
7、调用setup_new_exec()设置新进程的名称、清空信号处理表、关闭已经打开的文件;
8、调用setup_arg_pages()分配命令行参数和环境变量的地址空间;
9、处理二进制文件中类型为PT_LOAD的programheader,装入text、data段;
10、调用set_brk()分配bss段空间;
11、根据前期获取的ELF程序解析器,加载“解析器”二进制文件;
12、调用create_elf_tables()初始化命令行参数和环境变量;
13、设置新进程的text、data、bss段的起始地址和终止地址;
14、调用start_kernel()启动新进程(首先执行的是“解析器”程序);
总之,load_elf_binary()依次处理ELF的programheader,并为新进程分配独占的资源,最后启动新进程。
五、总结
linux通过sys_execve()系统调用从文件系统中读取、识别并加载二进制文件;根据二进制文件中记录的文件类型,调用相应的处理函数识别、加载二进制文件到新进程的地址空间中,最后启动该新进程。至此,新进程就彻底与父进程毫无“瓜葛”了。
版权声明:
原创作品,如非商业性转载,请注明出处;如商业性转载出版,请与作者联系。