实验:ELF文件格式与程序的编译链接
一、可执行文件的创建
?从源代码到可执行程序所要经历的过程概述:
?源代码(.c .cpp .h)经过c预处理器(cpp)后生成.i文件,编译器(cc1、cc1plus)编译.i文件后生成.s文件,汇编器(as)汇编.s文件后生成.o文件,链接器(ld)链接.o文件生成可执行文件。gcc是对cpp、cc1(cc1plus)、as、ld这些后台程序的包装,它会根据不同的参数要求去调用后台程序。
?以helloworld程序为例:
gcc -E -o hello.cpp hello.c -m32 //生成预处理文件
hello.cpp //预处理负责把include的文件包含进来及宏替换等工作
gcc -x cpp-output -S -o hello.s hello.cpp -m32 编译成汇编代码hello.s
gcc -x assembler -c hello.s -o hello.o -m32 编译成目标代码,得到二进制文件hello.o
gcc -o hello hello.o -m32 链接成可执行文件hello
./hello 运行hello文件
二、可执行文件的组成
?(1)以ELF为格式的主要有三种文件:
?①可重定位文件:保持着代码和适当的数据,用来和其他的object文件一起来创建一个可执行文件或者一个共享文件。例如.o文件。
?②可执行文件:可以运行的文件。该文件指出了exec(BA_OS)如何来创建进程映象。再来联想下程序和进程的区别。到底这种可执行文件是进程还是程序?我们发现它的段中只含.text和.data一类的段,而不含有堆栈段。所以可以确定它只是程序。当它被操作系统调入内存开始执行时才会真正的成为进程。例如.out文件。
?③共享object文件:保存着代码和数据,被两个链接器链接。一个是连接编辑器,可以和其他可重定位和共享object文件来创建其他的object。第二个是动态链接器,联合一个可执行文件和其他共享object文件来创建一个进程映像。
?(2)ELF文件的头部:使用命令 readelf -h hello 查看hello文件的头:
三、可执行程序的加载
(1)装载:可执行程序的执行环境shell。shell就是用户键入命令,加载并执行可执行程序的控制台.shell的本质就是提供图形化的界面,将用户写入的字符串解析成真正执行的命令或者说可执行程序。
?有两个问题:
?①真正执行程序的是什么程序或指令?答案是execve系统调用(库函数exec*都是execve的封装例程)。
?②如何给该系统调用传参?也就是传参的默认格式是什么?答:shell会传入execve的参数有两种,一种是程序本身参数,也就是main的参数argc,argv;第二种是shell环境变量的参数,envp字符串数组中。
?来看下execve的参数类型:
int execve(const char * filename,char * const argv[ ],char * const envp[ ]);
?当然,有些程序的main函数是不处理环境变量参数的,例如常见的:
int main(int argc, char *argv[])。
?但是有时候也支持,例如,所以这个时候传入的环境变量参数才会被解析使用。
int main(int argc, char *argv[], char *envp[])
?(2)execve是如何从内核态将参数传给进程(假设该进程是用户态)的用户态堆栈的?
?发现,仍是从用户态的数据段中复制到用户态堆栈中的。那么跟内核什么关系呢?执行execve的进程就是当前的shell,所以参数首先会被压在当前shell进程的内核堆栈中。关键在传入内核的参数是指针,所以内核(sys_ execve)要做的就是把指针的值复制回新进程的代码段,再复制到进程的用户堆栈段。初始化后的进程内存地址空间就是第二张图那样。也就解释了sys_ execve加载进程并初始化的作用、结果。shell每次都fork一个shell去执行命令,所以,当新进程起来后,启动它的shell结束,曾经保存的参数就不要了。
?(3)两种动态链接
?①装载时动态链接(Load-time Dynamic Linking):这种方法的前提是在编译之前已经明确知道要调用的动态库的哪些函数,编译时在目标文件中只保留必要的链接信息,而不含动态库函数代码;当程序执行时,调用函数的时候利用链接信息加载动态库函数代码并在内存中将其链接入调用程序的执行空间中(全部函数加载进内存),其主要目的是便于代码共享。(动态加载程序,处在加载阶段,主要为了共享代码,共享代码内存)
?②运行时动态链接(Run-time Dynamic Linking):这种方式是指在编译之前并不知道将会调用哪些动态库函数,完全是在运行过程中根据需要决定应调用哪个函数,将其加载到内存中(只加载调用的函数进内存);并标识内存地址,其他程序也可以使用该程序,并获得动态库函数的入口地址。(动态库在内存中只存在一份,处在运行阶段)
四、使用gdb跟踪分析一个execve系统调用内核处理函数sys_execve
?(1)增加execve系统调用指令:
?(2)在Makefile找到启动内核命令:
?(3)启动内核后,找到增加的exec命令,执行exec——新加载的执行程序来输出的“hello world”:
?(4)冻结后gdb跟踪,设置断点:
?(5)进入do_execve内部
?(6)继续执行,到了load_elf_binary
?(7)list后,可以看出静态链接时elf_interp为空
?(8)再执行,跟踪到start_thread
五、总结:
?新的可执行程序是从new_ ip开始执行,start_ thread实际上是把返回到用户态的位置从Int 0x80的下一条指令,变成了规定的新加载的可执行文件的入口位置,即修改内核堆栈的EIP的值作为新程序的起点。当执行到execve系统调用时,陷入内核态,用execve加载的可执行文件覆盖当前进程的可执行程序,当execve系统调用返回时,返回新的可执行程序的执行起点(main函数位置),所以execve系统调用返回后新的可执行程序能顺利执行。对于静态链接的可执行程序和动态链接的可执行程序execve系统调用返回时,如果是静态链接,elf_ entry指向可执行文件规定的头部(main函数对应的位置0x8048***);如果需要依赖动态链接库,elf_entry指向动态链接器的起点。