实验: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文件的头:
ELF的头保存的是元数据,也就是路线图,描述了文件的组织情况。比如程序头表(program header table)告诉系统如何来创建一个进程的内存映像。section头表(section header table)包含描述文件sections的信息。每个section在这个表中有一个入口;每个入口给出了该section的名字,大小等等信息。ELF的剩余部分是sections,包括代码段,数据段。这些在程序变成进程映像时加载到内存的虚拟地址空间中,从ELF头开始加载,所以从图中可以看出真正的代码从0x400430开始,这才是真正的程序入口。静态链接时程序所需要的代码全部在代码段中,而动态链接就不一样了,它会在运行时候去找内存中间部分加载的库函数。
三、可执行程序的加载
(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[])