履行文件是如安在shell中被履行的。本文中尽可能少用一些源码,以免太过于无聊,首要讲清这个进程,感兴趣的同学能够去查看相应的源码了解更多的信息。
1.父进程的行为: 仿制,等候
履行应用程序的方法有许多,从shell中履行是一种常见的状况。交互式shell是一个进程(一切的进程都由pid号为1的init进程fork得到,关于这个论题涉及到Linux发动和初始化,以及idle进程等,有空再说),当在用户在shell中敲入./test履行程序时,shell先fork()出一个子进程(这也是许多文章中说的子shell),而且wait()这个子进程完毕,所以当test履行完毕后,又回到了shell等候用户输入(假如创立的是所谓的后台进程,shell则不会等候子进程完毕,而直接持续往下履行)。所以shell进程的首要作业是仿制一个新的进程,并等候它的完毕。
2.子进程的行为: 履行应用程序
2.1 execve()
另一方面,在子进程中会调用execve()加载test并开端履行。这是test被履行的要害,下面咱们详细剖析一下。
execve()是操作体系供给的非常重要的一个体系调用,在许多文章中被称为exec()体系调用(留意和shell内部exec指令不一样),其实在Linux中并没有exec()这个体系调用,exec仅仅用来描绘一组函数,它们都以exec最初,分别是:
#include
int execl(const char *path, const char *arg, …);
int execlp(const char *file, const char *arg, …);
int execle(const char *path, const char *arg, …, char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
这几个都是都是libc中经过包装的的库函数,最终经过体系调用execve()完结(#define __NR_evecve 11,编号11的体系调用)。
exec函数的作用是在当时进程里履行可履行文件,也便是依据指定的文件名找到可履行文件,用它来替代当时进程的内容,而且这个替代是不可逆的,即被替换掉的内容不再保存,当可履行文件完毕,整个进程也随之僵死。由于当时进程的代码段,数据段和仓库等都现已被新的内容替代,所以exec函数族的函数履行成功后不会回来,失利是回来-1。可履行文件既能够是二进制文件,也能够是可履行的脚本文件,两者在加载时略有不同,这儿首要剖析二进制文件的运转。
2.2 do_execve()
在用户态下调用execve(),引发体系中断后,在内核态履行的相应函数是do_sys_execve(),而do_sys_execve()会调用do_execve()函数。do_execve()首先会读入可履行文件,假如可履行文件不存在,会报错。然后对可履行文件的权限进行查看。假如文件不是当时用户是可履行的,则execve()会回来-1,报permission denied的过错。不然持续读入运转可履行文件时所需的信息(见struct linux_binprm)。
2.3 search_binary_handler()
接着体系调用search_binary_handler(),依据可履行文件的类型(如shell,a.out,ELF等),查找到相应的处理函数(体系为每种文件类型创立了一个struct linux_binfmt,并把其串在一个链表上,履行时遍历这个链表,找到相应类型的结构。假如要自己界说一种可履行文件格局,也需求完结这么一个handler)。然后履行相应的load_binary()函数开端加载可履行文件。
2.4 load_elf_binary()
加载elf类型文件的handler是load_elf_binary(),它先读入ELF文件的头部,依据ELF文件的头部信息读入各种数据(header information)。再次扫描程序段描绘表,找到类型为PT_LOAD的段,将其映射(elf_map())到内存的固定地址上。假如没有动态链接器的描绘段,把回来的进口地址设置成应用程序进口。完结这个功用的是start_thread(),start_thread()并不发动一个线程,而仅仅用来修改了pt_regs中保存的PC等寄存器的值,使其指向加载的应用程序的进口。这样当内核操作完毕,回来用户态的时分,接下来履行的便是应用程序了。
2.5 load_elf_interp()
假如应用程序中运用了动态链接库,就没有那么简略了,内核除了加载指定的可履行文件,还要把控制权交给动态衔接器(program interpreter,ld.so in linux)以处理动态链接的程序。内核搜索段表,找到标记为PT_INTERP的段中所对应的动态衔接器的称号,并运用load_elf_interp()加载其映像,并把回来的进口地址设置成load_elf_interp()的回来值,即动态链接器进口。当execve退出的时分动态链接器接着运转。动态衔接器查看应用程序对同享衔接库的依赖性,并在需求时对其进行加载,对程序的外部引证进行重定位。然后动态衔接器把控制权交给应用程序,从ELF文件头部中界说的程序进入点开端履行。(比方test.c中运用了userlib.so中函数foo(),在编译的时分这个信息被放进了test这个ELF文件中,相应的句子也变成了call fakefoo()。当加载test的时分,知道foo()是一个外部调用,所以求助于动态链接器,加载userlib.so,解析foo()函数地址,然后让fakefoo()重定向到foo(),这样call foo()就成功了。)
简略的说,整个在shell中键入./test履行应用程序的进程为:当时shell进程fork出一个子进程(子shell),子进程运用execve来脱离和父进程的联系,加载test文件(ELF格局)到内存中。假如test运用了动态链接库,就需求加载动态链接器(或许叫程序解说器),进一步加载test运用到的动态链接库到内存,偏重定位以供test调用。最终从test的进口地址开端履行test。
PS: 现代的动态链接器由于功能等原因都采用了推迟加载和推迟解析技能,推迟加载是动态衔接库在需求的时分才被加载到内存空间中(经过页面反常机制),推迟解析是指到动态链接库(以加载)中的函数被调用的时分,才会去把这个函数的开始地址解析出来,供调用者运用。动态链接器的完结适当的杂乱,为了功能等原因,对仓库的直接操作被很多运用,感兴趣的能够找相关的代码看看。