类Unix系统提供了一系列函数,这些函数能用可执行文件所描述的新上下文代替进程的上下文
。这样的函数名以前缀exec开始,后跟一个或两个字母。
下表列出了exec族函数
,它们之间的差别在于如何解释参数。
函数名 | 路径搜索 | 命令行参数 | 环境数组 |
---|---|---|---|
execl() | 否 | 列表 | 否 |
execlp() | 是 | 列表 | 否 |
execle() | 否 | 列表 | 是 |
execv() | 否 | 数组 | 否 |
execvp() | 是 | 数组 | 否 |
execve() | 否 | 数组 | 是 |
每个函数的第一个参数表示被执行文件的路径名。
除了第一个参数,execl(), execlp()和exec1e()函数包含的其他参数个数都是可变的。
相反,execv(), execvp()和execve()函数指定单个参数的命令行参数,正如函数名中的“v”字符所隐含的一样,这单个参数是指向命令行参数串的指针向量地址
。
execle()和execve()函数的最后一个参数是指向环境串
的指针数组的地址:数组的最后一个元素照样必须为NULL。
其他函数对新程序环境参数的访问是通过C库定义的外部全局变量environ进行的。
所有的
exec函数
(除execve()外)都是C库定义的封装例程
,并利用了execve()系统调用
,这是Linux所提供的处理程序执行的唯一系统调用
。
sys_execve()服务例程接收下列参数:
文件路径名的地址
(在用户态地址空间)。命令行参数
。环境变量
。sys_execve()把可执行文件路径名
拷贝到一个新分配的页框。
然后调用do_execve()函数
,传递给它的参数为指向这个页框的指针、指针数组的指针及把用户态寄存器内容保存到内核态堆栈的位置。
do_execve()依次执行下列操作:
动态地分配一个linux_binprn数据结构
,并用新的可执行文件的数据填充linux_binprn数据结构。
调用path_lookup()
, dentry_open()
和path_release()
,以获得与可执行文件相关的目录项对象、文件对象和索引节点对象。如果失败,则返回相应的错误码。
检查是否可以由当前进程执行该文件,再检查索引节点的i_writecount字段,以确定可执行文件没被写入;把-1存放在这个字段以禁止进一步的写访问。
在多处理器系统中,调用sched_exec()函数
来确定最小负载CPU以执行新程序,并把当前进程转移过去。
调用ini_new_context()
检查当前进程是否使用自定义局部描述符表。如果是,函数为新程序分配和淮备一个新的LDT。
调用prepare_binprm()
函数填充linux_binprm数据结构,这个函数又依次执行下列操作:
e_uid
和e_gid字段
,考虑可执行文件的setuid和setgid标志的值。这些字段分别表示有效的用户ID和组ID。也要检查进程的权能。buf字段
。这些字节包含的是适合于识别可执行文件格式的一个魔数和其他信息。把文件路径名、命令行参数及环境串拷贝到一个或多个新分配的页框中,最终它们会被分配给用户态地址空间。
调用search_binary_handler()函数
对formats链表
进行扫描,并尽力应用每个元素的load_binary方法,把linux_binprm数据结构传递给这个函数。只要load_binary方法成功应答了文件的可执行格式,对formats的扫描就终止。
如果可执行文件格式不在formats链表中,就释放所分配的所有页框并返回错误码 -ENOEXEC,表示Linux不认识这个可执行文件格式。
否则,函数释放linux_binprm数据结构,返回从这个文件可执行格式的load_binary方法中所获得的代码。
可执行文件格式对应的load_binary方法执行下列操作(假定这个可执行文件所在的文件系统允许文件进行内存映射并需要一个或多个共享库):
检查存放在文件前128字节中的一些魔数以确认可执行格式。如果魔数不匹配,则返回错误码 -ENOEXEC。
读可执行文件的首部。这个首部描述程序的段和所需的共享库
。
从可执行文件获得动态链接程序的路径名,并用它来确定共享库的位置并把它们映射到内存。
获得动态链接程序的目录项对象,也就获得了索引节点对象和文件对象。
检查动态链接程序的执行许可权。
把动态链接程序的前128字节拷贝到缓冲区。
对动态链接程序类型执行一些一致性检查。
调用flush_old_exec()
函数释放前一个计算所占用的几乎所有资源。这个函数又依次执行下列操作:
清除进程描述符的PF_FORKNOEXEC标志
。这个标志用于在进程创建时设置进程记账,在执行一个新程序时清除进程记账。
设立进程新的个性,即设置进程描述符的personality字段。
调用arch_pick_mmap_layout(),以选择进程线性区的布局。
调用setup_arg_pages()函数为进程的用户态堆栈分配一个新的线性区描述符,并把那个线性区插入到进程的地址空间。setup_arg_pages()还把命令行参数和环境变量串所在的页框分配给新的线性区。
调用do_map()函数创建一个新线性区来对可执行文件正文段(即代码)进行映射。这个线性区的起始线性地址依赖于可执行文件的格式,因为程序的可执行代码通常是不可重定位的。因此,这个函数假定从某一特定逻辑地址的偏移量开始装入正文段。ELF程序被装入的起始线性地址为0x080480000。
调用do_mmap()函数创建一个新线性区来对可执行文件的数据段进行映射。这个线性区的起始线性地址也依赖于可执行文件的格式,因为可执行代码希望在特定的偏移量(即特定的线性地址)处找到它自己的变量。在ELF程序中,数据段正好被装在正文段之后。
为可执行文件的其他专用段分配另外的线性区,通常是无。
调用一个装入动态链接程序的函数。如果动态链接程序是ELF可执行的,这个函数就叫做load_elf_interp()
。一般情况下,这个函数执行第12-14步的操作,不过要用动态链接程序代替被执行的文件。动态链接程序的正文段和数据段在线性区的起始线性地址是由动态链接程序本身指定的,但它们处于高地址区(通常高于0x40000000),这是为了避免与被执行文件的正文段和数据段所映射的线性区发生冲突。
把可执行格式的linux_binfmt对象的地址存放在进程描述符的binfmt字段中。
确定进程的新权能。
创建特定的动态链接程序表并把它们存放在用户态堆栈,这些表处于命令行参数和指向环境串的指针数组之间。
设置进程的内存描述符的start_code、end_code、start_data、end_data、start_brk、brk及start_stack字段。
调用do_brk()函数创建一个新的匿名线性区来映射程序的bss段(当进程写入一个变量时,就触发请求调页,进而分配一个页框)。这个线性区的大小是在可执行程序被链接时就计算出来的。因为程序的可执行代码通常是不可重新定位的,因此,必须指定这个线性区的起始线性地址。在ELF程序中,bss段正好装在数据段之后。
调用start_thread()宏修改保存在内核态堆栈但属于用户态寄存器的eip和esp的值,以使它们分别指向动态链接程序的入口点和新的用户态堆栈的栈顶。
如果进程正被跟踪,就通知调试程序execve()系统调用已完成。
返回0(成功)。
当execve()系统调用终止且调用进程重新恢复它在用户态的执行时,执行上下文被大幅度改变,调用系统调用的代码不复存在。
从这个意义上看,我们可以说execve()从未成功返回。取而代之的是,要执行的新程序已被映射到进程的地址空间。
但是,新程序还不能执行,因为动态链接程序还必须考虑共享库的装载。
如果可执行文件是静态链接的,即如果不需要共享库,load_binary方法只需将程序的正文段、数据段、bss段和堆栈段映射到进程线性区,然后把用户态eip寄存器的内容设置为新程序的入口点即可。
尽管动态链接程序运行在用户态,但我们还要在这里简要概述一下动态链接程序是如何运作的。
魔数:就是一个标识作用的信息。 linux中魔数的作用
Linux程序的执行过程
深入理解Linux内核中文版(第三版)
Update your browser to view this website correctly. Update my browser now