可执行程序的装载
注:作者:臧文君,原创作品转载请注明出处,《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
一、预处理、编译、链接和目标文件的格式
1、可执行程序是怎么来的?
例:C语言代码-->编译器预处理-->编译成汇编代码-->汇编器编译成目标代码-->链接成可执行文件,再由操作系统加载到内存中执行
(1)预处理.cpp:负责把include的文件包含进来及宏替换等工作。
gcc -E -o hello.cpp hello.c -m32
(2)编译成汇编代码.s:gcc -x cpp-output -S -o hello.s hello.cpp -m32
(3)编译成目标代码.o:gcc -x assembler -c hello.s -o hello.o -m32
hello.o中是二进制的文件,有机器指令。
(4)链接成可执行文件:gcc -o hello hello.o -m32
hello和hello.o都是ELF格式的文件,都是二进制。
这样编译出的hello可执行文件是使用共享库(libC库)。
若静态链接gcc -o hello.static hello.o -m32 -static,是完全把所有需要执行的依赖的东西放在程序内部,文件会更大。
2、可执行文件的内部是怎样的?---目标文件的格式ELF
(1)格式的发展:
PE在Windows系统,ELF在Linux系统。
(2)ELF:Executable And Linkable Format
目标文件也叫ABI---应用程序二进制接口,在目标文件中已经是二进制兼容的格式,目标文件已经是适应到某一种CPU体系结构上的二进制指令。
(3)ELF中的三种目标文件
Object文件参与程序的链接(创建一个程序)和程序的执行(运行一个程序):
入口地址Entry point address:程序的起点。
(4)当创建或增加一个进程映像的时候,系统在理论上将拷贝一个文件的段到一个虚拟的内存段。
3、静态链接的ELF可执行文件与进程的地址空间
(1)ELF可执行文件加载到内存时默认地址是0x8048000;其前面一般是ELF文件头部的信息,大小可能不同,入口点的位置0x8048x00,这里是程序的实际入口,是可执行文件加载到内存中开始执行的第一行代码。
(2)一般静态链接会将所有代码放在一个代码段。
动态链接的进程会有多个代码段。
二、可执行程序、共享库和动态链接
1、可执行程序之前的工作
通过Shell程序启动一个可执行程序
(1)Shell命令行、main函数的参数与execve的参数
(2)Shell先fork()一个子进程,在子进程中调用execlp来加载可执行程序。
(3)命令行参数和环境变量是如何进入新程序的堆栈的?
Shell程序-->execve-->sys_execve,然后在初始化新程序堆栈时拷贝进去。
先函数调用参数传递,再系统调用参数传递。
先将命令行参数和环境变量压栈,main函数的起点start_stack,在创建新的用户态堆栈时,将命令行参数和环境变量的内容通过指针传递到系统调用的内核处理函数,然后内核处理函数在创建可执行程序的新的用户态堆栈时,将参数和变量拷贝进去,来初始化新的可执行程序的上下文环境。
2、装载时动态链接和运行时动态链接应用举例
动态链接分为可执行程序装载时动态链接和运行时动态链接。
(1)准备.so文件
共享库:
shlibexample.h (1.3 KB) - Interface of Shared Lib Example
shlibexample.c (1.2 KB) - Implement of Shared Lib Example
(2)编译成libshlibexample.so共享库文件
命令:gcc -shared shlibexample.c -o libshlibexample.so -m32
dllibexample.h (1.3 KB) - Interface of Dynamical Loading Lib Example
dllibexample.c (1.3 KB) - Implement of Dynamical Loading Lib Example
编译成libdllibexample.so动态加载文件
命令:gcc -shared dllibexample.c -o libdllibexample.so -m32
(3)分别以共享库和动态加载共享库的方式使用libshlibexample.so文件和libdllibexample.so文件。
(4)编译main
注意这里只提供shlibexample的-L(库对应的接口头文件.h所在目录)和-l(库名,如libshlibexample.so去掉lib和.so的部分),并没有提供dllibexample的相关信息,只是指明了-ldl
1.$ gcc main.c -o main -L/path/to/your/dir -lshlibexample -ldl -m32
注:dl表示动态加载。
2.$ export LD_LIBRARY_PATH=$PWD #将当前目录加入默认路径(usrlib),否则main找不到依赖的库文件,当然也可以将库文件copy到默认路径下。
3.$ ./main
两种动态链接的方式:一种是在装载可执行程序时完成动态链接的过程,另一种是在程序执行过程中由程序自身装载共享库。
三、可执行程序的装载
1、可执行程序的装载相关关键问题分析
(1)可执行程序的装载也是系统调用execve,execve和fork都是特殊的系统调用。
fork进入内核态后两次返回:第一次返回到原来父进程的位置继续执行,第二次在子进程中从ret_from_fork开始执行然后返回用户态。
当前程序执行到execve系统调用时陷入内核态,在内核中用execve加载可执行文件,把当前进程的可执行文件覆盖掉,execve系统调用返回到新的可执行程序的起点。
(2)sys_execve内核处理过程
sys_execve内部会解析可执行文件格式:do_execve -> do_execve_common -> exec_binprm
根据文件头部信息寻找对应的文件格式处理模块
retval = fmt->load_binary(bprm);用来解析ELF文件格式
对于ELF格式的可执行文件fmt->load_binary(bprm);执行的应该是load_elf_binary其内部是和ELF文件格式解析的部分需要和ELF文件格式标准结合起来阅读。
(3)内核时如何支持多种不同可执行文件格式的?
观察者模式,多态,发布订阅架构。
elf_format 和 init_elf_binfmt,这里就是观察者模式中的观察者,list_for_each_entry是被观察者。
(4)execve系统调用返回到用户态从哪里开始执行?
load_elf_binary->start_thread
通过修改内核堆栈中EIP的值作为新程序的起点。
2、sys_execve的内部处理过程
sys_execve -> do_execve -> do_execve_common -> exec_binprm -> ret = search_binary_handler寻找可执行文件的处理函数 -> list_for_each_entry。
elf_format结构体变量,加载到链表中去,是发布订阅架构模式或观察者模式或多态机制。
elf_format结构体变量如何进入到内核中?
init_elf_binfmt:register_binfmt(&elf_format);注册结构体变量elf_format到一个链表中去
elf_format是专门处理ELF格式的模块。
ELF可执行文件会被默认映射到0x8048000这个地址。
需要动态链接的可执行文件先加载连接器ld。
在start_thread时有两种可能:静态链接时elf_entry就指向可执行文件中main函数对应的位置;动态链接时将CPU控制权交给ld来加载依赖库并完成动态链接。
start_thread把返回到用户态的位置从原来的int0x80的下一条指令变成了新加载的可执行文件的elf_entry的位置,对于静态链接的文件elf_entry是新程序执行的起点。
3、使用gdb跟踪sys_execve内核函数的处理过程
(1)重新克隆一个menu。
(2)编写一个test_exec.c,覆盖test.c:mv test_exec.c test.c。
(3)查看test.c:vi test.c,Shift+G直接到文件末尾。
(4)修改Makefile:vi Makefile,然后make rootfs。
(5)跟踪:
qemu -kernel ../linux-3.18.6/arch/x86/boot/bzImage -initrd ../rootfs.img -s -S
gdb
target remote:1234
(6)设置断点:b sys_execve(可以先停在sys_execve然后再设置其他断点),b load_elf_binary,b start_thread。
(7)输入c继续执行,输入指令exec,list查看,按s可以跟踪进行到do_execve的内部。
po new_ip(po=printobject),new_ip是返回到用户态的第一条指令的地址。
查看hello可执行程序的入口点地址:readelf -h hello,与new_ip的值相同。
4、可执行程序的装载和庄生梦蝶的故事
庄周(调用execve的可执行程序)入睡(调用execve陷入内核),醒来(系统调用execve返回用户态)发现自己是蝴蝶(被execve加载的可执行程序)。
5、浅析动态链接的可执行程序的装载
动态链接库的依赖关系会形成一个树,动态链接库的装载过程是一个图的遍历。
ELF格式中的.interp和.dynamic需要依赖动态链接器来解析,entry返回到用户态时不是返回到可执行程序规定的起点,返回到动态链接器的入口。
装载和链接之后ld将CPU的控制权交给可执行程序。
动态链接的过程不是由内核完成的,是由动态链接器完成的,是libc的一部分。
总结:
1、可执行程序的产生:
C语言代码-->编译器预处理-->编译成汇编代码-->汇编器编译成目标代码-->链接成可执行文件,再由操作系统加载到内存中执行
2、ELF中的三种目标文件:
可重定位文件,可执行文件,可共享文件。
3、静态链接时,ELF可执行文件加载到内存时默认地址是0x8048000。
4、命令行参数和环境变量是如何进入新程序的堆栈的?
Shell程序-->execve-->sys_execve,然后在初始化新程序堆栈时拷贝进去。
先函数调用参数传递,再系统调用参数传递。
5、动态链接分为可执行程序装载时动态链接和运行时动态链接。
6、当前程序执行到execve系统调用时陷入内核态,在内核中用execve加载可执行文件,把当前进程的可执行文件覆盖掉,execve系统调用返回到新的可执行程序的起点。
7、动态链接库的装载过程是一个图的遍历。
ELF格式中的.interp和.dynamic需要依赖动态链接器来解析,entry返回到用户态时不是返回到可执行程序规定的起点,返回到动态链接器的入口。