目标文件
目标文件的格式
可执行文件格式主要是Windows下的PE(Portable Executable)和Linux的ELF(Executable Linkable Format),都是COFF(Common file format)格式的变种。目标文件的内容和结构与可执行文件的内容与结构很相似,一般跟可执行文件格式采用一种格式存储。
可执行文件,动态链接库以及静态链接库都按照可执行文件格式存储。静态链接库稍有不同,静态链接库把很多目标文件捆绑在一起形成一个文件,再加上一些索引。
ELF文件标准把系统中采用ELF格式的文件归为下面四类。
ELF文件类型 | 说明 | 实例 |
---|---|---|
可重定位文件 | 这类文件包含了代码和数据,可以被用来链接成可执行文件或共享目标文件,静态链接库也可以归为这一类 | Linux的.o Window的 .obj |
可执行文件 | 可以直接执行的程序,一般没有扩展名 | 比如/bin/bash 文件window的 .exe |
共享目标文件 | 这种文件包含了代码和数据,可以在以下两种情况下使用,一种是链接器可以使用这种文件跟其他的可重定位文件和共享目标文件链接,产生新的目标文件。第二种是动态链接器可以将这几个这种共享目标文件与可执行文件结合,作为进程映像的一部分来运行。 | linux的.so Windows的DLL |
核心转储文件 | 当进程意外终止时,系统可以将该进程的地址空间的内容及终止时的一些其他信息 | Linux的core dump |
目标文件是什么样的
目标文件包括链接时所需要的一些信息,比如符号表,调试信息,字符串等。一般目标文件将这些信息按不同的属性,以Section
形式存储。一般情况下,它们都表示一个一定长度的区域,基本上不加以区别。
程序源代码编译后的机器指令经常被放在代码段里面,代码段常见的名字有.code
或.text
;
看一个简单的程序被编译成目标文件后的结构。
假设图3-1的可执行文件的格式是ELF
,从图中看到,ELF文件的开头是一个“文件头”,它描述了整个文件的文件属性,包括文件是否可执行,是静态链接还是动态链接以及入口地址,目标硬件,目标操作系统等信息,文件头还包括一个段表,段表其实是一个描述文件中各个段的数组。段表描述了文件中各个段在文件中的偏移位置及段的属性等,从段表中可以得到每个段的所有信息。文件头后面就是各个段的内容。
对比图3-1来看,一般C语言的编译后执行语句都编译成机器代码,保存在.text
段;已初始化的全局变量和局部静态变量都保存在.data
段,未初始化的全局变量和局部静态变量一般放在一个叫.bss
段。未初始化的全局变量和局部静态变量默认值都为0,本来它们是放在.data
段,但是它们都是0,所以为它们在.data
段分配空间并且存放数据0是没有必要的。程序运行时它们的确要占用内存空间,并且可执行文件必须记录所有未初始化的全局变量和局部静态变量的大小总和,记为.bss
段。所以.bss段只是为未初始化的全局变量和局部静态变量预留位置而已,它并没有内容,所以它在文件中也不占据空间。
总体来说,程序源代码被编译以后主要分成两种段:程序指令和程序数据。代码段属于程序指令,而数据段和.bss段属于程序数据。
挖掘SimpleSection.o
/*
* SimpleSection.o
* Linux: gcc -c SimpleSection.c
*/
int printf(const char* format, ...);
int global_init_var = 84;
int global_uninit_var;
void func1(int i)
{
printf("%d\n", i);
}
int main(void)
{
static int static_var = 85;
static int static_var2;
int a = 1;
int b;
func1(static_var + static_var2 + a + b);
return 0;
}
使用GCC编译这个文件
$gcc -c SimpleSection.c
使用以下命令查看各种目标文件的结构和内容
$objdump -h SimpleSection.o
参数-h
就是把ELF文件的各个段的基本信息打印出来。除了最基本的代码段,数据段和BSS段以外,还有三个段分别是只读代码段(.rodata
),注释信息段(.comment
)和堆栈提示段。每个段的第2行中的"CONTENTS",“ALLOC”等表示段的各种属性,“CONTENTS”表示该段在文件中存在,我们可以看到BSS段中没有“CONTENTS”,表示它实际上在ELF文件中不存在。ELF文件中实际存在的段只有.text
,.rodata
,.comment
4个段。它们的长度和在文件中的偏移位置在下图中。
代码段
利用objdump
的-s
参数将所有段的内容以十六进制的方式打印出来,-d
参数可以将所有包含指令的段反编译。
$objdump -s -d SimpleSection.o
Contents of section .text
就是.text
的数据以十六进制方式打印出来的内容,总共0x5b
字节,最左面一列是偏移量,中间4列是16进制内容,最右面一列是.text
段的ASCII码形式。对照下面的反汇编结果,可以很明显地看到,.text
段里面所包含的正是SimpleSection.c
里面两个函数func1()
和main()
的指令。.text
段的第一个字节“0x55”就是func1()
函数的第一条push %ebp
指令,而最后一个字节0xc3
正是main()
函数的最后一条指令ret
。
数据段和只读数据段
.data
段保存的是那些已经初始化的全局静态变量和局部静态变量。前面的SimpleSection.c
代码中一共有两个这样的变量,分别是global_init_varabal
与static_var
这两个变量4个字节,一共8个字节,所以.data
这个段的大小为8个字节。
SimpleSection.c
里面在调用printf
的时候,用到了一个字符串常量%d\n
,它只是一种只读数据,所以被放到了.rodata
段。这个段的4个字节的长度刚好是这个字符串常量的ASCII字节序,最后以\0
结尾。
.rodata
段存放的只是只读数据,一般是程序里面的只读变量(如const
修饰的变量)和字符串常量。单独设置.rodata
段有很多好处,在语义上支持了C++的const
关键字,而且操作系统在加载的时候可以将.rodata
段的属性映射成只读,这样对于这个段的任何修改操作都会作为非法操作处理,保证了程序的安全性。
BSS段
.bss
段存放的是未初始化的全局变量和局部静态变量,上述代码中global_uninit_var
和static_var2
被存放在.bss
段,其实更准确的说法是.bss
段为它们预留了空间,但是可以看到该段的大小只有4字节,与global_uninit_var
和static_var2
大小的8个字节不符合。
可以通过符号表看到,只有static_var2
存放在.bss
段,而global_uninit_var
却没有被存放在任何段,这其实跟不同的语言与不同的编译器实现有关,有些编译器会将全局的未初始化变量存放在目标文件的.bss
段,有些则不放,只是需要预留一个未定义的全局变量符号,等到最终链接成可执行文件的时候再在.bss
段分配空间。
ELF文件结构描述
ELF目标文件格式的最前部是ELF文件头,它包含了描述整个文件的基本属性,比如ELF文件版本,目标机器型号,程序入口地址等,紧接着是ELF文件各个段。其中ELF文件中与段有关的重要结构就是段表,该表描述了ELF文件包含的所有段的信息,比如每个段的段名,段的长度,在文件中的偏移,读写权限及段的其他属性。
文件头
可以使用readelf
命令来详细查看ELF文件。
ELF的文件头定义了ELF魔数,文件机器字节长度,数据存储方式,版本,运行平台,ABI版本,ELF重定位类型,硬件平台版本,入口地址,程序头入口和长度,段表的位置和长度以及段的数量。