在讲解elf文件格式之前,我们来回顾一下,一个用C语言编写的高级语言程序是从编写到打包、再到编译执行的基本过程,我们知道在CPU上执行的是低级别的机器语言,从高级语言到低级别的机器语言肯定是要经过翻译过程,这个过程大体的过程如下图所示:
在Unix系统中,从源文件到可执行目标文件是由编译驱动程序完成的,如大名鼎鼎的gcc,翻译过程包括图中的四个阶段;
Ø 预处理阶段
预处理器(cpp)根据以字符#开头的命令修给原始的C程序,结果得到另一个C程序,通常以.i作为文件扩展名。主要是进行文本替换、宏展开、删除注释这类简单工作。
对应的命令:linux> gcc -E hello.c hello.i
Ø 编译阶段
编译器将文本文件hello.i翻译成hello.s,包含相应的汇编语言程序
对应的命令:linux> gcc -S hello.c hello.s
Ø 汇编阶段
将.s文件翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件.o中(把汇编语言翻译成机器语言的过程)。
把一个源程序翻译成目标程序的工作过程分为五个阶段:词法分析;语法分析;语义检查和中间代码生成;代码优化;目标代码生成。主要是进行词法分析和语法分析,又称为源程序分析,分析过程中发现有语法错误,给出提示信息。
对应的命令:linux> gcc -c hello.c hello.o
Ø 链接阶段
此时hello程序调用了printf函数。 printf函数存在于一个名为printf.o的单独的预编译目标文件中。 链接器(ld)就负责处理把这个文件并入到hello.o程序中,结果得到hello文件,一个可执行文件。最后可执行文件加载到储存器后由系统负责执行, 函数库一般分为静态库和动态库两种。静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了。其后缀名一般为.a。动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库,这样可以节省系统的开销。动态库一般后缀名为.so,gcc在编译时默认使用动态库。
可执行链接格式(Executable and Linking Format)最初是由 UNIX 系统实验室(UNIX System Laboratories,USL)开发并发布的,作为应用程序二进制接口(Application Binary Interface,ABI)的一部分。工具接口标准(Tool Interface Standards,TIS)委员会将还在发展的 ELF 标准选作为一种可移植的目标文件格式。
Linux下面一共有四类目标文件是按照ELF的格式来保存的,如下:
由此我们可知由汇编器生成的就是可重定位目标文件,经过链接器作用后才生成可执行目标文件,链接器的作用就是以一组可重定位目标文件作为输入,生成可加载和运行的可执行目标文件,具体需要完成以下两个工作:
最终生成可执行目标文件
注意:.a的静态链接库可以理解为.o的打包,因此本质上也属于ELF类型。
说到这里好像还是没有说清楚这两种目标文件有什么区别,我们还是先把这个问题放一下,相信你看完下一节,应该会有答案,下面我们开始引入目标文件ELF文件。
目标文件再不同的系统或平台上具有不同的命名格式,在Unix和X86-64 Linux上称为ELF(Executable and Linkable Format, ELF)。
ELF文件格式提供了两种不同的视角,在汇编器和链接器看来,ELF文件是由Section Header Table描述的一系列Section的集合,而执行一个ELF文件时,在加载器(Loader)看来它是由Program Header Table描述的一系列Segment的集合
左边是从汇编器和链接器的视角来看这个文件,开头的ELF Header描述了体系结构和操作系统等基本信息,并指出Section Header Table和Program Header Table在文件中的什么位置,Program Header Table在汇编和链接过程中没有用到,所以是可有可无的,Section Header Table中保存了所有Section的描述信息。右边是从加载器的视角来看这个文件,开头是ELF Header,Program Header Table中保存了所有Segment的描述信息,Section Header Table在加载过程中没有用到,所以是可有可无的。注意Section Header Table和Program Header Table并不是一定要位于文件开头和结尾的,其位置由ELF Header指出,上图这么画只是为了清晰。
我们在汇编程序中用.section
声明的Section会成为目标文件中的Section,此外汇编器还会自动添加一些Section(比如符号表)。Segment是指在程序运行时加载到内存的具有相同属性的区域,由一个或多个Section组成,比如有两个Section都要求加载到内存后可读可写,就属于同一个Segment。有些Section只对汇编器和链接器有意义,在运行时用不到,也不需要加载到内存,那么就不属于任何Segment。
目标文件需要链接器做进一步处理,所以一定有Section Header Table;可执行文件需要加载运行,所以一定有Program Header Table;而共享库既要加载运行,又要在加载时做动态链接,所以既有Section Header Table又有Program Header Table。
这里贴一下《程序员的自我修养》一书中的一个ELF文件常见段和对应用途总结表:
下图用一个简单的示例直观的表示了程序编译后的ELF目标文件格式(省略了一些段):
下面以一个修改过的《程序员的自我修养》一书中的例子来实际看看目标文件的各个段的情况
文件:test.c
int printf(const char* format , ...);
int global_init_var = 1; //data段
int global_uninit_var; //bss段
void func1(int i){
printf("%d\n", i); //%d\n 在rodata段
}
int main(void){
static int static_init_var = 2; //data
static int static_uninit_var; //bss
static const int static_const_init_var = 3; //rodata
static const int static_const_uninit_var;
const int const_init_var = 4;
int init_var = 5;
int uninit_var;
func1(static_init_var + static_uninit_var + static_const_uninit_var + static_const_uninit_var + init_var + uninit_var);
return init_var;
}
gcc -c test.c
编译得到text.o
下面用readelf
工具读出目标文件test.o
的ELF Header和Section Header Table,然后我们逐段分析。
首先会是一个文件头,它描述了这个ELF文件的属性,包括是否是可执行文件、大端还是小端、文件适应的目标硬件架构等。文件头内容如下:readelf -h test.o
文件头中还有一个段表(Section Header Table),包含了各个段的信息,段表如下:readelf -S test.o
可以看到一共有13各个段,而后面要用到的ojbdump -h
命令则会省略一些不关键的辅助性质的段。
根据以上信息可以描绘出整个目标文件的布局。参考:ELF文件格式
改用objdump
命令查看段情况如下:objdump -h test.o
可以看到有id从0-5的6个段(省略了非关键的段),可以使用objdump -s -d test.o
命令可以看到各段存放的内容按16进制展示如下:
而3(.bss)因为没有实际内容所以不包含在里面,其中4、5、6是辅助功能用到的段,这里先不讨论,下面我们看下0 1 2 3三个段,也就是代码段.text,只读数据段.rodata,数据段.data,以及.bss段。
代码段里保存的都是机器码,用objdump -s -d test.o
命令可以得到反汇编之后的汇编代码,内容如下:
这里不过多解释汇编语句,可以看到内容对应我们写的两个函数。
.rodata
,根据字面意思就很好理解,read only data
,和.data
段类似,但是是保存只读的静态常量,
可以看到有两个只读数据,因为字节序(大端小端)的关系字节的顺序和我们的习惯顺序是反着的,0x25640a00是"%d\n"对应的ascii码加上一个结束的\0,而0x03000000则是对应的static const int static_const_init_var = 3;
只有静态变量或者常量才有必要提前定义在数据段里,所以可以看到const int const_init_var
这种并不会保存在数据段里,而是直接在指令里写死临时分配在栈上,可以参考text段
的汇编代码。
数据段保存已经初始化的全局静态变量和局部静态变量,0x01000000和0x02000000
分别对应int global_init_var = 1;
和static int static_init_var = 2;
.bss
段(Block Started by Symbol)则保存未初始化的全局变量和局部静态变量,实际上只是place holder,不会保存实际内容,可以说是通过.bss段给变量预留空间,不需要占用ELF文件的空间,加载到内存里才会实际占用空间。上面的例子里我们也能看.bss段在列表里,但并没有.bss段的内容。需要注意的一种特殊情况是初始化为0也有可能被编译器当成未初始化放在.bss段里以节省空间。
可以总结为:
这个名字不像其他段那么直观,有兴趣进一步深入了解的可以参照【参考4】和【参考5】。
重定位表是用于链接阶段的重定位的,在独立地生成每个编译单元的时候很多变量和函数的地址是没法确定的,需要在链接阶段进行修正,后续静态链接会详细说明这个过程,这里先看下重定位表的结构,每一个需要重定位操作的段都会对应一个重定位表段,比如.text
对应了一个.rela.text
,可以用objdump -r
命令查看,可以看到上面的示例程序有两个重定位表段,分别是.text
的和.eh_frame
的。
以printf的调用,也就是.text
重定位表中的第2行为例,这行是说OFFSET为1b的地方需要后续链接阶段重定位,.text
段的1b位置正是对printf的call指令的寻址部分,也就是printf的地址需要重定位,重定位类型为R_X86_64_PC32
,这是一种相对寻址的重定位类型,后续聊静态链接的时候再展开讲。
ELF文件中用到了很多字符串,比如段名、变量名等,通常由.strtab
和.shstrtab
两个段保存,分别为字符串表(string table)和段表字符串表(section header string table),前者用来保存普通的字符串,比如符号的名字,后者用来保存段表中用到的字符串,比如段名。因为字符串是变长的,这里采取的是连续保存并用\0分割,通过offset
来获取。
我们可以用readelf命令打出来:readelf -x .strtab test.o
readelf -x .shstrtab test.o
这里贴个ascii码表方便对照着看:
比如.strtab这个字符串,2e7374 72746162就是对应内容,前后各有一个\0,因此有一个为9的offset就可以获取到。
要将多个目标文件链接在一起,本质上就是将各个目标文件的内容合并后并且能保证运行时互相的变量访问和函数调用正常,也就是对内外部函数和变量的访问都能找到正确的地址,在链接中将函数和变量统称为符号,函数名或变量名就是符号名,整个链接过程的核心就是根据符号来确定正确的地址。每一个目标文件都会有一个相应的符号表(Symbol Table,.symtab段),记录了目标文件所用到的所有符号,注意是所用到的所有,不管包含在内部的符号还是外部符号。每个定义的符号有一个对应的值,叫符号值,对于变量和函数来说,就是他们的地址。
符号有不同的类型,上图说明了:
(1)func1和main的类型是T
,说明是在.text段,且全局可见。
(2)global_init_var的类型是D
,表明是全局可见且在.data段的。
(3)global_uninit_var的类型是C
,表明是全局可见的在common块的。
(4)printf的类型是U
,说明是未定义的,该符号在编译单元外部。
(5)static_const_init_var的类型是r
,说明在.rodata段。
(6)static_const_uninit_var和static_uninit_var的类型是b
,说明在.bss段。
(7)static_init_var的类型是d
,表明是局部可见且在.data段,大小写表明了可见性。
先看可执行文件header的变化 readelf -a test
- Type:类型由目标文件类型变成可执行文件类型
- Number of program headers: 9 (test.0 是 0)
- Number of section headers: 31
在看section header的变化 readelf -S test
.text
和.data
的加载地址分别改成了0x00400430和0x00601028。.rel.text
段就是用于链接过程的,链接完了就没用了,所以也删掉了。
在看多出来的program header
这部分要再根据:ELF文件格式 消化一下
本章节用到的工具命令
#查看test的ELF Header头信息
readelf -h test
#查看Section Header Table中的每个Section Header Entry信息
readelf -S test
objdump -h test
#查看各段存放的内容按16进制展示如下
objdump -s -d test
Linux 下查看可执行文件的工具(这里列出一部分)
其他参考资料: