我们经过编译的目标文件,里面到底存放的内容是什么呢?其实,目标文件本身已经是编译后的可执行文件了,只是还没有经过链接,所以内部很多符号还没有分配地址。其实目标文件本身就是按照可执行文件的格式进行存储的,只是目标文件和可执行文件在文件结构上略微有些区别。
1.目标文件的格式
目前主流的pc平台所流行的可执行文件格式主要分为两种,一种是Windows下的PE(Portable Executable),另一种是Linux下的ELF(Executable Linkable Format),它们都是COFF(Common File Format)的变种。广义上来看,目标文件,动态库文件和可执行文件其实都是同样按照可执行文件的格式存储的。
以Linux平台为例,我们可以看到目标文件,动态库文件,可执行文件的文件格式都为ELF文件,区别在于目标文件是Relocatable,动态库文件是Shared object,可执行文件是Executable。
目标文件(MBB_PUSCH_FDP_Task.o)
![]()
动态库文件(libfft.so)
可执行文件 (ls)

ELF文件标准里把系统中采用ELF格式的文件归为4类,分别是:
(1)可重定位文件(Relocatable): 这类文件包含了代码和数据,可以被用来链接成可执行文件或者共享库文件。
(2)可执行文件(Executable): 这类文件包含了可以直接执行的程序。
(3)共享文件(Shared object): 这类文件包含了代码和数据,可以被用来和可重定位文件连接成新的目标文件,或者可以被用来和可执行文件一起动态链接,即在可执行程序执行过程中,加载进程序进程映像的一部分来运行。
(4)核心转储文件(Core Dump): 当进程意外终止时,系统可以将该进程的地址空间的内容及终止时的一些其让信息存储到该文件中,进行问题定位排查。
注:静态库文件其实是许多目标文件的集合,以标准静态库文件为例,我们使用Linux下的ar命令,可以看到libc.a由许许多多的.o文件组成。

2.目标文件的内容
我们知道,一个可执行程序有许多“段”来组成,他们都表示一个一定长度的内存块,都有特定的内存属性,例如数据段保存的是需要初始化的全局变量,代码段保存的是我们具体的指令等等,其实目标文件同样也是由众多的段组成的,我们可以以下述代码为例:
/*
SimpleSection.c
*/
int printf( const char* format, ...);
int global_init_var = 84;
int global_uinit_var;
void func1(int i)
{
printf("%d\n", i);
}
int main()
{
static int static_var = 85;
static int static_var2;
int a = 1;
int b;
func1( static_var + static_var2 + a + b);
return 0;
}
在linux环境下执行下述命令,我们可以得到一个SimpleSection.o目标文件,这个文件的大小为1872Byte.
gcc -c SimpleSection.c

在linux环境下执行下述命令,我们可以看到,SimpleSection.o目标文件内部除了包括数据段,代码段,BSS段之外,还包括了rodata段,comment段,note.GNU-stack段和eh_frame段。
objdump -h SimpleSection.o

每个段都包含了一些诸如CONTENT,ALLOC,LOAD之类的描述字段,以CONTENT为例,CONTENT描述了该段是否在该目标文件中存在,我们发现,BSS段没有CONTENT描述,因此可知BSS段虽然有大小定义,但本身并不保存在目标文件中。同时,我们还注意到了虽然.note.GNU-stack段有CONTENT描述,但该段的大小为0。因此,根据图中每个段的起始偏移和大小,我们可知目标文件的内容如下:

.data段保存的是初始化的全局静态变量和局部静态变量,对应上述代码中的global_init_varabal和static_var这两个变量,每个变量1个word,因此.data段的大小刚好为8个字节。.rodata段保存的是只读常量数据,上述代码中,函数printf的第1个参数是一个字符串常量"%d\n",因此.rodata段的大小是4个字节。.bss段保存的是未初始化的全局变量和静态局部变量,对应上述代码中的global_uninit_var和static_var2。但是这两个变量每个大小都是1个word?为何.bss段的总大小才只有4byte呢?因为在gcc便一起下,全局的未初始化变量在目标文件中指示预留一个未定义的符号,等到最终链接成可执行文件的时候才在.bss段中分配空间。
3.自定义段
除了上述段之外,目标文件中很可能还包括其他各种各样的段,例如.debug段保存的是调试信息,.comment段保存的是编译器的版本信息,.line段保存的是调试时的行号信息等等。但是除了这些默认的段之外,我们可不可以自定义段空间,使得我们的变量数据保存在我们指定的段中呢?答案当然是可以的。
gcc编译器提供了扩展机制,允许我们自定义段空间,语法如下:
__attribute__((section("FOO"))) int global = 42;
我们可以看到,新编译生成的目标文件中多了名为FOO的段。
![]()
4.ELF文件结构
除了刚刚我们提到过的段之外,细心的同学们可能发现了目标文件中除了段之外,还有ELF Header和Other Data。总体上来说,目标文件的文件结构中,除了段之外,有两个非常重要的结构,一个是ELF头,一个是段表。根据ELF头,我们才能识别这个目标文件的格式和信息,而根据段表,我们才能知道目标文件中包含的段以及每个段的属性。
(1). ELF头
在linux下使用readelf的命令“readelf -h SimpleSection.o”查看SimpleSection.o文件,显示如下,图左中的信息即为ELF头所包含的信息。下图右是64位版本的ELF头结构,可以发现,ELF头结构的大小为64Byte,所以我们在上文中可以看到第一个段.text段在目标文件中的偏移即为0x40,64Byte。

(2). 段表
段表是目标文件中除了ELF头以外最重要的结构,我们的目标文件中保存着许许多多的段,而段表则保存了这些段的基本属性,例如每个段的段名,段的长度,在文件中的偏移,读写权限和其他一些属性。也就是说,ELF文件的段结构就是由段表决定的,同时编译器,链接器和装载器都需要依靠段表来定位和访问各个段。
在linux下使用readelf的命令“readelf -S SimpleSection.o”查看SimpleSection.o文件,我们可以发现,前文中“objdump -h SimpleSection.o”只显示了目标文件中关键的一些段,而还有类似.symtab(符号表段),.Strtab(字符串表段)没有显示。

readelf输出的结果就是段表的内容,段表的结构其实很简单,它是一个“Elf64_Shdr”的结构体数组,格式如下。该结构描述了段的段名,类型(例如程序段,符号表,字符串表,重定位表等等。其中常见的数据段,代码段,bss段其实都属于程序段),标志(可写,可分配空间,可执行)等等属性。

到这一步,我们才彻底把SimpleSection.o的所有内容挖掘清楚了,该目标文件的布局如下所示:

(3). 重定位表
我们可以看到,.rela.text和.rela.eh_frame段的段类型都是“RELA”,这说明了这两个段是重定位表,那么重定位表的作用是什么呢?因为如前文所讲,在链接器进行链接的时候,需要对目标文件中一些符号重新计算地址,因此重定位表便保存了这些需要重新计算地址的符号的信息。
(4). 字符串表
Elf目标文件中用到了很多字符串来表示段名,变量名等等,每个字符串的大小长度往往都不固定,因此目标文件把字符串集中起来,存放到一个表中,然后段名,变量名等等,使用字符串在该字符串表中的偏移来引用该字符串。
(5). 符号表
我们在编译链接过程中,把函数和变量统称为符号,符号是链接过程的粘合剂,目标文件之间,正是通过符号才能正确的链接在一起。因此,符号表中保存了一个目标文件内部的所有符号的各种信息。
下一篇博文中,我们讲针对符号,展开详细的讨论。
本文详细介绍了目标文件的格式,包括Windows的PE和Linux的ELF,以及目标文件在Linux下的段组成,如.data、.rodata、.bss等。此外,还提及了自定义段、ELF头、段表、重定位表、字符串表和符号表等关键概念,为深入理解程序的链接和装载奠定基础。
5170

被折叠的 条评论
为什么被折叠?



