execve系统调用

sys_execve()

在真正的开始执行系统调用函数之前,系统调用服务程序已经将一些系统调用的函数的参数传递给了相应的寄存器,比如这里的ebx,ecx,edx都分别保存了系统调用的参数,ebx保存的是第一个参数,依次类推(当然最多传递的参数个数不能大于5个),首先这个函数通过ebx获取需要执行的文件的绝对路径,他通过这样一个函数实现
filename = getname((char __user *) regs.ebx);
	error = PTR_ERR(filename);



获取到文件名之后他就会调用do_execve();

do_execve()

ecx,edx寄存器就是传递给可执行文件的参数指针和环境变量参数指针,这里会生成一个记录可执行文件的信息的结构体--struct linux_binprm,结构如下:

struct linux_binprm{
	char buf[BINPRM_BUF_SIZE];
	struct page *page[MAX_ARG_PAGES];
	struct mm_struct *mm;
	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;
	void *security;
	int argc, envc;
	char * filename;	/* Name of binary as seen by procps */
	char * interp;		/* Name of the binary really executed. Most
				   of the time same as filename, but could be
				   different for binfmt_{misc,script} */
	unsigned interp_flags;
	unsigned interp_data;
	unsigned long loader, exec;
};



这个结构体记录可执行文件的信息,用于比对可执行文件的格式找到相应的加载函数,如a.out就调用load_aout_binary()函数开始通过 linux_binprm结构体里面的信息准备此程序的执行工作


这个函数里面会调用open_exec()函数通过获取到的filename将文件打开,并且通过寄存器ecx,edx指向的参数先通过count()计算函数参数的个数和环境变量的个数,然后根据路径和个数从用户空间拷贝过来保存在这个结构体里面,同时还要拷贝可执行文件头部128字节的信息到这个结构体里面的buff中(可执行文件的头部包含了大量的可执行文件的信息,如:文件的类型,文件的大小,代码段在本文件中的偏移量等信息,这些信息后边都要用到),这个时候通过用户传递的信息已经全部从用户空间读取到了这个内核linux_binprm这个临时结构体中,此时就要做的工作就是:在内核支持的可执行文件的对应的加载函数队列中循环遍历,找到适合此文件类型的加载函数(通过search_binary_handle()实现搜索),如a.out格式的文件的加载函数是:load_aout_binary()
注释:内核中有一个formats队列,相当于各种文件的代理人,由他来通过读进来的128字节头部信息来认领这个可执行文件

load_aout_binary()

到了这个函数就是要建立新进程了,与过去告别的时候了,它通过调用flush_old_exec()函数来独立门户,准备成为一个独立的进程,比如更新PCB,更新信号处理表,内存等(如果这里的信号处理表是通过指针共享父进程的信号处理表,这里就要对其进行复制)。

flush_old_exec()

这个函数就专门负责与过去告别,他先调用exec_mmap()函数将内存结构释放,他将从父进程复制过来的(fork())mm_struct下的vm_area_struct全部释放掉,但是如果通过vfork()到此步骤就不需要释放(因为vfork()他全部是通过指针共享父进程的mm_struct的等这种结构),这里有人可能会有疑问,问什么从fork()复制过来的大部分信息在这里要全部释放掉呢,这样从某种角度可能看提高了fork()的效率,因为fork()之后不一定要调用exec ve()函数,到了这里非替换不可时在替换,其实提高了fork()的效率。当然要是调用execve()这里是很浪费效率的,于是就有了vfork()他就是通过共享指针共享父进程的结构这里就不需要释放,大大提高了效率,虽然vm_area_struct不用释放,但是mm_struct结构也是共享,此时要独立就需要自己有一个独立的,因此要开辟一个mm_struct结构,同时释放父进程mm_struct的引用计数(如果释放之后mm_struct的引用计数变为零了,就表明这个结构没有进程使用了,就需要释放他,这里用mmdrop()函数实现释放),当然对于vfork()的父进程的mm_struct是不可能为零的,此时就成为了真正的进程,还需要把其从父进程的线程组中脱离出来,这个通过de_thread()实现,此时vfork()之后调用的execve()就成为了一个进程了。
虚拟内存反面独立之后就完成了一大步了,这里就是信号的独立了,信号处理表是从父进程复制过来的,而信号处理表指向的信号服务程序有三种方式,他们分别是:忽视此信号,采用默认的方式,采用进程注册的用户处理方法,但是从父进程复制过来的信号处理表指向父进程的用户注册的函数,并不在次进程空间,因此这 里要调用flush_signal_handle()函数来遍历一遍信号处理表将第三种方式的处里信号的方式改为默认的,这里完成了信号的独立。
最后就是关闭从父进程复制过来的文件系统,将父进程打开的文件在这里关闭,这里可以保留0,1,2,三个文件不关闭(stdin,stdout,stderr)

这里就完全和父进程独立成为了一个真正的进程了,然后就是建立可执行文件和虚拟内存之间的映射,建立vm_area_struct结构,将text,data,bss,建立起虚拟映射,同时他还要将在运行main()函数的参数和环境变量建立起映射,这个都在linux_binprm结构中,到了这里execve()函数的任务也就完成了

在真正执行main()函数之前,例如gcc他会先执行真正的其实起始函数_start(),他通过libc_start_main()来调用main()函数,在调用main()函数之前,他会初始化IO,堆等执行环境,这样在执行main()的时候用户才可以尽情使用malloc,printf,scanf等操作,main()执行完之后,他会调用_exit()结束进程的生命(释放内存等),他不带返回值,因为他是进程结束前最后一个函数。



猜你喜欢

转载自blog.csdn.net/crazy_to_imagine/article/details/75898917