2018-2019-120189224 《庖丁解牛Iinux内核分析》第八周学习总结
ELF
程序编译
链接与库
程序装载实验
实验要求:
理解编译链接的过程和ELF可执行文件格式,详细内容参考本周第一节;
编程使用exec*库函数加载一个可执行文件,动态链接分为可执行程序装载时动态链接和运行时动态链接,编程练习动态链接库的这两种使用方式,详细内容参考本周第二节;
使用gdb跟踪分析一个execve系统调用内核处理函数sys_execve ,验证您对Linux系统加载可执行程序所需处理过程的理解,详细内容参考本周第三节;推荐在实验楼Linux虚拟机环境下完成实验。
实验过程如下:
1.更新menu后用test.c覆盖test_exec.c
int Exec(int argc, char argv[])
{
int pid;
/ fork another process /
pid = fork();
if (pid < 0)
{
/ error occurred /
fprintf(stderr,"Fork Failed!");
exit(-1);
}
else if (pid == 0)
{
/ child process /
printf("This is Child Process!
");
execlp("/hello","hello",NULL);
}
else
{
/ parent process /
printf("This is Parent Process!
");
/ parent will wait for the child to complete*/
wait(NULL);
printf("Child Complete!
");
}
}
2.编译的时候执行了hello.c,并把init 和 hello 放到了rootfs.img目录下,所以在执行exec命令的时候就相当于自动了加载了hello这个程序。如下所示:
3.gdb跟踪分析
execve 是比较特殊的系统调用,exec_binprm 在保存了 bprm 后调用该函数来进一步操作,execve 加载的可执行文件会把当前的进程覆盖掉,返回之后就不是原来的程序而是新的可执行程序起点。这个函数除了保存 pid 以外,还执行了 search_binary_handler 来查询能够处理相应可执行文件格式的处理器,并调用相应的load_binary 方法以启动新进程。
load_elf_binary 调用 start_thread 函数。修改 int 0x80 压入内核堆栈的 EIP,当 load_elf_binary 执行完毕,返回至 do_execve 再返回至 sys_execve 时,系统调用的返回地址,即 EIP 寄存器,已经被改写成了被装载的 ELF 程序的入口地址。
hello的入口地址和new_ip的值都是0x8048d0a,对hello程序链接到了执行程序中
Linux 系统通过用户态 execve 函数调用内核态 sys_execve 系统调用,负责将新的程序代码和数据替换到新的进程中,打开可执行文件,载入依赖的库文件,申请新的内存空间,最后执行 start_thread 函数设置 new_ip 和 new_sp,完成新进程的代码和数据替换,然后返回并执行新的进程代码。
总结
可执行文件描述了如何初始化一个新的执行上下文。在init的时候会将支持的可执行程序解析程序注册添加到内核的链表中,在对可执行文件进行解析时,从链表头开始找,找到匹配的处理函数就对其进行解析。在shell中启动一个可执行程序时,会创建一个新进程,通过覆盖父进程的进程环境,将用户态堆栈清空,获得需要的执行上下文环境。命令行参数和环境变量会通过shell传递给execve,excve通过系统调用参数传递,传递给sys_execve,最后sys_execve在初始化新进程堆栈的时候拷贝进去。当execve()系统调用终止且进程重新恢复它在用户态执行时,执行上下文被大幅度改变,要执行的新程序已被映射到进程空间,从elf头中的程序入口点开始执行新程序。如果这个新程序是静态链接的,那么这个程序就可以独立运行,elf头中的这个入口地址就是本程序的入口地址;如果这个新程序是动态链接的,那么还需要装载共享库,elf头中的这个入口地址是动态链接器ld的入口地址。load_elf_binary->start_thread(…)通过修改内核堆栈中EIP的值作为新程序的起点。