Linux内核分析之简析加载和启动一个可执行程序

来源:互联网 发布:php获取url参数 编辑:程序博客网 时间:2024/05/17 04:35

SA16225055冯金明    原创作品转载请注明出处 

Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

实验内容

实验要求:

  • 理解编译链接的过程和ELF可执行文件的格式
  • 编程使用exec* 库函数加载一个可执行文件,动态链接分为可执行程序装载时动态链接和运行时动态链接,编程练习动态链接库的这两种使用方式
  • 使用gdb跟踪分析一个execve系统调用内核处理函数sys_execve,验证对linux系统可执行程序所需处理过程的分析的理解
  • 特别关注新的可执行程序是从哪里开始执行的?为什么execve系统调用返回后新的可执行程序能顺利执行?对于静态链接的可执行程序和动态链接的可执行程序execve系统调用返回时会有什么不同
关键实验截图:
  • 在test.c中添加Exec()函数,并在menu命令中添加exec命令,修改Makefile,如图一所示
  • 使用gdb调试,添加相应的断点,如图二所示
  • 通过调试,查看加载和启动一个可执行程序的具体过程,如图三所示

图一 Exec( )

图二 设置断点

图三 查看具体执行流程
加载和启动一个可执行程序
可执行文件的创建:
.c文件经过预处理,生成.cpp预处理的中间文件;经过编译生成汇编代码(.asm文件);再通过汇编器生成目标代码(.o文件);再通过连接器,链接成可执行文件(.out文件)。操作系统将可执行文件加载到内存中执行,示意图如下所示:

预处理:
gcc -E -o hello.cpp hello.c -m32    预处理(文本文件) 预处理负责把include的文件包含进来及宏替换等工作

编译:

gcc -x cpp-output -S -o hello.s hello.cpp -m32    编译成汇编代码(文本文件)

汇编:

gcc -x assembler -c hello.s -o hello.o -m32    汇编成目标代码(ELF格式,二进制文件,有一些机器指令,只是还不能运行)

链接:

gcc -o hello hello.o -m32    链接成可执行文件(ELF格式,二进制文件) 在hello可执行文件里面使用了共享库,会调用printf,libc库里的函数

gcc -o hello.static hello.o -m32 -static    静态链接把执行所需要依赖的东西都放在程序内部

ELF三种主要的目标文件:

  • 可重定位:保存代码和适当数据,用来和其他的object文件一起创建可执行/共享文件,主要是.o文件
  • 可执行文件:指出了exec如何创建程序进程映像,怎么加载,从哪里开始执行
  • 共享object文件:保存代码和适当数据,用来被下面的两个连接器链接
  1. 连接editor,连接可重定位、共享object文件。即装载时链接。
  2. 动态链接器,联合可执行、其他共享object文件创建进程映像。即运行时链接。
可执行程序的执行环境:
  • 在启动一个可执行程序时,会将输入的命令行参数和环境串都放在用户态堆栈中。
  • 动态链接分为可执行程序装载时动态链接和运行时动态链接。
  • 可执行程序装载时的动态链接过程如下。
  1. 输入命令gcc -shared dllibexample.c -o libdlibexample.so -m32生成链接库。
  2. 然后在程序中使用#include导入该头文件即可使用该链接库中的函数。
  3. 使用命令gcc main.c -o main -L /path/to/your/dir -l shlibexample -ldl -m32执行。
  • 运行时动态链接过程如下
  1. 同样使用使用上述命令生成动态链接库。
  2. 在程序中动态调用
  3. 首先,我们使用函数dlopen打开动态链接库,然后定义一个函数指针,然后通过函数dlsym来找到我们想要的函数并附值给上面定义的函数指针,最后利用函数指针调用该函数。
  • 调用exec系统调用装载一个可执行程序的过程。
  1. 首先在用户态调用exec*函数,对应的内核入口为sys_execve
  2. 在sys_execve中调用do_execve函数
  3. 在do_execve函数中调用open_execve打开可执行文件
  4. 调用copy_strings_kernel从系统空间拷贝
  5. 调用copy_strings从用户空间拷贝
  6. 调用exec_binprm函数,然后调用search_binary_handler函数
  7. 调用list_for_each_entry来搜寻formats队列中的成员来执行
  8. 调用copy_thread来附值进程控制快的相关信息
  9. 最后调用start_thread函数来设置新的程序执行的开始地址

执行过程分析:


execve系统调用和fork系统调用一样,都是特殊的系统调用。fork系统调用返回两次,一次返回父进程执行,一回返回特定的点ret_from_fork执行,然后返回到用户态。execve系统调用在内核中将当前的可执行程序覆盖掉了,当返回时不再是原来的可执行程序了。Shell会调用execve将命令行参数和环境参数传递给可执行程序的main函数:

int execve(const char * filename,char * const argv[ ],char * const envp[ ]);

而所有的库函数exec*都是execve的封装例程。

系统调用sys_execve会解析可执行文件格式do_execve->do_execve_common->exec_binprm然后执行search_binary_handler找到符合文件头部标明的文件格式的解析模块。对于linux下的ELF文件,fmt->load_binary(bprm)实际执行的就是static int load_elf_binary(struct linux_binprm *bprm)。

执行start_thread(regs, elf_entry, bprm->p)时,如果是静态链接的,elf_entry就是文件头部标明的入口。进入内核后

start_thread(struct pt_regs *regs, unsigned long new_ip, unsigned long new_sp){    set_user_gs(regs, 0);    regs->fs        = 0;    regs->ds        = __USER_DS;    regs->es        = __USER_DS;    regs->ss        = __USER_DS;    regs->cs        = __USER_CS;    regs->ip        = new_ip;    regs->sp        = new_sp;    regs->flags        = X86_EFLAGS_IF;    /*     * force it to the iret return path by making it look as if there was     * some work pending.     */    set_thread_flag(TIF_NOTIFY_RESUME);}

execve在返回前用新的ip和sp更新了进程的ip和sp。对于需要动态链接的程序,elf_entry就会加载动态链接器ld的入口地址。

总结

在linux环境下,可执行文件的格式为ELF,文件头部信息会标明文件在加载到内存中的相关信息,随后的是以段形式存在的代码和数据。段的划分主要依据是加载到内存中的读写属性。系统调用execve则是负责可执行文件的调度和执行,先进行相关参数的传递和调用前环境的设置,然后加载可执行文件的信息,查找相关执行文件解析模块,对ELF格式的可执行文件,按照格式要求加载到内存中相应的地址空间当中。如果是静态链接,则将文件头部标明的入口地址作为开始执行的地址,如果是依赖动态链接库的 可执行文件则需要将动态链接器ld的入口地址作为开始的地址。

参考资料

  • http://swordautumn.blog.51cto.com/1485402/1633663
  • http://blog.csdn.net/u010521171/article/details/51106773
  • http://www.cnblogs.com/petede/p/5351696.html
内核代码,纷繁复杂,在前人总结的基础上,勉强可以看懂三分,肯定是会存在理解上的诸多文件,还请各位大佬多多指教,如有什么不足,还请不吝赐教!!!

0 0