Linux内核如何装载和启动一个可执行程序
有了上次的教训,这次直接用vmware完成 (~ ̄3 ̄)~
先观察MenuOS新增的函数
1 int Exec(int argc, char *argv[]) 2 { 3 int pid; 4 /* fork another process */ 5 pid = fork(); 6 if (pid < 0) 7 { 8 /* error occurred */ 9 fprintf(stderr,"Fork Failed!"); 10 exit(-1); 11 } 12 else if (pid == 0) 13 { 14 /* child process */ 15 printf("This is Child Process! "); 16 execlp("/hello","hello",NULL); 17 } 18 else 19 { 20 /* parent process */ 21 printf("This is Parent Process! "); 22 /* parent will wait for the child to complete*/ 23 wait(NULL); 24 printf("Child Complete! "); 25 } 26 }
和上次的Fork差不多,只不过在子进程的分支中调用了execlp。
这里要提一下exec大家族,一共有6个函数
(1)int execl(const char *path, const char *arg, ......);
(2)int execle(const char *path, const char *arg, ...... , char * const envp[]);
(3)int execv(const char *path, char *const argv[]);
(4)int execve(const char *filename, char *const argv[], char *const envp[]);
(5)int execvp(const char *file, char * const argv[]);
(6)int execlp(const char *file, const char *arg, ......);
只有4-execve是本体,其他都是execve的封装。函数的区别从函数名就能看出来:
带p的5和6的第一个参数是文件名,而不带p的1-4是路径名。5和6会在PATH系统变量中搜索输入的文件名
带e的2和4的最后一个参数是环境变量字符串的数组。环境变量字符串的形式是“MYENV=123”的键值对。而不带e的函数将把当前进程的环境变量保留给新程序。
最后是带v的345和带l的126的区别。 v就是vector,即参数是通过字符串数组传递。l指的是list,参数直接通过不定长的参数列表传递,最后要加一个NULL表示结束。
这次使用的execlp就是仅指定了文件名,参数直接通过函数参数列表传递,并且最后用NULL表示结束。
exec家族都是调用execve系统调用,具体是通过do_execve函数实现的。而do_execve也只是简单的封装一下输入的参数与环境变量,调用了do_execve_common函数。
do_execve_common函数也不长,除去一些基本的检查、后处理代码,核心是创建了一个linux_binprm结构体
简单填充了一些如文件名、文件句柄、参数、环境变量以及它们的个数等等信息,
接下来调用exec_binprm,使用前面准备的bprm来启动程序。
因为linux支持多种可执行文件格式(elf/a.out/coff等), 所以内核首先要通过search_binary_handler找到用于加载bprm中指定的程序的加载器
所有的加载器都串联在formats链表上,因此可以用list_for_each_entry遍历这个链表,逐个尝试解析可执行程序。
如果某个解析器无法解析这个程序,就返回ENOEXEC。
通过 objdump -f hello 可以知道hello是一个elf格式的可执行程序
所以是由load_elf_binary函数装载这个程序。在该函数设置断点,断点确实触发。
load_elf_binary比较长。前面基本上是各种检查,然后解析elf文件头
调用参数以及系统变量也是在create_elf_tables函数中设置到栈上
再处理动态链接的段
接下来把可执行程序的各个段映射到内存中
最后设置了执行的起点。如果需要动态链接,则起点交给动态库的加载器。否则直接由elf文件的执行入口位置开始。
这个起始地址最后通过start_thread函数保存到用户态的EIP寄存器中,一起保存的还有用户堆栈
当进程从execve系统调用返回的时候,会把用户态的寄存器恢复,就自动从指定的地址开始运行。并且能从堆栈中取出参数与系统变量。
总结
Linux装载可执行程序的过程基本就是按照特定的格式,将程序放入虚拟地址空间。最后利用系统调用的机制,巧妙地在内核态设置了返回用户态的入口,使得新加载的程序开始运行。