【Linux 1.0内核源码剖析】执行程序——exec.c

来源:互联网 发布:淘宝实名认证用银行卡 编辑:程序博客网 时间:2024/05/01 01:29

父进程 fork的子进程的目的自然不是创建一个几乎与自己一模一样的进程。而是通过子进程调用 exec 函数簇去执行另外一个程序。exec() 系统调用必须定位该执行文件的二进制映像,加载并执行它。

exec() 的Linux实现支持不同的二进制格式,这是通过 linux_binfmt 结构来达到的,其中内嵌了两个指向函数的指针,一个用于加载可执行文件,另一个用于加载库函数,每种二进制格式都实现有这两个函数。

/* This structure defines the functions that are used to load the binary formats that * linux accepts. */struct linux_binfmt{  int (*load_binary)(struct linux_binprm *, struct  pt_regs * regs);  int (*load_shlib)(int fd);};
Linux的内核是单独实现 sys_execve() 调用的,它执行一个非常简单的任务:加载可执行文件的头部,并试着去执行它。如果头两个字节是 “#!”,那么就会解析该可执行文件的第一行并调用一个解释器来执行它,否则的话,就会顺序地调用各个注册过的二进制格式。
Linux 本身的格式是由 fs/exec.c 直接支持的,并且相关的函数是 load_aout_binary 和 load_aout_library。对于二进制,函数将加载一个“a.out” 可执行文件(Linux 环境下编程 gcc 生成的可执行文件默认就是 a.out)并以使用 mmap() 加载磁盘文件或调用 read_exec() 而结束。

Linux1.0(如无特殊说明,本系列均指Linux1.0 内核环境下)。其执行顺序为: execve() -> sys_execve() -> do_execve()

/* * sys_execve() executes a new program. */ //系统调用的时候,把参数依次放在:ebx,ecx,edx,esi,edi,edp寄存器中asmlinkage int sys_execve(struct pt_regs regs){int error;char * filename;//在系统空间建立一个路径名的副本error = getname((char *) regs.ebx, &filename);if (error)return error;//执行一个新的程序error = do_execve(filename, (char **) regs.ecx, (char **) regs.edx, &regs);//释放路径名副本putname(filename);return error;}

/* * sys_execve() executes a new program. */static int do_execve(char * filename, char ** argv, char ** envp, struct pt_regs * regs){//二进制程序结构声明struct linux_binprm bprm;//用于存放加载二进制文件时用的参数struct linux_binfmt * fmt;//定义了Linux接受的被用于加载二进制格式的函数,为函数结构体指针unsigned long old_fs;int i;int retval;int sh_bang = 0;//如果regs所指的堆栈内容中的cs不是用户态的值,返回if (regs->cs != USER_CS)return -EINVAL;//参数和环境字符串空间中的偏移指针,初始化为最后一个字节处bprm.p = PAGE_SIZE*MAX_ARG_PAGES-4;for (i=0 ; i<MAX_ARG_PAGES ; i++)/* clear page-table */bprm.page[i] = 0;//清页面//打开文件程序,bprm.inode 为对应文件路径名的i 节点指针retval = open_namei(filename, 0, 0, &bprm.inode, NULL);if (retval)return retval;bprm.filename = filename;//文件名bprm.argc = count(argv);//参数个数bprm.envc = count(envp);//环境变量个数restart_interp://判断打开文件的有效性,必须是常规文件,可执行程序,权限允许等,if (!S_ISREG(bprm.inode->i_mode)) {/* must be regular file */retval = -EACCES;goto exec_error2;}if (IS_NOEXEC(bprm.inode)) {/* FS mustn't be mounted noexec */retval = -EPERM;goto exec_error2;}if (!bprm.inode->i_sb) {retval = -EACCES;goto exec_error2;}i = bprm.inode->i_mode;if (IS_NOSUID(bprm.inode) && (((i & S_ISUID) && bprm.inode->i_uid != current->    euid) || ((i & S_ISGID) && !in_group_p(bprm.inode->i_gid))) &&    !suser()) {retval = -EPERM;goto exec_error2;}/* make sure we don't let suid, sgid files be ptraced. *///如果当前进程被置PF_PTRACED位,则设置结构中的u_uid和e_gid为当前进程中的值if (current->flags & PF_PTRACED) {bprm.e_uid = current->euid;bprm.e_gid = current->egid;} else {//否则取前面获得的文件类型和属性值bprm.e_uid = (i & S_ISUID) ? bprm.inode->i_uid : current->euid;bprm.e_gid = (i & S_ISGID) ? bprm.inode->i_gid : current->egid;}//检查将要执行文件的文件权限if (current->euid == bprm.inode->i_uid)i >>= 6;else if (in_group_p(bprm.inode->i_gid))i >>= 3;//判断文件是否具有执行权限if (!(i & 1) &&    !((bprm.inode->i_mode & 0111) && suser())) {retval = -EACCES;goto exec_error2;}//清空结构中的缓冲区memset(bprm.buf,0,sizeof(bprm.buf));//取得fs寄存器值old_fs = get_fs();//重置fs寄存器的内容为内核数据段set_fs(get_ds());//读取可执行文件,文件路径名,参数等信息放入buf中retval = read_exec(bprm.inode,0,bprm.buf,128);//设置fs为老的保留的值,恢复现场set_fs(old_fs);if (retval < 0)goto exec_error2;//读取出错处理//如果执行文件名开始的两个字节为#!,并且sh_bang为0,则处理脚本文件的执行if ((bprm.buf[0] == '#') && (bprm.buf[1] == '!') && (!sh_bang)) {/* * This section does the #! interpretation. * Sorta complicated, but hopefully it will work.  -TYT */char *cp, *interp, *i_name, *i_arg;//释放该执行文件的i 节点iput(bprm.inode);bprm.buf[127] = '\0';//最后位设置0,换句话说读取前128字节,判断文件格式//查找bprm.buf中是否含有换行符,如果没有则让cp指向最后一个字符处if ((cp = strchr(bprm.buf, '\n')) == NULL)cp = bprm.buf+127;*cp = '\0';//如果有则设置为0,找到第一个换行符替换为0//删除改行的空格以及制表符,替换为0while (cp > bprm.buf) {cp--;if ((*cp == ' ') || (*cp == '\t'))*cp = '\0';elsebreak;}//检查文件名,前面两个为#!for (cp = bprm.buf+2; (*cp == ' ') || (*cp == '\t'); cp++);//如果文件名为空,则转到exec_error1处if (!cp || *cp == '\0') {retval = -ENOEXEC; /* No interpreter name found */goto exec_error1;}//让i_name指向程序名,如果有/指向最后一个,(/表示目录,如果有目录程序名则在最后)//第一个字符串表示的脚本解释程序名interp = i_name = cp;i_arg = 0;for ( ; *cp && (*cp != ' ') && (*cp != '\t'); cp++) { if (*cp == '/')i_name = cp+1;}//如果文件名后面还有字符的话,则应该是参数,让i_reg指向它while ((*cp == ' ') || (*cp == '\t'))*cp++ = '\0';if (*cp)i_arg = cp;/* * OK, 我们已经解析出程序的文件名以及(可选)参数 *///若sh_bang标志没有设置,则设置它,并复制指定个数的环境变量串和参数串到环境和参数空间中//sh是程序为脚本程序的标志位if (sh_bang++ == 0) {bprm.p = copy_strings(bprm.envc, envp, bprm.page, bprm.p, 0);bprm.p = copy_strings(--bprm.argc, argv+1, bprm.page, bprm.p, 0);}/* * Splice in (1) the interpreter's name for argv[0]存放解释程序名 *           (2) (optional) argument to interpreter可选参数 *           (3) filename of shell script脚本程序的名字 * * This is done in reverse order, because of how the * user environment and arguments are stored. *///复制脚本程序文件名到参数和环境空间中bprm.p = copy_strings(1, &bprm.filename, bprm.page, bprm.p, 2);bprm.argc++;//复制解释程序的参数到参数和环境空间中if (i_arg) {bprm.p = copy_strings(1, &i_arg, bprm.page, bprm.p, 2);bprm.argc++;}//复制解释程序文件名到参数和环境空间中bprm.p = copy_strings(1, &i_name, bprm.page, bprm.p, 2);bprm.argc++;if (!bprm.p) {retval = -E2BIG;goto exec_error1;}/* * OK, now restart the process with the interpreter's inode. * Note that we use open_namei() as the name is now in kernel * space, and we don't need to copy it. *///打开文件retval = open_namei(interp, 0, 0, &bprm.inode, NULL);if (retval)goto exec_error1;goto restart_interp;}//如果sh_bang标志没有被设置,则复制指定个数的环境变量字符串到参数if (!sh_bang) {bprm.p = copy_strings(bprm.envc,envp,bprm.page,bprm.p,0);bprm.p = copy_strings(bprm.argc,argv,bprm.page,bprm.p,0);if (!bprm.p) {retval = -E2BIG;goto exec_error2;}}//如果sh标志已经被设置了,则表明改程序是脚本程序,这个时候的环境变量页面已经被复制了bprm.sh_bang = sh_bang;fmt = formats;//让fmt执行对应的格式,即下面说明的三种格式之一//让fn指向对应的加载二进制函数//Linux1.0只支持3中二进制文件格式:a.out,elf,coffdo {//函数指针赋值int (*fn)(struct linux_binprm *, struct pt_regs *) = fmt->load_binary;if (!fn)break;//加载程序,对应信息均位于bprm和regs中了retval = fn(&bprm, regs);//如果加载成功了,释放该i节点后返回if (retval == 0) {iput(bprm.inode);current->did_exec = 1;return 0;}fmt++;//遍历下一个格式,下一个就是加载共享库,一共只有两次加载机会} while (retval == -ENOEXEC);//出错处理exec_error2:iput(bprm.inode);exec_error1:for (i=0 ; i<MAX_ARG_PAGES ; i++)free_page(bprm.page[i]);return(retval);}
现在来讲讲上面 do_execve() 函数的执行情况:

1、do_execve() 开始执行后,马上处理参数和环境变量,目的就是要将参数和环境变量加载到内存的页面中,为了对参数和环境变量可能占用的内存页面进行管理。do_execve() 函数先对page管理结构清0(18-19行)。

2、系统想要对文件进行全方位检测,就先要对文件的 i 节点中提供的文件属性信息进行分析,文件自身的所有属性都记载在 i 节点中,其具体步骤是:调用 open_namei() 函数,从虚拟盘上读取文件的 i 节点,该函数是通过文件路径(如“/bin/sh”),最终找到该文件的 i 节点,并将其登记到 i 节点管理表中。(21行)

3、接下来对参数的个数和环境变量的个数进行统计(25-26行),这是因为,参数和环境变量是系统给用户预留的自由设置信息。默认设置是 init/main.c 中的(Linux1.0)

static char * argv_rc[] = { "/bin/sh", NULL };static char * envp_rc[] = { "HOME=/", term, NULL };
4、接下来开始对文件的检测工作,对 i 节点进行分析。通过对 i 节点中“文件属性”的分析,可以得知这个文件是不是一个“常规文件”。只有常规文件(除块设备文件、字符设备文件、目录文件、管道文件等特殊文件外的文件)才有被载入的可能。以及根据 i 节点提供的:inode->i_mode、inode->i_uid、inode->i_gid,检测当前文件是否有执行文件的权限(30-68行)

5、对文件头进行检测,先将 inode对应信息读入缓冲区中(76行),然后对文件头进行分析,如果是脚本文件(82行),进入if 里面执行,解析该可执行文件的第一行(截断了换行符后面的内容)并调用一个解释器来执行它。(88-125行),否则直接执行后面的代码(168行开始)

6、将环境变量和参数拷贝到内存指定的页面中

7、加载二进制格式文件(181-195行)

上面do_execve() 函数中 fmt = formats(178行),这里指明加载方式(Linux1.0 支持三种格式:a.out、elf、coff)

/* Here are the actual binaries that will be accepted  */struct linux_binfmt formats[] = {{load_aout_binary, load_aout_library},#ifdef CONFIG_BINFMT_ELF{load_elf_binary, load_elf_library},#endif#ifdef CONFIG_BINFMT_COFF{load_coff_binary, load_coff_library},#endif{NULL, NULL}};
经过183和187行,这样执行加载对应二进制格式的函数(exec.c中只有load_aout_binary 和 load_aout_library)

如果是之前子进程没有和父进程拉开区别,那么从加载二进制文件开始,子进程就真正开始它的旅途。首先得和父进程“撇清”关系,放弃从父进程继承而来的全部空间,不管是通过复制还是通过共享,统统放弃,建立自己独立的空间

/* * These are the functions used to load a.out style executables and shared * libraries.  There is no binary dependent code anywhere else. *//*该函数用于加载a.out类型的二进制文件格式和共享库。没有任何的二进制依赖代码*/int load_aout_binary(struct linux_binprm * bprm, struct pt_regs * regs){struct exec ex;struct file * file;int fd, error;unsigned long p = bprm->p;//取得执行文件的头ex = *((struct exec *) bprm->buf);/* exec-header *//*继续对文件头部各种信息进行判断*///不是需要分页可执行文件(ZMAGIC)if ((N_MAGIC(ex) != ZMAGIC && N_MAGIC(ex) != OMAGIC &&      N_MAGIC(ex) != QMAGIC) ||    ex.a_trsize || ex.a_drsize ||//代码、数据重定位部分长度不等于0    //可执行文件长度小于代码段+数据段+符号表长度+执行头部分长度的总和    bprm->inode->i_size < ex.a_text+ex.a_data+ex.a_syms+N_TXTOFF(ex)) {return -ENOEXEC;}//执行头部分文件长度限制if (N_MAGIC(ex) == ZMAGIC &&    (N_TXTOFF(ex) < bprm->inode->i_sb->s_blocksize)) {printk("N_TXTOFF < BLOCK_SIZE. Please convert binary.");return -ENOEXEC;}//文件头部分长度不等于一个缓冲块大小,不能执行if (N_TXTOFF(ex) != BLOCK_SIZE && N_MAGIC(ex) == ZMAGIC) {printk("N_TXTOFF != BLOCK_SIZE. See a.out.h.");return -ENOEXEC;}/* 释放从父进程继承来的用户空间,与父进程"决裂"*/flush_old_exec(bprm);//冲刷所有当前执行程序的痕迹,以便新程序开始执行//各种初始化current->end_code = N_TXTADDR(ex) + ex.a_text;//设置当前进程代码段的大小current->end_data = ex.a_data + current->end_code;//设置当前进程数据段的大小current->start_brk = current->brk = current->end_data;//设置当前进程brk段的大小current->start_code += N_TXTADDR(ex);//设置start_code//各种值设置current->rss = 0;current->suid = current->euid = bprm->e_uid;current->mmap = NULL;current->executable = NULL;  /* for OMAGIC files */current->sgid = current->egid = bprm->e_gid;//根据a.out具体的不同格式,以不同的方式装入代码段和数据段//如果代码和数据段是紧跟在头部后面if (N_MAGIC(ex) == OMAGIC) {do_mmap(NULL, 0, ex.a_text+ex.a_data,PROT_READ|PROT_WRITE|PROT_EXEC,MAP_FIXED|MAP_PRIVATE, 0);read_exec(bprm->inode, 32, (char *) 0, ex.a_text+ex.a_data);} else {//如果不是if (ex.a_text & 0xfff || ex.a_data & 0xfff)//没有按页对齐printk("%s: executable not page aligned\n", current->comm);fd = open_inode(bprm->inode, O_RDONLY);//以只读方式打开节点if (fd < 0)return fd;file = current->filp[fd];//让file指向文件描述符指针//如果文件操作函数为空或者 mmap 为空,则关闭上面打开的文件描述符if (!file->f_op || !file->f_op->mmap) {sys_close(fd);do_mmap(NULL, 0, ex.a_text+ex.a_data,PROT_READ|PROT_WRITE|PROT_EXEC,MAP_FIXED|MAP_PRIVATE, 0);read_exec(bprm->inode, N_TXTOFF(ex),  (char *) N_TXTADDR(ex), ex.a_text+ex.a_data);goto beyond_if;}//重新映射该文件error = do_mmap(file, N_TXTADDR(ex), ex.a_text,PROT_READ | PROT_EXEC,MAP_FIXED | MAP_SHARED, N_TXTOFF(ex));//如果返回值不等于文件代码段加载到内存后的地址,则关闭描述符,并发送SIGSEGV信号if (error != N_TXTADDR(ex)) {sys_close(fd);send_sig(SIGSEGV, current, 0);return 0;};//出错则重新映射 error = do_mmap(file, N_TXTADDR(ex) + ex.a_text, ex.a_data,PROT_READ | PROT_WRITE | PROT_EXEC,MAP_FIXED | MAP_PRIVATE, N_TXTOFF(ex) + ex.a_text);sys_close(fd);//同样如果,映射地址出错if (error != N_TXTADDR(ex) + ex.a_text) {send_sig(SIGSEGV, current, 0);return 0;};//executable为进程管理结构中的成员,表示该进程所对应的可执行文件的i节点指针current->executable = bprm->inode;//指向inode,建立关系bprm->inode->i_count++;}beyond_if:sys_brk(current->brk+ex.a_bss);//修改idt表中描述符基地址和段限长p += change_ldt(ex.a_text,bprm->page);p -= MAX_ARG_PAGES*PAGE_SIZE;//在新的用户堆栈中创建环境和参数变量指针表,并返回该堆栈指针p = (unsigned long) create_tables((char *)p,bprm->argc,bprm->envc,0);current->start_stack = p;//设置eip为可执行文件的入口点regs->eip = ex.a_entry;/* eip, magic happens :-) */regs->esp = p;/* stack pointer */if (current->flags & PF_PTRACED)send_sig(SIGTRAP, current, 0);return 0;}
ok,最后大致的总结一下:

1、sys_execve(),系统调用,让进程执行某个程序。首先在系统空间中建立一个路径名的副本,然后带着这个路径名信息去执行do_execve()
上面建立副本是通过 getname() 函数来实现的,该函数首先分配一个物理页面作为缓冲区,然后在系统空间中定义一个缓冲指针指向他,将路径名从用户空间复制到缓冲区,再用缓冲区指针指向这个物理页面。其实就是将用户空间的数据复制到系统空间的过程。

2、do_execve(),首先打开可执行文件(open_namei),然后初始化bprm这个用来保存可执行文件上下文的数据结构,从可执行文件中读取头128个字节到bprm缓冲区,这128个字节包含了关于可执行文件属性的必要信息,然后解析,将其中的文件路径名从系统空间复制到bprm,再将参数,环境变量从用户空间复制到bprm

3、load_aout_binary(),加载二进制格式的可执行文件。这里直接是加载a.out格式的可执行文件。首先进行各种检查,格式对不对,大小对不对,可否执行等等。然后冲刷掉继承而来的父进程的用户空间,开始与父进程真正区别,设置代码段,数据段,bss段等等初始化操作。然后根据a.out具体的不同格式,以不同的方式装入代码段和数据段。映射地址。然后与可执行文件建立关系。

在用户空间的堆栈区顶部建立一个虚拟内存空间,将参数与环境变量所占的物理页面与这些虚拟空间建立映射。

4、调整eip值,使之指向程序的第一条指令处,即程序的入口处。寄存器ip存放的就是下一条将要执行的指令地址。所以下一步系统将依据eip提供的指令地址继续执行,也就是新程序。

ok,fork子进程后,子进程加载二进制格式文件执行新程序的大致流程就是这样了。当然exec.c文件中还有一部分函数,请参考源文件。


参考资料《LInux 内核设计的艺术》






0 0
原创粉丝点击