加载可执行文件到进程实体
上一篇初步了解了fork创建子进程的一些知识:http://www.cnblogs.com/zhouyoulie/archive/2013/05/28/3104001.html,但是仔细想想fork只是克隆了一个新的进程,该进程的代码数据等信息跟父进程完全是一样的,如何才能创建一个“其他”进程呢?答案就是exec函数族,可以用它做fork创建的子进程中做与父进程不一样的事。
exec函数族顾名思义是一组函数,它包括6个函数:
#include <unistd.h> extern char **environ; int execl(const char *path, const char *arg, ...); int execlp(const char *file, const char *arg, ...); int execle(const char *path, const char *arg,..., char * const envp[]); int execv(const char *path, char *const argv[]); int execvp(const char *file, char *const argv[]); int execvpe(const char *file, char *const argv[],char *const envp[]);在这六个函数中真正属于系统调用的是execve函数,其他五个都是基于它的库封装函数API。在上一篇中我们有可执行文件runner,是否可以用execl小试一下牛刀加载它呢,有代码如下
1 /***************************************** 2 filename: testfork.c 3 Author: zhouyoulie 4 date: 2013.05.27 5 *****************************************/ 6 #include <stdlib.h> 7 #include <unistd.h> 8 int main() 9 { 10 execl("/home/zhouyou/LinuxOS/exper2/runner","runner",NULL); 11 perror("execl"); 12 exit(1); 13 }
运行它会有如下结果

图1
可以看到生成的二进制可执行文件call起了runner的功效,这就是exec函数族的作用所在了。它根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。这里的可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件。这样一来当系统需要执行一个其他进程时就可以先fork一个进程完了再调用exec+即可实现新进程的调用。
介绍完了exec+以及它的用法我们再来看看系统调用的内部实现吧。
前面说过exec+函数族归根结底其实就是执行execve,看一下do_execve的源码

图1
在do_execve中调用do_execve_common
1 /* 2 * sys_execve() executes a new program. 3 */ 4 static int do_execve_common(const char *filename, 5 struct user_arg_ptr argv, 6 struct user_arg_ptr envp) 7 { 8 struct linux_binprm *bprm; 9 struct file *file; 10 struct files_struct *displaced; 11 bool clear_in_exec; 12 int retval; 13 const struct cred *cred = current_cred(); 14 15 /* 16 * We move the actual failure in case of RLIMIT_NPROC excess from 17 * set*uid() to execve() because too many poorly written programs 18 * don't check setuid() return code. Here we additionally recheck 19 * whether NPROC limit is still exceeded. 20 */ 21 if ((current->flags & PF_NPROC_EXCEEDED) && 22 atomic_read(&cred->user->processes) > rlimit(RLIMIT_NPROC)) { 23 retval = -EAGAIN; 24 goto out_ret; 25 } 26 27 /* We're below the limit (still or again), so we don't want to make 28 * further execve() calls fail. */ 29 current->flags &= ~PF_NPROC_EXCEEDED; 30 31 retval = unshare_files(&displaced); 32 if (retval) 33 goto out_ret; 34 35 retval = -ENOMEM; 36 bprm = kzalloc(sizeof(*bprm), GFP_KERNEL); 37 if (!bprm) 38 goto out_files; 39 40 retval = prepare_bprm_creds(bprm); 41 if (retval) 42 goto out_free; 43 44 retval = check_unsafe_exec(bprm); 45 if (retval < 0) 46 goto out_free; 47 clear_in_exec = retval; 48 current->in_execve = 1; 49 50 file = open_exec(filename); 51 retval = PTR_ERR(file); 52 if (IS_ERR(file)) 53 goto out_unmark; 54 55 sched_exec(); 56 57 bprm->file = file; 58 bprm->filename = filename; 59 bprm->interp = filename; 60 61 retval = bprm_mm_init(bprm); 62 if (retval) 63 goto out_file; 64 65 bprm->argc = count(argv, MAX_ARG_STRINGS); 66 if ((retval = bprm->argc) < 0) 67 goto out; 68 69 bprm->envc = count(envp, MAX_ARG_STRINGS); 70 if ((retval = bprm->envc) < 0) 71 goto out; 72 73 retval = prepare_binprm(bprm); 74 if (retval < 0) 75 goto out; 76 77 retval = copy_strings_kernel(1, &bprm->filename, bprm); 78 if (retval < 0) 79 goto out; 80 81 bprm->exec = bprm->p; 82 retval = copy_strings(bprm->envc, envp, bprm); 83 if (retval < 0) 84 goto out; 85 86 retval = copy_strings(bprm->argc, argv, bprm); 87 if (retval < 0) 88 goto out; 89 90 retval = search_binary_handler(bprm); 91 if (retval < 0) 92 goto out; 93 94 /* execve succeeded */ 95 current->fs->in_exec = 0; 96 current->in_execve = 0; 97 acct_update_integrals(current); 98 free_bprm(bprm); 99 if (displaced) 100 put_files_struct(displaced); 101 return retval; 102 103 out: 104 if (bprm->mm) { 105 acct_arg_size(bprm, 0); 106 mmput(bprm->mm); 107 } 108 109 out_file: 110 if (bprm->file) { 111 allow_write_access(bprm->file); 112 fput(bprm->file); 113 } 114 115 out_unmark: 116 if (clear_in_exec) 117 current->fs->in_exec = 0; 118 current->in_execve = 0; 119 120 out_free: 121 free_bprm(bprm); 122 123 out_files: 124 if (displaced) 125 reset_files_struct(displaced); 126 out_ret: 127 return retval; 128 }
这个过程主要是打开文件,拷贝可执行文件的描述信息等,我们来重点关注一下search_binary_handler,它的代码是这样的
1 int search_binary_handler(struct linux_binprm *bprm) 2 { 3 unsigned int depth = bprm->recursion_depth; 4 int try,retval; 5 struct linux_binfmt *fmt; 6 pid_t old_pid, old_vpid; 7 8 /* This allows 4 levels of binfmt rewrites before failing hard. */ 9 if (depth > 5) 10 return -ELOOP; 11 12 retval = security_bprm_check(bprm); 13 if (retval) 14 return retval; 15 16 retval = audit_bprm(bprm); 17 if (retval) 18 return retval; 19 20 /* Need to fetch pid before load_binary changes it */ 21 old_pid = current->pid; 22 rcu_read_lock(); 23 old_vpid = task_pid_nr_ns(current, task_active_pid_ns(current->parent)); 24 rcu_read_unlock(); 25 26 retval = -ENOENT; 27 for (try=0; try<2; try++) { 28 read_lock(&binfmt_lock); 29 list_for_each_entry(fmt, &formats, lh) { 30 int (*fn)(struct linux_binprm *) = fmt->load_binary; 31 if (!fn) 32 continue; 33 if (!try_module_get(fmt->module)) 34 continue; 35 read_unlock(&binfmt_lock); 36 bprm->recursion_depth = depth + 1; 37 retval = fn(bprm); 38 bprm->recursion_depth = depth; 39 if (retval >= 0) { 40 if (depth == 0) { 41 trace_sched_process_exec(current, old_pid, bprm); 42 ptrace_event(PTRACE_EVENT_EXEC, old_vpid); 43 } 44 put_binfmt(fmt); 45 allow_write_access(bprm->file); 46 if (bprm->file) 47 fput(bprm->file); 48 bprm->file = NULL; 49 current->did_exec = 1; 50 proc_exec_connector(current); 51 return retval; 52 } 53 read_lock(&binfmt_lock); 54 put_binfmt(fmt); 55 if (retval != -ENOEXEC || bprm->mm == NULL) 56 break; 57 if (!bprm->file) { 58 read_unlock(&binfmt_lock); 59 return retval; 60 } 61 } 62 read_unlock(&binfmt_lock); 63 #ifdef CONFIG_MODULES 64 if (retval != -ENOEXEC || bprm->mm == NULL) { 65 break; 66 } else { 67 #define printable(c) (((c)=='\t') || ((c)=='\n') || (0x20<=(c) && (c)<=0x7e)) 68 if (printable(bprm->buf[0]) && 69 printable(bprm->buf[1]) && 70 printable(bprm->buf[2]) && 71 printable(bprm->buf[3])) 72 break; /* -ENOEXEC */ 73 if (try) 74 break; /* -ENOEXEC */ 75 request_module("binfmt-%04x", *(unsigned short *)(&bprm->buf[2])); 76 } 77 #else 78 break; 79 #endif 80 } 81 return retval; 82 }
函数的主体是两层循环,内层循环是对队列formats做循环,如果找到合适文件则将文件装载并运行,否则continue循环。在内层循环中最后如果没有找到合适的运行载体则查看是否能进行动态安装,也就是代码中的request_module函数,从文件中寻找合适的代理人,有的话就将文件加载并重新执行内循环。
最后调用成功之后还会执行acct_update_integrals(current),这个函数主要是设置task_struct中与时间相关的变量,用于以后的调度工作。其实说到底execve的核心就是加载文件函数search_binary_handler,前面主要是做了将文件名从用户空间移到内核空间,打开可执行文件等工作。
最后对可执行程序的加载过程做一下总结
分析加载过程首先要说的是ELF可执行文件,ELF文件是linux文件主要的可执行文件,主要分为三种类型:
1.可重定向文件(Relocatable file):文件保存着代码和适当的数据,用来和其他的目标文件一起创建一个可执行文件或者一个共享目标文件。由编译器和汇编器生成,将由链接器处理。
2.可执行文件(Executable File):文件保存着一个用来执行的程序;该文件指出了exec+如何来创建程序进程映象。所有重定向和符号都解析完成了,如果存在共享库的链接,那么将在运行时解析。
3.共享目标文件(Shared object file):就是所谓的共享库。文件保存着代码和合适的数据,用来被下面的两个链接器链接。第一个是连接编辑器,可以和其他的可重定向和共享目标文件来创建其他的目标文件。第二个是动态链接器,联合一个可执行文件和其他的共享目标文件来创建一个进程映象。包含链接时所需的符号信息和运行时所需的代码。
我们这里主要是分析可执行文件,上述代码中的runner便属于可执行文件。一般的ELF文件包含三个索引表分别为ELF header、Program header table以及Section header table。ELF header处于文件的开端,保存了文件的路线图,用来描述文件的组织情况。Program header table告诉系统如何创建进程映像。用来构造进程映像的目标文件必须具有程序头部表,exec+在利用ELF文件构造进程时就要用到此表。Section header table:包含了描述文件节区的信息,每个节区在表中都有一项,每一项给出诸如节区名称、节区大小这类信息。用于链接的目标文件必须包含节区头部表。
那么具体来说exec+如何加载呢?首先execl会有一个ELF文件的绝对路径,上述代码中的“/home/zhouyou/LinuxOS/exper2/runner”就是的标识可执行文件具体位置的路径。execl函数会调用Libc库中的封装函数execve,接着在内核中会逐层调用如下函数:sys_exec()>do_execve()>search_binary_handler()。search_binary_handler函数会遍历链表formats,找到elf文件的处理函数load_elf_binary。load_elf_binary原型为static int load_elf_binary(struct linux_binprm *bprm, struct pt_regs *regs),在bprm->buf中存放了二进制文件的内容。

浙公网安备 33010602011771号