Linux程序执行相关函数-exec函数 - STEMHA's Blog

Linux程序执行相关函数-exec函数

类Unix系统提供了一系列函数,这些函数能用可执行文件所描述的新上下文代替进程的上下文。这样的函数名以前缀exec开始,后跟一个或两个字母。

exec族函数

下表列出了exec族函数,它们之间的差别在于如何解释参数。

函数名 路径搜索 命令行参数 环境数组
execl() 列表
execlp() 列表
execle() 列表
execv() 数组
execvp() 数组
execve() 数组

参数说明

第一个参数

每个函数的第一个参数表示被执行文件的路径名。

  • 路径名可以是绝对路径或是当前进程目录的相对路径。
  • 此外,如果路径名中不包含“/”字符,execlp()和execvp()函数就在PATH环境变量指定的所有目录中搜索这个可执行文件。

execl(), execlp()和exec1e()中间的参数

除了第一个参数,execl(), execlp()和exec1e()函数包含的其他参数个数都是可变的。

  • 每个参数指向一个字符串,这个字符串是对新程序命令行参数的描述,正如函数名中“l”字符所隐含的一样,这些参数组织成一个列表,最后一个值为NULL。
  • 通常情况下,第一个命令行参数复制可执行文件名。

execv(), execvp()和execve()的第二个参数

相反,execv(), execvp()和execve()函数指定单个参数的命令行参数,正如函数名中的“v”字符所隐含的一样,这单个参数是指向命令行参数串的指针向量地址

  • 数组的最后一个元素必须存放NULL值。

最后一个参数

execle()和execve()函数的最后一个参数是指向环境串的指针数组的地址:数组的最后一个元素照样必须为NULL。
其他函数对新程序环境参数的访问是通过C库定义的外部全局变量environ进行的。

所有的exec函数(除execve()外)都是C库定义的封装例程,并利用了execve()系统调用,这是Linux所提供的处理程序执行的唯一系统调用

sys_execve()服务

sys_execve()服务例程接收下列参数:

  1. 可执行文件路径名的地址(在用户态地址空间)。
  2. 以NULL结束的字符串指针数组的地址(在用户态地址空间),每个字符串表示一个命令行参数
  3. 以NULL结束的字符串指针数组的地址(也在用户态地址空间)。每个字符串以NAME = value形式表示一个环境变量

sys_execve()执行过程

sys_execve()把可执行文件路径名拷贝到一个新分配的页框。
然后调用do_execve()函数,传递给它的参数为指向这个页框的指针、指针数组的指针及把用户态寄存器内容保存到内核态堆栈的位置。

do_execve()

do_execve()依次执行下列操作:

  1. 动态地分配一个linux_binprn数据结构,并用新的可执行文件的数据填充linux_binprn数据结构

  2. 调用path_lookup(), dentry_open()path_release(),以获得与可执行文件相关的目录项对象、文件对象和索引节点对象。如果失败,则返回相应的错误码。

  3. 检查是否可以由当前进程执行该文件,再检查索引节点的i_writecount字段,以确定可执行文件没被写入;把-1存放在这个字段以禁止进一步的写访问。

  4. 在多处理器系统中,调用sched_exec()函数来确定最小负载CPU以执行新程序,并把当前进程转移过去。

  5. 调用ini_new_context()检查当前进程是否使用自定义局部描述符表。如果是,函数为新程序分配和淮备一个新的LDT。

  6. 调用prepare_binprm()函数填充linux_binprm数据结构,这个函数又依次执行下列操作:

    • 再一次检查文件是否可执行(至少设置一个执行访问权限)。如果不可执行,则返回错误码(因为带有CAP_DAC_OVERRIDE权能的进程总能通过检查,所以第3步中的检查还不够。
    • 初始化linux_binprm结构的e_uide_gid字段,考虑可执行文件的setuid和setgid标志的值。这些字段分别表示有效的用户ID和组ID。也要检查进程的权能。
    • 用可执行文件的前128字节填充linux_binprm结构的buf字段。这些字节包含的是适合于识别可执行文件格式的一个魔数和其他信息。
  7. 文件路径名、命令行参数及环境串拷贝到一个或多个新分配的页框中,最终它们会被分配给用户态地址空间。

  8. 调用search_binary_handler()函数formats链表进行扫描,并尽力应用每个元素的load_binary方法,把linux_binprm数据结构传递给这个函数。只要load_binary方法成功应答了文件的可执行格式,对formats的扫描就终止。

  9. 如果可执行文件格式不在formats链表中,就释放所分配的所有页框并返回错误码 -ENOEXEC,表示Linux不认识这个可执行文件格式。

  10. 否则,函数释放linux_binprm数据结构,返回从这个文件可执行格式的load_binary方法中所获得的代码

load_binary方法

可执行文件格式对应的load_binary方法执行下列操作(假定这个可执行文件所在的文件系统允许文件进行内存映射并需要一个或多个共享库):

  1. 检查存放在文件前128字节中的一些魔数以确认可执行格式。如果魔数不匹配,则返回错误码 -ENOEXEC。

  2. 读可执行文件的首部。这个首部描述程序的段和所需的共享库

  3. 可执行文件获得动态链接程序的路径名,并用它来确定共享库的位置并把它们映射到内存

  4. 获得动态链接程序的目录项对象,也就获得了索引节点对象和文件对象。

  5. 检查动态链接程序的执行许可权

  6. 把动态链接程序的前128字节拷贝到缓冲区

  7. 对动态链接程序类型执行一些一致性检查。

  8. 调用flush_old_exec()函数释放前一个计算所占用的几乎所有资源。这个函数又依次执行下列操作:

    1. 如果信号处理程序的表为其他进程所共享,那么就分配一个新表并把旧表的引用计数器减1;而且它将进程从旧的线程组脱离。这是通过调用de_ thread()函数完成的。
    2. 如果与其他进程共享,就调用unshare_files()拷贝描述进程已打开文件的files_struct结构。
    3. 调用exec_mmap()函数释放分配给进程的内存描述符、所有线性区及所有页框,并清除进程的页表。
    4. 将可执行文件路径名赋给进程描述符的comm字段。
    5. 用flush_thread()函数清除浮点寄存器的值和在TSS段保存的调试寄存器的值。
    6. 调用flush_signal_handlers()函数,用于将每个信号恢复为默认操作,从而更新信号处理程序的表。
    7. 调用flush_old_files()函数关闭所有打开的文件,这些打开的文件在进程描述符的files->close_on_exec字段设置了相应的标志。现在,已经不能返回了,如果真出了差错,这个函数再不能恢复前一个计算
  9. 清除进程描述符的PF_FORKNOEXEC标志。这个标志用于在进程创建时设置进程记账,在执行一个新程序时清除进程记账。

  10. 设立进程新的个性,即设置进程描述符的personality字段。

  11. 调用arch_pick_mmap_layout(),以选择进程线性区的布局

  12. 调用setup_arg_pages()函数为进程的用户态堆栈分配一个新的线性区描述符,并把那个线性区插入到进程的地址空间。setup_arg_pages()还把命令行参数和环境变量串所在的页框分配给新的线性区。

  13. 调用do_map()函数创建一个新线性区来对可执行文件正文段(即代码)进行映射。这个线性区的起始线性地址依赖于可执行文件的格式,因为程序的可执行代码通常是不可重定位的。因此,这个函数假定从某一特定逻辑地址的偏移量开始装入正文段。ELF程序被装入的起始线性地址为0x080480000。

  14. 调用do_mmap()函数创建一个新线性区来对可执行文件的数据段进行映射。这个线性区的起始线性地址也依赖于可执行文件的格式,因为可执行代码希望在特定的偏移量(即特定的线性地址)处找到它自己的变量。在ELF程序中,数据段正好被装在正文段之后。

  15. 为可执行文件的其他专用段分配另外的线性区,通常是无。

  16. 调用一个装入动态链接程序的函数。如果动态链接程序是ELF可执行的,这个函数就叫做load_elf_interp()。一般情况下,这个函数执行第12-14步的操作,不过要用动态链接程序代替被执行的文件。动态链接程序的正文段和数据段在线性区的起始线性地址是由动态链接程序本身指定的,但它们处于高地址区(通常高于0x40000000),这是为了避免与被执行文件的正文段和数据段所映射的线性区发生冲突。

  17. 把可执行格式的linux_binfmt对象的地址存放在进程描述符的binfmt字段中。

  18. 确定进程的新权能。

  19. 创建特定的动态链接程序表并把它们存放在用户态堆栈,这些表处于命令行参数和指向环境串的指针数组之间。

  20. 设置进程的内存描述符的start_code、end_code、start_data、end_data、start_brk、brk及start_stack字段

  21. 调用do_brk()函数创建一个新的匿名线性区来映射程序的bss段(当进程写入一个变量时,就触发请求调页,进而分配一个页框)。这个线性区的大小是在可执行程序被链接时就计算出来的。因为程序的可执行代码通常是不可重新定位的,因此,必须指定这个线性区的起始线性地址。在ELF程序中,bss段正好装在数据段之后。

  22. 调用start_thread()宏修改保存在内核态堆栈但属于用户态寄存器的eip和esp的值,以使它们分别指向动态链接程序的入口点和新的用户态堆栈的栈顶。

  23. 如果进程正被跟踪,就通知调试程序execve()系统调用已完成。

  24. 返回0(成功)。

当execve()系统调用终止且调用进程重新恢复它在用户态的执行时,执行上下文被大幅度改变,调用系统调用的代码不复存在。
从这个意义上看,我们可以说execve()从未成功返回。取而代之的是,要执行的新程序已被映射到进程的地址空间。
但是,新程序还不能执行,因为动态链接程序还必须考虑共享库的装载。

如果可执行文件是静态链接的,即如果不需要共享库,load_binary方法只需将程序的正文段、数据段、bss段和堆栈段映射到进程线性区,然后把用户态eip寄存器的内容设置为新程序的入口点即可。

动态链接程序如何运作

尽管动态链接程序运行在用户态,但我们还要在这里简要概述一下动态链接程序是如何运作的。

  • 它的第一个工作就是从内核保存在用户态堆栈的信息(处于环境串指针数组和arg start之间)开始,为自己建立一个基本的执行上下文。
  • 然后,动态链接程序必须检查被执行的程序,以识别哪个共享库必须装入及在每个共享库中哪个函数被有效地请求。
  • 接下来,解释器发出几个mmap()系统调用来创建线性区,以对将存放程序实际使用的库函数(正文和数据)的页进行映射。
  • 然后,解释器根据库的线性区的线性地址更新对共享库符号的所有引用。
  • 最后,动态链接程序通过跳转到被执行程序的主入口点而终止它的执行。
  • 从现在开始,进程将执行可执行文件的代码和共享库的代码。

注解

魔数:就是一个标识作用的信息。 linux中魔数的作用

参考资料

Linux程序的执行过程
深入理解Linux内核中文版(第三版)

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×