首页 > 技术文章 > 实验七:Linux内核如何装载和启动一个可执行程序

Justtry 2016-04-09 16:01 原文

实验七:Linux内核如何装载和启动一个可执行程序

姓名:王朝宪

学号:20135114

注: 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

可执行文件的创建

C代码(.c) - 经过编译器预处理,编译成汇编代码(.asm) - 汇编器,生成目标代码(.o) - 链接器,链接成可执行文件(.out) - OS将可执行文件加载到内存里执行。

1. 预处理

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

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

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

4. 链接
gcc -o hello hello.o -m32    链接成可执行文件(ELF格式,二进制文件)
在hello可执行文件里面使用了共享库,会调用printf,libc库里的函数
gcc -o hello.static hello.o -m32 -static    静态链接
把执行所需要依赖的东西都放在程序内部

 

(2)ELF三种主要的目标文件:

1.可重定位:保存代码和适当数据,用来和其他的object文件一起创建可执行/共享文件,主要是.o文件

2.可执行文件:指出了exec如何创建程序进程映像,怎么加载,从哪里开始执行

3.共享object文件:保存代码和适当数据,用来被下面的两个连接器链接。

(1)连接editor,连接可重定位、共享object文件。即装载时链接。

(2)动态链接器,联合可执行、其他共享object文件创建进程映像。即运行时链接。


(3)可执行程序的执行环境

  • 命令行参数和shell环境,一般我们执行一个程序的Shell环境,我们的实验直接使用execve系统调用。

    • $ ls -l /usr/bin 列出/usr/bin下的目录信息

    • Shell本身不限制命令行参数的个数, 命令行参数的个数受限于命令自身

      • 例如,int main(int argc, char *argv[])

      • 又如, int main(int argc, char *argv[], char *envp[])

    • Shell会调用execve将命令行参数和环境参数传递给可执行程序的main函数

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

      • 库函数exec*都是execve的封装例程

 

(4)可执行程序的装载

  • 命令行参数和shell环境,一般我们执行一个程序的Shell环境,我们的实验直接使用execve系统调用。

    • Shell本身不限制命令行参数的个数, 命令行参数的个数受限于命令自身

      • 例如,int main(int argc, char *argv[])

      • 又如, int main(int argc, char *argv[], char *envp[])

    • Shell会调用execve将命令行参数和环境参数传递给可执行程序的main函数

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

      • 库函数exec*都是execve的封装例程

  • sys_execve内部会解析可执行文件格式

 

实验步骤:

 

1、先把menu删掉,在克隆一个,用test_exec.c覆盖掉test.c。

 

2、打开test.c。发现增加了一句MenuConfig。

 

3、打开Makefile,首先静态编译了hello.c,生成根文件系统时把init和hello都放入rootfs image里面,这样执行exec的时候就自动的帮我们加载hello这个文件。

 

4、执行结果hello world! 是新加载的一个可执行程序输出的。

 

5、-S -s单步调试,窗口被冻结。

 

6、设置三个断点:sys_execve,load_elf_binary,start_thread。

 

7、list列出来跟踪, 输入s可以进入do_execve的内部。按c继续执行,跑到load_elf_binary。list查看代码,输入n一句一句跟踪,nnnc,追踪到start_thread。

 

8、观察hello这个可执行程序的入口,发现也是0x8048d0a,和new_ip的位置一样。new_ip是返回到用户态第一条指令的地址。

 

9、将new_ip和new_sp赋值,并设了一个新堆栈。

 

实验截图:

技术分享

 

 

技术分享

 

技术分享

 

技术分享

 

技术分享

 

 

Linux系统加载可执行程序过程理解

当execve()系统调用终止且进程重新恢复它在用户态执行时,执行上下文被大幅度改变,要执行的新程序已被映射到进程空间,从elf头中的程序入口点开始执行新程序。

如果这个新程序是静态链接的,那么这个程序就可以独立运行,elf头中的这个入口地址就是本程序的入口地址。

如果这个新程序是动态链接的,那么此时还需要装载共享库,elf头中的这个入口地址是动态链接器ld的入口地址。

新的可执行程序执行,需要以下: 
1. 它所需要的库函数。 
2. 属于它的进程空间:代码段,数据段,内核栈,用户栈等。 
3. 它所需要的运行参数。 
4. 它所需要的系统资源。 

execve系统调用会调用sys_execve,然后sys_execve调用do_execve,然后do_execve调用do_execve_common,然后do_execve_common调用exec_binprm,在exec_binprm中:

对于ELF文件格式,fmt函数指针实际会执行load_elf_binary,load_elf_binary会调用start_thread,在start_thread中通过修改内核堆栈中EIP的值,使其指向elf_entry,跳转到elf_entry执行。 
对于静态链接的可执行程序,elf_entry是新程序的执行起点。对于动态链接的可执行程序,需要先加载链接器ld, 
elf_entry = load_elf_interp(…) 
将CPU控制权交给ld来加载依赖库,再由ld在完成加载工作后将CPU控制权还给新进程。

 

总结:

创建一个新进程

新进程调用execve()系统调用执行指定的ELF文件

调用内核的入口函数sys_execve(),sys_execve()服务例程修改当前进程的执行上下文;

推荐阅读