一、可执行文件格式
现在PC平台流行的可执行文件格式主要是Windows下的PE( Portable Executable ) 和Linux 的ELF( Executable Linkable Format ), 它们都是COFF( Common file format )格式的变种。
1、ELF文件结构描述
(1)ELF文件头(ELF header)
ELF文件格式最前部是:ELF文件头(ELF header),包含描述整个文集基本属性的信息,比如ELF魔数、文件机器字节长度、数据存储方式、版本、运行平台、ABI版本、ELF重定位类型、硬件平台、硬件平台版本、入口地址、程序头入口和长度、段表的位置和长度及段的数量等。
ELF文件头结构及相关常数被定义在“/usr/include/elf.h”里,因为ELF文件有32位版本和64位版本,它的文件头结构也有这两个版本,分别叫做“Elf32_Ehdr”和“Elf64_Ehdr”
(2)段表(Section Header Table)
它描述了ELF的各个段的信息,比如每个段的段名、段的长度、在文件中的偏移、读写权限及段的其他属性。ELF文件的段结构就是由段表决定的,编译器、链接器和装载器都是依靠段表来定位和访问各个段的属性的。
段表在ELF文件中的位置由ELF文件头的“e_shoff”成员决定。
段表的结构是一个以“Elf32_Shdr”结构体为元素的数组。数组元素的个数等于段的个数,每个“Elf32_Shdr”结构体对应一个段。
“Elf32_Shdr”又称为段扫描符(Section Despcriptor)。
段表数组的第一个元素是无效的段扫描符,它的类型为NULL,除此之外每个段扫描符都对应一个段。
(3)、重定位表(Relocation Table)
链接器在处理目标文件时,需要对目标文件中的某些部位进行重定位,即代码段和数据段中哪些对绝对地址的引用的位置。这些重定位的信息都记录在ELF文件的重定位表里面,对于每个需要重定位的代码段或数据段,都会有一个相应的重定位表。(只要段中有对绝对地址的引用,就会有一个相应的重定位表)。
一个重定位表同时也是ELF的一个段,这个段的类型(sh_type)就是“SHT_REL”类型的,它的“sh_link”表示符号表的下标,它的“sh_info”表示它作用于哪个段。比如“.rel.text”作用于“.text”段。
(4)、字符串表
ELF文件中用到了许多字符串,比如段名、变量名等。它们集中存放在一个表中,然后使用字符串在表中的偏移来引用字符串。通过这种方法,在ELF文件中引入字符串只须以段的形式保存,常见的段名为“.strtab”或“.shstrtab”。这两个字符串表分别为字符串表(String Table)和段表字符串表(Section Header String Table)。字符串表用来保存普通的字符串,比如符号的名字;段表字符串表用来保存段表中用到的字符串,最常见的就是段名(sh_name)。
ELF文件头中的“e_shstrndx”的含义是:段表字符串表所在段在段表中的下标,段表字符串表本身也是ELF文件中的一个普通的段,“e_shstrndx”的内容标识了段表字符串表在段表中的位置。
(5)、符号表(Symbol Table)
在链接中,我们将函数和变量统称为符号(Symbol),函数名或变量名就是符号名(Symbol Name );
每一个目标文件中都会有一个相应的符号表(Symbol Table),这个表里面记录了目标文件中所用到的所有符号,每个定义的符号有一个对应的值,叫做符号值(Symbol Value),对于变量和函数来说,符号值就是它们的地址。
符号表中符号的分类:
*** 定义在本目标文件的全局符号,可以被其他目标文件引用;
*** 在本目标文件中引用的全局符号,却没有定义在本目标文件,一般叫做“外部符号(External Symbol)”;
*** 段名,这种符号往往由编译器产生,它的值就是该段的起始地址。比如“.text”、“.data”等;
*** 局部符号,这类符号只在编译单元内部可见,调试器可以使用这些符号来分析或崩溃时的核心转储文件。这些局部符号对于链接过程没有作用,链接器往往也忽略它们;
*** 行号信息,即目标文件指令与源代码中代码行的对应关系,它也是可选的。
链接过程只关心全局符号的相互“粘合”,即只关心前两类全局符号,局部符号、段名、行号等都是次要的,它们对于其他目标文件来说是“不可见的”,在链接过程中也是无关紧要的。
ELF文件中的符号表往往是文件中的一个段,段名一般叫做“.symtab”。 符号表的结构是一个Elf32_Sym结构(32位ELF文件)的数组,每一个Elf32_Sym结构对应一个符号。这个数组的第一个元素,也就是下标为0的元素为无效的“未定义”符号。
Elf32_Sym结构包含六个字段:
typedef struct{
Elf32_Word st_name; //符号名
Elf32_Addr st_value; //符号值
Elf32_Word st_size; //符号大小
unsigned char st_info; //符号类型和绑定信息
unsigned char st_other; //该成员目前为0,保留
Elf32_Half st_shndx; //符号所在的段
} Elf32_Sym;
2、特殊符号
当我们使用Id作为链接器来链接生产可执行文件时,它会为我们定义很多特殊的符号,这些符号是被定义在Id链接器的链接脚本中的,但是可以在程序中直接声明并且引它,我们称之为特殊符号。
链接器会在将程序最终链接成可执行文件时将其解析成正确的值,注意,只有使用Id链接生产最终可执行文件的时候这些符号才会存在。
几个很具有代表性的特殊符号如下:
__executable_start :表示程序起始地址,注意,不是入口地址,时程序的最开始的地址;
__etext 或_etext 或 etext ,表示为代码段的结束地址,即代码段最末尾的地址;
_edata 或 edata ,表示数据段结束地址,即数据段最末尾的地址;
_end 或 end, 表示程序结束地址。
注:以上地址都是程序被装载时的虚拟地址。
3、符号修饰与函数签名
最开始,编译器编译源代码产生目标文件时,符号名与相应的变量和函数的名称是一样的。比如一个汇编源代码里面包含了一个函数foo,那么汇编器将它编译成目标文件后,foo函数在目标文件中相对应的符号名也是foo。
后来,当UNIX平台和C语言发明时,已经存在了相当多的使用汇编编写的库和目标文件。问题是:如果一个C程序要使用这些汇编编写的库和目标文件,那么C语言中不可以使用这些库中定义的函数和变量的名字作为符号名,否则将会跟现有的目标文件冲突。
此时,一种简单而原始的解决方法是:UNIX下的C语言就规定,C语言源代码文件中的所有全局变量和函数经过编译以后,相对应的符号名前加上下划线“_”。而Fortran语言的源代码经过编译以后,所有的符号名前加上“_”,后面也加上“_”。这种方法的确能够暂时减少多种语言目标文件之间的符号冲突的概率,但是还是没有从根本上解决符号冲突的问题。比如说,当程序很大时,不同的模块有多个部门(个人)开发
,它们之间的命名规范如果不严格,则有可能导致冲突。
再后来,C++在设计时开始考虑到这个问题,增加了名称空间(Namespace)的方法来解决多模块的符号冲突问题。
随着时间的推移,许多操作系统和编译器被完全重写了好几遍,上面提到的与古老的汇编库的符号冲突问题已经不是那么明显了。现在的Linux下的GCC编译器中,默认情况下已经去掉了在C语言前加“_”的方式,但是Windows平台下的编译器还保持原来的传统,比如Visual C++编译器就会在C语言符号前面加“_”。GCC在WIndows平台下的本本也会加“_”。GCC编译器也可以通过参数选型“-fleading-underscore”或“-fno-leading-underscore”来打开和关闭是否在C语言符号前加上“_”。
由于C++拥有类、继承、虚机制、重载、名称空间等这些特性。它们使得符号管理更为复杂。为了支持C++这些复杂的特性,人们发明了符号修饰(Name Decoration)或符号改编(Name Mangling)的机制。
函数签名(Function Signature),函数签名包含了一个函数的信息,包括函数名、它的参数类型、它所在的类和名称空间及其他信息。函数签名用于识别不同的函数,就像签名用于识别不同的人一样,函数的名字只是函数签名的一部分。
在编译器及链接器处理符号时,它们使用某种名称修饰的方法,使得每个函数签名对应一个修改后名称(Decorated Name)。编译器在将C++源代码编译成目标文件时,会将函数和变量的名字进行修饰,形成符号名,C++编译器和链接器都使用符号来识别和处理函数和变量,所有对于不同函数签名的函数,即使函数名相同,编译器和链接器都认为它们是不同的函数。
注意:签名和名称修饰机制不光被使用到函数上,c++中的全局变量和静态变量也有同样的机制。
注:Microsoft提供了一个修饰后名字转换成函数签名的API,比如在链接、调试程序的时候可能会用到。该API是:UnDecorateSymbolName().
由于不同的编译器采用不同的名字修饰方法,必然会导致由不同编译器产生的目标文件无法正常相互链接,这是导致不同编译器之间不能互操作的主要原因之一。
4、extern "C"
c++为了与C兼容,在符号的管理上,C++有一个用来声明或定义一个C的符号的“extern "C" ”关键字。
C++编译器会将在extern “C”的大括号内部的代码当作C语言代码处理,这时C++的名称修饰机制将不会起作用,extern “C”里面的符号都是修饰后的符号。
注:在C++中使用c代码的一个小技巧:
问题:很多时候我们会碰到有些头文件声明了一些C语言的函数和全局变量,但是这个头文件可能会被C语言代码或者C++代码包含。如果不加任何处理,当我们的C语言程序包含该头文件时,并且用到了其中的函数,编译器会将该函数符号引用正确处理,但是在C++语言中,编译器会认为这个函数是一个C++函数,会使用符号修饰和函数签名机制,这样链接器就无法与C语言库中的该函数符号进行链接。
解决方法:可以使用C++的宏“__cplusplus”,C++编译器会在编译C++的程序时默认定义这个宏,可以使用条件宏来判断当前编译单位是不是C++代码,例如:
#ifdef _cplusplus
extern "C" {
#endif
void *memset( void *, size_t );
#ifdef __cplusplus
}
#endif
5、弱符号和强符号
初始化的全局变量的定义称为强符号(Strong Symbol),未初始化的全局变量的定义称为弱符号(Weak Symbol)。对于C/C++语言来说,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。注意:强符号和弱符号是针对定义来说的。 通过GCC的“__attribute__((weak)) ”来定义任何一个强符号为弱符号。例如:
int weak;
int strong = 1;
__attribute__( (weak) ) weak2 = 2;
int main()
{
return 0;
}
其中, "weak"和“weak2”是弱符号, “strong”和“main”是强符号,而“ext”既非强符号也非弱符号,因为它是一个外部变量的引用。
链接器会按照如下规则处理与选择被多次定义的全局符号:
* 规则1:不允许强符号被多次定义(即不同的目标文件中不能有同名的强符号);如果有多个强符号定义,则链接器报符号重复定义错误。
* 规则2:如果一个符号在某个目标文件时强符号,在其他文件中都是弱符号,那么选择强符号。
* 规则3:如果一个符号在所有目标文件中都是弱符号,那么选择其中占用空间最大的一个。(尽量不要使用多个不同类型的弱符号,否则容易导致很难发现程序错误)。
强引用(Strong Reference)与弱引用(Weak Reference )
强引用如果没有定义,链接器会报未定义错误,弱引用如果未定义,链接器不会报告错误。一般对于未定义的弱引用,链接器默认其为0,或者是一个特殊的值,以便于程序代码能够识别。
GCC中,我们通过使用“__attribute__( (weakref ) )”关键字来声明对一个外部函数的引用为弱引用,比如:
__attribute__ ( (weakref) ) void foo();
int main()
{
foo();
}
上述代码在编译成一个可执行文件时,GCC并不会报链接错误,但是运行该可执行文件时会发生运行错误。
这种弱符号和弱引用对于库来说是十分有用的,比如库中定义的弱符号可以被用户定义的强符号所覆盖,从而使得程序可以使用自定义版本的库函数;或者程序可以对某些扩展功能模块的引用定义为弱符号,当我们将扩展模块与程序链接在一起时,功能模块就可以正常使用;如果我们去掉了某些功能模块,那么程序也可以正常链接,只是缺少了相应的功能,这使得程序的功能更加容易裁剪和组合。
6、调试信息
几乎所有的现代编译器都支持源代码级别的调试,前提是编译器必须提前将源代码与目标代码之间的关系等,保存到目标文件中。
现在的ELF文件采用一个叫DWARF( Debug With Arbitrary Record Format)的标准的调试信息格式。Microsoft自己的调试信息格式是CodeView。
注意:调试信息在目标文件和可执行文件中占用很大的空间,往往比程序的代码和数据本身大好几倍,所以Release发布时,需要将调试信息去掉。
二、Windows平台 PE/COFF 文件格式
Windows下的可执行文件采用PE(Protable Executable)文件格式,Visual C++编译器产生的目标文件使用COFF(Common Object File Format)格式。
PE格式是COFF格式的一种扩展,它们在结构上很大程度上是相同的,甚至跟ELF文件的基本结构也相同,都是基于段的结构。
<一>、COFF文件结构
1、 COFF文件由文件头及后面的若干个段组成。
COFF文件的文件头部包含两个部分:映射头(Image Header)和段表(Section Table),映射头描述文件总体结构和属性;段表描述文件中包含的段属性。
映射头是一个“IMAGE_FILE_HEADER”的结构,其定义如下:
typedef struct _IMAGE_FILE_HEADER{
WORD Machine; //目标机器类型
WORD NumberOdSections; //文件包含的“段”的数量
DWORD TimeDateStamp; //文件的创建时间
DWORD PointerToSymbolTable; //符号表在文件中的位置
DWORD NumberOfSymbols; //符号的数量
WORD SizeOfOptionalHeader; //Optional Header 的大小,该结构只存在于PE可执行文件,COFF目标文件中该结构不存在,为0
WORD Characteristics; //
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
COFF文件的段表是一个“IMAGE_SECTION_HEADER”结构的数组,数组中的每一个元素代表一个段。该结构定义如下:
typedef struct _IMAGE_SECTION_HEADER{
BYTE Name[8]; //段名
union {
DWORD PhysicalAddress; //物理地址
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress; //虚拟地址
DWORD SizeOfRawData; //原始数据大小(该段在文件中的大小)
DWORD PointerToRawData; //段在文件中的位置
DWORD PointerToRelocations; //该段的重定位表在文件中的位置
DWORD PointerToLinednumbers; //该段的行号表在文件中的位置
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics; //段的属性(主要包含段的类型、对齐方式、读写和执行的权限等)
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
2、COFF文件头后跟的就是一个个段的实际内容,像代码段、数据段、BSS段几乎和ELF文件一样。还有一些段是ELF文件中不存在的段,就是:“.drectve”段和“.debug$S”段。
(1)、 ".drectve"段实际上是“Directive”的缩写,它的内容是编译器传递给链接器的指令(Directive),这些指令告诉链接器应该怎样链接这个目标文件。
该段的最后一个属性是标志位“flags”,即IMAGE_SECTION_HEADERS里面的Characteristics成员。在该段中该标志位是“0x100A00”,是以下位的组合:0x00100000( IMAGE_SCN_ALIGN_1BYTES: 1字节对齐,相当于不对齐)、0x00000800(IMAGE_SCN_LNK_REMOVE:最终链接成映射文件时抛弃该段)、0x00000200( IMAGE_SCN_LNK_INFO: 该段包含的是注释或其他信息),输出信息中flags之后是该段在文件中的原始数据(RAW DATA),该原始数据的解析结果是“链接指令”。
(2)、“.debug”段包含调试信息,比如“.debug$S”表示包含的是符号(Symbol)相关的调试信息段; “.debug$P”表示包含预编译头文件(Precompiled Header Files)相关的调试信息段,“.debug$T”表示包含类型(Type)相关的调试信息段。
<二>PE文件
PE文件基于COFF的扩展,最主要的变化有两个:文件头不是COFF文件头,而是“DOS MZ可执行文件格式的文件头和桩代码(DOS MZ File Header and Stub)”,第二个变化是:PE文件头结构包含了原来COFF中的“Image Header“及新增的PE扩展头部结构(PE Optinal Header)。
PE文件的结构:
PE文件中的”Image DOS header“和”DOS Stub“这两个结构就是为了兼容DOS系统而设计的;
IMAGE_DOS_HEADER结构中唯一值得关心的是”e_lfanew“成员,这个成员表明了PE文件头(IMAGE_NT_HEADERS)在PE文件中的偏移,我们需要使用这个值来定位PE文件头。
当Windows开始执行一个后缀名为”.exe“的文件时,它会判断”e_lfanew“成员是否为0,如果为0,则该”.exe“文件时一个DOS ”MZ“可执行文件,WIndos会启动DOS子系统来执行它,如果不为0,那么它就是一个Windows的PE可执行文件。
1、”IMAGE_NT_HEADERS“是PE真正的文件头,它的结构如下(一个标记+两个结构体):
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER OptionalHeader;
} IAMGE_NT_HEADERS, *PIMAGE_NT_HEADERS;
"IMAGE_FILE_HEADER"与COFF目标文件中的”IMAGE_FILE_HEADER“一样,”IMAGE_OPTIONAL_HEADER“是PE可执行文件(包括DLL)必须的,COFF目标文件中没有该结构。
IMAGE_OPTIONAL_HEADER结构的定义如下:
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
//
// NT additional fields
//
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SIzeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[ IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
PE数据目录
“IMAGE_OPTIONAL_HEADER”结构中的“DataDirectory”成员是一个“数据目录(DataDirectory)”的结构,由于PE文件在装载时往往要很快找到一些需要装载的数据结构,比如导入表、导出表、重定位表,这些常用的数据的位置和长度都被保存在“数据目录”结构中。
该结构的定义如下:
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VIrtualAddress;
DWORD Size;
} IAMGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY ;
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
注:在“WinNT.h”中定义了一些以“IMAGE_DIRECTORY_ENTRY_”开头的宏,数值从0到15,用作相关表的宏定义在数组中的下标,例如:IMAGE_DIRECTORY_ENTRY_EXPORT被定义为0,表示数组的第一个元素包含的是导出表(Export Table)所在的地址和长度。