进程调度之 4:系统调用execve

date: 2014-10-24 12:09java

1 用户空间的编程接口

这部分详情请参考APUE(第2版)第8章。linux

1.1 六种exec函数

有6种不一样的exec函数可供使用,这些函数最终都是经过系统调用execve来实现的:编程

<unistd.h>
    int execl(const char *pathname, const char *arg1, ... /* (char*)0 */ );
    int execlp(const char *filename, const char *arg1, ... /* (char*)0 */ );
    int execle(const char *pathname, const char *arg1, ... 
    												/* (char*)0, char * const *envp */);
    int execv(const char *pathname, char * const argv[]);
    int execvp(const char *filename, char * const argv[]);
    int execve(const char *pathname, char * const argv[], char * const envp[]);

它们间的关系以下图:数组

6种exec函数间的关系

1.2 进程相关的ID

在《进程四要素》中咱们简单看了下task_struct结构,其中有6个与进程相关的ID:xss

ID 意义 备注
uid/gid 实际用户ID/实际组ID 我其实是谁
euid/egid 有效用户ID/有效组ID 我还具备哪些额外的“特权”
suid/sgid 保存的设置用户ID/保存的设置组ID 由exec函数保存

一般进程的有效ID就是用户的实际ID,但当进程执行一个程序文件时(经过execve系统调用),若是可执行文件设置了set-user-ID(设置用户ID)位或set-group-ID(设置组ID)位,那么执行该程序文件的进程,其有效用户ID将被设置为程序文件的全部者ID,其有效组ID将被被设置为程序文件所在组的ID,这样,进程就具备一些额外的“特权“了。同时execve系统调用还会将设置后的有效用户ID保存到“保存的设置用户ID”中(对“保存的设置组ID”也是一样的处理),以方便其余函数使用,好比setuid函数需会根据“保存的设置用户ID”来判断是否能够将进程的有效ID设置为某个指定的用户ID。函数

2 系统调用execve

这部分咱们重点关注下以下问题:布局

  1. 子进程是如何摆脱父进程自立门户的?子进程如何摆脱对父进程用户空间的依赖?
  2. 为何说execve“一去不复返”?即为何execve没法返回到(父进程)用户空间调用execve的地方?那么该系统调用返回到用户空间时,又返回到了哪里?
  3. 有效用户ID及有效组ID的处理。
  4. 传递给execve系统调用的argv如何传递给可执行文件的入口main函数?

这里假定execve执行的程序文件为aout格式的,具体来讲是aout格式中的“非可重入代码”,便可执行程序包含正文段(text)、数据段(data)和未初始化数据段(bss)。虽然aout格式已非主流,elf才是当前流行的可执行程序文件的格式,但elf格式比较复杂,涉及到动态加载(loader)与动态连接(linker),而aout格式相对简单,用来了解上述问题是比较合适的。这些问题的答案一样适用于elf格式(或其余格式)的可执行文件。ui

系统调用execve的内核入口为sys_execve,定义在<arch/kernel/process.c>中:代理

asmlinkage int sys_execve(struct pt_regs regs)
    {
    	int error;
    	char * filename;
    
    	filename = getname((char *) regs.ebx);
    	error = PTR_ERR(filename);
    	if (IS_ERR(filename))
    		goto out;
    	error = do_execve(filename, (char **) regs.ecx, (char **) regs.edx, &regs);
    	if (error == 0)
    		current->ptrace &= ~PT_DTRACE;
    	putname(filename);
    out:
    	return error;
    }

regs.ebx保存着系统调用execve的第一个参数,便可执行文件的路径名。由于路径名存储在用户空间中,这里要经过getname拷贝到内核空间中。getname在拷贝文件名时,先申请了一个page做为缓冲,而后再从用户空间拷贝字符串。为何要申请一个页面而不使用进程的系统空间堆栈?首先这是一个绝对路径名,可能比较长,其次进程的系统空间堆栈大约为7K,比较紧缺,不宜滥用。用完文件名后,在函数的末尾调用putname释放掉申请的那个页面。指针

sys_execve的核心是调用do_execve函数,传给do_execve的第一个参数是已经拷贝到内核空间的路径名filename,第二个和第三个参数仍然是系统调用execve的第二个参数argv和第三个参数envp,它们表明的传给可执行文件的参数和环境变量仍然保留在用户空间中。

2.1 do_execve主要流程

do_execve定义在<fs/exec.c>中。它的主要流程(忽略掉异常状况的处理)以下:

do_execve流程

2.2 linux_binprm结构

可执行文件(目标文件)做为一个文件以外,还有一些其余的专属信息,为了将运行一个可执行文件时所需的信息组织在一块儿,内核定义了linux_binprm结构,其定义以下:

<include/linux/binfmts.h>
    struct linux_binprm{
	    char buf[BINPRM_BUF_SIZE];
	    struct page *page[MAX_ARG_PAGES];
	    unsigned long p; /* current top of mem */
	    int sh_bang;
	    struct file * file;
	    int e_uid, e_gid;
	    kernel_cap_t cap_inheritable, cap_permitted, cap_effective;
	    int argc, envc;
	    char * filename;	/* Name of binary */
	    unsigned long loader, exec;
    };

buf用来从可执行文件中读入前128个字节,据此能够判断处可执行文件的类型(好比aout、elf、java、或者脚本等)。

page是一个物理页面指针数组,这些物理页面用来存储execve系统调用中参数argv以及envp所指向的字符串表。数组的size为MAX_ARG_PAGES(32),但具体会分配多少个物理页面,取决于argv已经envp所指向的字符串表的大小。

p用来指向page数组所表明的存储空间的“游标”。

file便可执行文件对应的文件表项。

当可执行文件设置了set-user-ID或者set-group-ID,e_uid和e_gid分别用来存储可执行文件的全部者ID和所在组ID.

filename指向可执行文件的路径(该路径字符串已经拷贝到内核空间)。

2.3 linux_binfmt结构以及search_binary_handler

每一种可执行文件都有对应的“装载器”,用来处理可执行文件的加载甚至是连接,此即linux_binfmt结构。其定义以下:

<include/linux/binfmts.h>
    struct linux_binfmt {
	    struct linux_binfmt * next;
	    struct module *module;
	    int (*load_binary)(struct linux_binprm *, struct  pt_regs * regs);
	    int (*load_shlib)(struct file *);
	    int (*core_dump)(long signr, struct pt_regs * regs, struct file * file);
	    unsigned long min_coredump;	/* minimal dump size */
    };

其中关键的是几个函数指针,顾名思义,load_binary用来加载可执行文件;load_shlib用来加载共享库;而core_dump用来生成转储文件。

不一样的“加载器”经过next指针构成一个链表,链表头即为formats。

每一个加载器就像是内核为每种格式的可执行文件设置的代理人,每当执行一个可执行文件时,内核遍历formats中的每一个代理人,查看该可执行文件是否归某个代理人处理,若是对上了号,代理人则“认领”该可执行文件,负责后续的加载、执行等事务。这就是search_binary_handler函数的主要工做工程。但具体状况比这复杂,须要考虑内核还没有为某种格式的可执行文件设置代理人的情形。

aout格式对应的inux_binfmt结构为aout_format,其定义以下:

<fs/binfmt_aout.c>
    static struct linux_binfmt aout_format = {
	    							NULL, 
	    							THIS_MODULE, 
	    							load_aout_binary, 
		    						load_aout_library, 
			    					aout_core_dump, 
			    					PAGE_SIZE
    };

可见aout类可执行文件的加载函数为load_aout_binary,这是流程图中的重点。

2.4 目标文件在内存中的布局以下图所示:

进程用户空间的布局

2.5 start_thread

在可执行文件加载完成,而且传递给main函数的argc和argv参数处理完毕后,load_aout_binary调用start_thread来设置子进程返回用户空间后的入口(即main函数)以及用户空间堆栈的栈顶指针。

start_thread(regs, ex.a_entry, current->mm->start_stack);

start_thread的实现以下:

<include/asm/processor.h>
    #define start_thread(regs, new_eip, new_esp) do {		\
	    __asm__("movl %0,%%fs ; movl %0,%%gs": :"r" (0));	\
	    set_fs(USER_DS);					\
	    regs->xds = __USER_DS;					\
	    regs->xes = __USER_DS;					\
	    regs->xss = __USER_DS;					\
	    regs->xcs = __USER_CS;					\
	    regs->eip = new_eip;					\
	    regs->esp = new_esp;					\
    } while (0)

可见,这里将aout文件的入口ex. a_entry写进eip,而将准备好argc以及argv以后用户空间堆栈的栈顶current->mm->start_stack写进esp,这样当从系统调用返回到子进程的用户空间中时,将从aout文件的入口main函数开始执行,而且经过esp能够获取传递给main函数的argc和argv参数。

相关文章
相关标签/搜索