当前位置: 首页 > 科技观察

Linux应用加载机制——你真的了解吗?

时间:2023-03-13 03:07:25 科技观察

前戏我们都知道Linux应用程序(可执行文件)可以在shell中启动,那么可执行文件是如何在shell中“执行”的呢?本文尽量少用源码,以免太枯燥,主要是讲解过程,废话不多说开始吧。我们先来看图。1、父进程的行为:复制,等待应用程序执行的方式有很多种,从shell执行是比较常见的情况。交互式shell是一个进程(所有进程都是由pid号为1的init进程fork出来的,这个话题涉及到linux的启动和初始化,还有空闲进程等,我们会找一篇文章讲),当用户在shell中输入./test执行程序时,shell先fork()出一个子进程(这也是很多文章中提到的子shell),wait的子进程()结束,所以当测试执行结束时,返回到shell等待用户输入(如果创建了所谓的后台进程,shell不会等待子进程结束,而是直接继续执行)。所以shell进程的主要工作就是复制一个新的进程,等待它结束。2、子进程的行为:“执行”应用程序2.1execve()另一方面,子进程中会调用execve()进行负载测试并开始执行。这是执行测试的关键。下面我们详细分析一下。什么是execve()?execve()是操作系统提供的一个非常重要的系统调用。在很多文章中被称为exec()系统调用(注意与shell内部的exec命令不同)。事实上,Linux中并没有exec()系统调用,exec只是用来描述一组函数,它们都是以exec开头,分别是:#includeintexecl(constchar*path,constchar*arg,...);intexeclp(constchar*file,constchar*arg,...);intexecle(constchar*path,constchar*arg,...,char*constenvp[]);intexecv(constchar*path,char*constargv[]);intexecvp(constchar*file,char*constargv[]);intexecve(constchar*路径,char*constargv[],char*constenvp[]);这些都是libc中封装好的库函数,***通过系统调用execve()(#define__NR_evecve11,11号系统调用)实现的。exec函数的作用是执行当前进程中的可执行文件,即根据指定的文件名找到可执行文件,并用它来替换当前进程的内容,而且这种替换是不可逆的,即就是,被替换的内容不再是Save,当可执行文件结束时,整个过程就卡住了。因为当前进程的代码段、数据段、栈都被新的内容替换了,所以exec函数族的函数执行成功后不会返回,失败则返回-1。可执行文件可以是二进制文件或可执行脚本文件。两者的加载略有不同。这里主要分析二进制文件的操作。2.2do_execve()在用户态调用execve(),触发系统中断后,在内核态执行的对应函数为do_sys_execve(),do_sys_execve()会调用do_execve()函数。do_execve()会先读取可执行文件,如果可执行文件不存在就会报错。然后检查可执行文件的权限。如果当前用户无法执行该文件,execve()将返回-1并报告权限被拒绝错误。否则,继续读入运行可执行文件所需的信息(参见structlinux_binprm)。execve()->do_sys_execve()->do_execve()(checkiffileexistandifcanberunedbycurrentuser)2.3search_binary_handler()然后系统调用search_binary_handler(),根据可执行文件的类型(如shell,a.out,ELF等)找到对应的处理等)函数(系统为每种文件类型创建一个structlinux_binfmt,串在一个链表上,执行时遍历链表,找到对应类型的结构体。如果要自己定义一个可执行文件格式,您还需要实现这个处理程序)。然后执行对应的load_binary()函数开始加载可执行文件。2.4load_elf_binary()加载elf类型文件的handler是load_elf_binary(),它先读取ELF文件头,然后根据ELF文件头信息读取各种数据(头信息)。再次扫描程序段描述表,找到类型为PT_LOAD的段,映射(elf_map())到内存的固定地址。如果没有动态链接器的描述段,则将返回的入口地址设置为应用入口。完成这个功能的是start_thread(),start_thread()并不启动一个线程,只是用来修改pt_regs中保存的PC等寄存器的值,使其指向加载应用程序的入口。这样,当内核运行结束,回到用户态后,接下来执行的就是应用程序了。2.5load_elf_interp()如果应用中使用了动态链接库,就没那么简单了。除了加载指定的可执行文件外,内核还将控制权交给动态链接器(程序解释器,linux中的ld.so)来处理动态链接的程序。内核查找段表,找到标记为PT_INTERP的段对应的动态链接器的名称,并使用load_elf_interp()加载其映像,并将返回的入口地址设置为load_elf_interp()的返回值,即动态链接器入口。当execve退出时,动态链接器继续运行。动态链接器检查应用程序对共享链接库的依赖性,在需要时加载它,并重新定位程序的外部引用。然后动态链接器将控制权交给应用程序,从ELF文件(一种文件格式,我们将在单独的一期中单独解释)头部定义的程序入口点开始执行。(比如test.c中使用了userlib.so中的函数foo(),在编译时将这些信息放入ELF文件test中,对应的语句就变成了callfakefoo()。加载test时知道foo()是外部调用,求助于动态链接器,加载userlib.so,解析foo()函数的地址,将fakefoo()重定向到foo(),所以调用foo()成功。)总结简单来说,整个在shell中输入./test执行应用程序的过程是:当前shell进程fork出一个子进程(subshel??l),子进程使用execve脱离与父进程的关系,加载测试文件(ELF格式)存入内存。如果测试使用了动态链接库,则需要加载动态链接器(或程序解释器),进一步将测试使用的动态链接库加载到内存中,重新定位以供测试调用。***从test的入口地址开始执行test。然而,出于性能和其他原因,现代动态链接器使用延迟加载和延迟解析技术。当调用链接库(待加载)中的函数时,会解析出该函数的起始地址,供调用者使用。动态链接器的实现相当复杂。出于性能等原因,直接操作堆栈被广泛使用。