asmlinkage int sys_execve(struct pt_regs regs)
前言

sys_execve()根据参数中指定的二进制文件路径,执行相应的二进制文件。我们可能会疑惑,参数中是一个pt_regs结构,哪里有文件路径?事实上,系统调用也属于中断,而对于系统调用,会将参数地址保存到指定寄存器中,这一事件发生的时间点是已经进入execve()代码,但是还未通过int 0x80中断真正调用sys_execve前,这时会将字符指针放到ebx中。

在随后的int 0x80中断会将FLAGS压栈,SP,IP压栈,然后进入system_call代码区,这里,SAVE_ALL将所有寄存器入栈,注意由于是系统调用,这里的栈已经切换成程序的内核空间栈。而这些保存到栈中的寄存器值包括ebx都将作为sys_execve()的参数regs使用。完成这一系列操作后系统堆栈如下图所示。


这样就到达具体的sys_execve()函数了。

1.预处理

首先在内核空间分配一个物理页面,然后调用do_getname()从用户空间拷贝文件名字符串。

2.调用主体函数do_execve()

2.1我们既然要执行参数中给的二进制文件,首先需要打开文件,获取文件句柄file

2.2然后我们需要一个linux_binprm结构体去保存函数具体的参数信息,包括文件名,argv,envp,还会将文件前128字节读到linux_binprm.buf中。

2.3因为可执行文件的种类很多,比如elf,a.out等格式。我们需要从内核全局linux_binfmt队列中找到一个能够处理参数中所给的可执行文件的linux_binfmt结构,具体就是依次试用linux_binfmt结构中各自的load_binary()函数。

3.可执行文件的装载和投运(a.out为例)
3.1与过去决裂,释放用户空间。

既然是要执行参数中给定的二进制文件,就需要放弃可能从父进程继承下来的用户空间,而使用本进程自己的用户空间。因此,需要检查是否与父进程通过指针共享用户空间,还是之前复制父进程用户空间。如果通过指针共享,说明本进程本身没有自己的用户空间,之前称为“进程”不合适,应该称作线程,就直接申请进程用户空间。如果复制父进程的用户空间,这是就需要全部释放。

3.2装载可执行文件数据段代码段

这时可以将可执行文件装入进程的用户空间了,这时分两种情况:

1.可执行文件不是"纯代码",需要通过do_brk()扩展数据段+代码段大小的空间,然后通过read()读取文件内容到用户空间

2.否则,如果文件系统提供mmap(),并且数据段和代码段长度与页面大小对齐,直接通过文件映射读取到用户空间,否则,通过1方法读取。

3.2装载可执行文件堆栈段和bss段

用户空间堆栈区顶部当然是用户虚存空间顶部,即TASK_SIZE,为3GB,虚存地址为0xC000 0000的位置。

这里主要是设置用户堆栈区,包括envp[],argv[]以及argc

4.start_thread()
#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)

这里主要关注eip和esp。

我们来看怎样调用start_thread()的

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

可以看出start_thread()中的eip来自 ex.a_entry,这就是该二进制文件的代码的起始地址

而esp来自stark_stack,这是设置好用户堆栈的函数参数后的堆栈指针位置。

这样,当cpu从系统调用返回到用户空间时,就从ex.a_entry确定的地址开始执行。

Logo

更多推荐