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

父进程 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, ®s);
//释放路径名副本
	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 ; ii_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
//删除改行的空格以及制表符,替换为0
		while (cp > bprm.buf) {
			cp--;
			if ((*cp == ' ') || (*cp == '\t'))
				*cp = '\0';
			else
				break;
		}
//检查文件名,前面两个为#!
		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,coff
	do {
		//函数指针赋值
		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
现在来讲讲上面 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 内核设计的艺术》






你可能感兴趣的:(OpenSource,Linux,Kernel)