在之前写的文章“编译和链接”这篇中讲述了C语言程序经过预编译、编译生成目标文件,那么目标文件里面究竟是如何存储编译后的内容呢?本篇文章做以讲述。
1.目标文件的格式
目标文件是源代码经过编译之后但没有进行链接的中间文件,和可执行文件的内容与结构很相似,因此目标文件的格式和可执行文件格式几乎是一样的。在Windows下,称为PE_COFF文件格式,Linux下为ELF文件格式。
注:ELF格式的文件有一下四类:
①可重定位文件:此类文件包含代码和数据,可以被用来链接成可执行文件或共享目标文件。
②共享目标文件:此类文件包含代码和数据,可以和其他可重定位文件链接产生新的目标文件,也可以通过动态链接器将多个共享目标文件与可执行文件结合,作为进程映像的一部分来运行。
③可执行文件:此类文件包含可以直接执行的程序,例如:ELF可执行文件。
④核心转储文件:当程序意外终止,系统可以将该进程的地址空间的内容以及终止时的一些其他信息转出到核心转储文件。
2.目标文件的样子
目标文件的内容是由一个一个的段组成,每一个段都有自己的名字,段里面存放的某一类的信息。
如下图:
注:为什么需要将目标文件里面的信息划分为许多段,而不全部放在一个段里面,就数据段和代码段为例说说原因:
①数据区可以读写,代码段只读,放在不同的段里面方便设置读写权限,从而避免代码段的内容被无意间修改。
②现在的CPU缓存一般都被设计为指令缓存和数据缓存分离,所以目标文件分为代码段和数据段有利益提高CPU的缓存命中率。
③当程序中运行这个多个该程序的副本时,它们的指令都是一样的,内存秩序保存一份该程序的指令部分。
3.目标文件的各个段
(1)代码段
通过objdump这个指令来查看程序各个段的相关内容。
参数“-s”:可以将所有段的内容以十六进制的方式打印出来。
参数“-d”:可以将所有包含指令的段反汇编。
(2)数据段和只读数据段
数据段:存放已经初始化的全局变量和局部静态变量。
只读数据段:存放只读数据,一般为程序中被const修饰的变量和字符串常量。
(3)BSS段
存放的是未初始化的局部静态变量。在有些编译器下未初始化的全局变量在BSS段,但是在有的编译器下则是一个未定义的“COMMON符号”,等待最终链接成可执行文件的时候再在.bas段分配空间。
(4)其他段
(5)自定义段
GCC提供一个扩展机制,可以是程序员指定变量所处的段;
__sttribute__((section("FOO"))) int globat =42;
指定全局变量global位于FOO段中
4.ELF文件结构描述
(1)文件头
文件头结构有两种版本。分别是Elf32_Ehdr,Elf_Ehdr。这两种版本的内容一样,只不过成员大小不同。
Elf32_Ehdr的定义如下:
typedef struct
{
unsigned char e_ident[16];
Elf32_Half e_type;//文件类型
Elf32_Half e_machine;//ELF文件的CPU平台属性
Elf32_Word e_version;//ELF版本号
Elf32_Addr e_entry;//入口地址
Elf32_Off e_phoff
Elf32_Off e_shoff;//段表在文件中的偏移
Elf32_Word e_flags;//ELF标志位
Elf32_Half e_ehsize;//ELF文件头本身的大小
Elf32_Half e_phentsize;
Elf32_Half e_phnum;
Elf32_Half e_shentsize;//段表描述符的大小
Elf32_Half e_shnum;//段表描述符数量
Elf32_Half e_shstrndx;
}Elf32_Ehdr;
(2)段表
段表保存段的基本属性和结构。
段表以数组的方式存放每一个段的详细信息,数组的每一个成员是一个自定义的结构体类型:
如:Elf32_Shdr段描述符结构;
typedef struct
{
Elf32_Word sh_name;//段名
Elf32_Word sh_type;//段的类型
Elf32_Word sh_flags;//段的标志
Elf32_Addr sh_addr;//段的虚拟地址
Elf32_Off sh_offset;//段偏移
Elf32_Word sh_size;//段的长度
Elf32_Word sh_link;//段链接信息
Elf32_Word sh_info;
Elf32_Word sh_addralign;//段地址对齐
Elf32_Word sh_entsize;//项的长度
Elf32_Shdr;
}
注:
①段的类型:
②段的标志:
③段的链接信息:
(3)重定位表
重定位表里面记录当前端的重定位信息。需要注意的是:当某一个段里面有需要重定位的代码段和数据段的时候,就会有与其对应的重定位表。
(4)字符串表
字符串表中存放很多普通字符串,比如:段名、变量名……,然后使用字符串在表中的偏移来引用字符串。
5.链接的接口——符号
将函数和变量统称为符号,函数名或变量名就是符号名。
(1)ELF符号表结构
符号表的结构很简单,是一个Elf32_Sym结构的数组,每一个Elf32_Sym结构对应一个符号。
(2)特殊符号
使用ld作为链接器来链接产生可执行文件时,会定义许多特殊的符号,这些符号并没有在程序中定义,但是可以直接声明并引用它。
例如:
程序的起始地址:__executable_start。
代码段结束地址:__etext。
数据段结束地址:_edata。
程序结束地址:_end。
(3)符号修饰与函数签名
为了放置符号名的冲突,在源代码文件中的所有全局变量和函数经过编译以后,都会对相应的符号进行修饰,从而减少多种预压目标文件之间的符号冲突的概率。
函数签名包含一个函数的信息,从而识别不同的函数,通过使用某种名称修饰的方法,使得每一个函数签名对应一个修饰后名称。
(4)exten"C"
使用exten"C"表名其后面紧跟的内容时为C语言的符号。
注:
#ifdef __cplusplus
extern "C"
#endif
void *memset(void*,int,size_t);
#ifdef _cpiusplus
#endif
使用C++的宏”__cplusplus“来判断当前编译单元是不是C++代码,如果是C++代码,那么就会执行里面的语句,否则表名是C的代码。
(5)弱符号与强符号
弱符号:未初始化的全局变量。
强符号:编译器默认函数和初始化的全局变量。
注:
使用__attribute__((weak))来定义一个强符号为弱符号。
6.调试信息
目标文件中还可能保存调试信息,ELF文件中采用一个叫DWARF的标准的调试信息格式。