程序员的自我修养--链接、装载与库笔记:Windows PE/COFF

1. Windows的二进制文件格式PE/COFF

在32位Windows平台下,微软引入了一种叫PE(Portable Executable)的可执行格式。作为Win32平台的标准可执行文件格式,PE有着跟ELF一样良好的平台扩展性和灵活性。PE文件格式事实上与ELF同根同源,它们都是由COFF(Common Object File Format)格式发展而来的,更加具体地讲是来源于当时著名的DEC(Digital Equipment Corporation)的VAX/VMS上的COFF文件格式。微软将它的可执行文件格式命名为”Portable Executable”,从字面意义上讲是希望这个可执行文件格式能够在不同版本的Windows平台上使用,并且可以支持各种CPU。

在Windows平台,VISUAL C++编译器产生的目标文件仍然使用COFF格式,而可执行文件为PE格式

随着64位Windows的发布,微软对64位Windows平台上的PE文件结构稍微做了一些修改,这个新的文件格式叫做PE32+。新的PE32+并没有添加任何结构,最大的变化就是把那些原来32位的字段变成了64位。绝大部分情况下,PE32+与PE的格式一致,我们可以将它看作是一般的PE文件。

         与ELF文件相同,PE/COFF格式也是采用了那种基于段的格式。一个段可以包含代码、数据或其它信息,在PE/COFF文件中,至少包含一个代码段,这个代码段的名字往往叫做”.code”,数据段叫做”.data”。不同的编译器产生的目标文件的段名不同,VISUAL C++使用”.code”和”.data”,而Borland的编译器使用”CODE”, “DATA”。也就是说跟ELF一样,段名只有提示性作用,并没有实际意义。

跟ELF一样,PE中也允许程序员将变量或函数放到自定义的段。在GCC中我们使用”__attribute__((section(“name”)))”扩展属性,在VISUAL C++中可以使用”#pragma”编译器指示,如”#pragma data_set(“name”)”。

2. PE的前身----COFF

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 a;
}

以管理员身份打开cmd.exe,然后在C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\bin\amd64目录下,先执行vcvars64.bat,然后定位到SimpleSection.c所在的目录E:\test\下,执行”cl.exe /c /Za SimpleSection.c”,会生成目标文件SimpleSection.obj,执行结果如下图所示:

“cl.exe”是VISUAL C++的编译器,即”Compiler”的缩写。”/c”参数表示只编译,不链接,即将.c文件编译成.obj文件,而不调用链接器生成.exe文件。如果不加这个参数,cl.exe会在编译”SimpleSection.c”文件以后,再调用link.exe链接器将该产生的SimpleSection.obj文件与默认的C运行库链接,产生可执行文件SimpleSection.exe。

VISUAL C++有一些C和C++语言的专有扩展,这些扩展并没有定义ANSI C标准或ANSI C++标准。”/Za”参数禁用这些扩展,使得我们的程序跟标准的C/C++兼容。使用”/Za”参数时,编译器自动定义了__STDC__这个宏,我们可以在程序里通过判断这个宏是否被定义而确定编译器是否禁用了Microsoft C/C++语法扩展。

跟GNU的工具链中的”objdump”一样,Visual C++也提供了一个用于查看目标文件和可执行文件的工具,就是”dumpbin.exe”。通过这个命令可以查看SimpleSection.obj的结构,”/ALL”参数是将打印输出目标文件的所有相关信息,包括文件头、每个段的属性和段的原始数据及符号表。也可以用”/SUMMARY”选项来查看整个文件的基本信息,它只输出所有段的段名和长度,执行结果如下图所示:

COFF文件结构:几乎跟ELF文件一样,COFF也是由文件头及后面的若干个段组成,再加上文件末尾的符号表、调试信息的内容,就构成了COFF文件的基本结构。COFF文件的文件头部包括了两部分,一个是描述文件总体结构和属性的映像头(Image Header),另外一个是描述该文件中包含的段属性的段表(Section Table)。文件头后面紧跟着的就是文件的段,包括代码段、数据段等,最后还有符号表等。

映像(Image):因为PE文件在装载时被直接映射到进程的虚拟空间中运行,它是进程的虚拟空间的映像。所以PE可执行文件很多时候被叫做映像文件(Image File)。

文件头里描述COFF文件总体属性的映像头是一个”IMAGE_FILE_HEADER”的结构,它跟ELF中的”Elf64_Ehdr”结构的作用相同。这个结构及相关常数被定义在” C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A\Include”里面:

//
// File header format.
//

typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine;
    WORD    NumberOfSections;
    DWORD   TimeDateStamp;
    DWORD   PointerToSymbolTable;
    DWORD   NumberOfSymbols;
    WORD    SizeOfOptionalHeader;
    WORD    Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

#define IMAGE_SIZEOF_FILE_HEADER             20

对照前面”SimpleSection.txt”中的输出信息,我们可以看到输出的信息里面最开始一段”FILE HEADER VALUES”中的内容跟COFF映像头中的成员是一一对应的,如下图所示:

可以看到这个目标文件的文件类型是”COFF OBJECT”,也就是COFF目标文件格式。文件头里面还包含了目标机器类型,例子里的类型是0x8664。按照微软的预想,PE/COFF结构的可执行文件应该可以在不同类型的硬件平台上使用,所以预留了该字段。在WinNT.h里面可以找到相应的以”IMAGE_FILE_MACHINE_”开头的目标机器类型的定义。文件头里面的”Number of Sections”是指该PE所包含的”段”的数量。”Time date stamp”是指PE文件的创建时间。“File pointer to symbol table”是符号表在PE中的位置。”Size of optional header”是指Optional Header的大小,这个结构只存在于PE可执行文件,COFF目标文件中该结构不存在,所以为0.

映像头后面紧跟着的就是COFF文件的段表,它是一个类型为”IMAGE_SECTION_HEADER”结构的数组,数组里面每个元素代表一个段,这个结构跟ELF文件中的”Elf64_Shdr”很相似。这个数组元素的个数刚好是该COFF文件所包含的段的数量,也就是映像头里面的”number of sections”。这个结构是用来描述每个段的属性的,它也被定义在WinNT.h里面:

//
// Section header format.
//

#define IMAGE_SIZEOF_SHORT_NAME              8

typedef struct _IMAGE_SECTION_HEADER {
    BYTE    Name[IMAGE_SIZEOF_SHORT_NAME];
    union {
            DWORD   PhysicalAddress;
            DWORD   VirtualSize;
    } Misc;
    DWORD   VirtualAddress;
    DWORD   SizeOfRawData;
    DWORD   PointerToRawData;
    DWORD   PointerToRelocations;
    DWORD   PointerToLinenumbers;
    WORD    NumberOfRelocations;
    WORD    NumberOfLinenumbers;
    DWORD   Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

#define IMAGE_SIZEOF_SECTION_HEADER          40

可以看到每个段所拥有的属性包括段名(Section Name)、物理地址(Physical address)、虚拟地址(Virtual address)、原始数据大小(Size of raw data)、段在文件中的位置(File pointer ro raw data)、该段的重定位表在文件中的位置(File pointer to relocation table)、该段的行号表在文件中的位置(File pointer to line numbers)、标志位(Characteristics)等。VirtualSize:该段被加载至内存后的大小;VirtualAddress:该段被加载至内存后的虚拟地址;SizeOfRawData:该段在文件中的大小。注意:这个值有可能跟VirtualSize的值不一样,比如.bss段的SizeOfRawData是0,而VirtualSize值是.bss段的大小。另外涉及一些内存对齐等问题,这个值往往比VirtualSize小。Characteristics:段的属性,属性里包含的主要是段的类型(代码、数据、bss)、对齐方式及可读可写可执行等权限。段的属性是一些标志位的组合,这些标志位被定义在WinNT.h里,比如IMAGE_SCN_CNF_CODE(0x00000020)表示该段里面包含的是代码,等等。

段表以后就是一个个的段的实际内容了。COFF中的代码段、数据段、BSS段的内容及它们的存储方式与ELF中几乎一样。

3. 链接指示信息

SimpleSection.txt中”.drectve”段相关的内容如下图所示:

“.drectve”段实际上是”Directive”的缩写,它的内容是编译器传递给链接器的指令(Directive),即编译器希望告诉链接器应该怎样链接这个目标文件。段名后面就是段的属性,包括地址、长度、位置等与ELF中存在的相同的属性,最后一个属性是标志位”flags”,即IMAGE_SECTION_HEADERS里面的Characteristics成员。”.drectve”段的标志位为”0x100A00”,它是下表中标志位的组合:

“dumpbin”已经为我们打印出了标志位的三个组合属性:Inof、Remove、1 byte align。即该段是信息段,并非程序数据;该段可以在最后链接成可执行文件的时候被抛弃;该段在文件中的对齐方式是1个字节对齐。

输出信息中紧随其后的是该段在文件中的原始数据(RAW DATA #1,用十六进制显示的原始数据及相应的ASCII字符)。”dumpbin”知道该段是个”.drectve”段,并且对段的内容进行了解析,解析结果为一个”/DEFAULTLIB:’LIBCMT’”的链接指令(Linker Directives),实际上它就是”cl.exe”编译器希望传给”link.exe”链接器的参数。这个参数表示编译器希望告诉链接器,该目标文件需要LIBCMT这个默认库。LIBCMT的全称是Library C Multithread,它表示VC的静态链接的多线程C库,对应的文件在VC安装目录下的lib/libcmt.lib。我们可以在cl.exe编译器里面加入/Zl来关闭默认C库的链接指令。

4. 调试信息

COFF文件中所有以”.debug”开始的段都包含着调试信息。比如”.debug$S”表示包含的是符号(Symbol)相关的调试信息段;”.debug$P”表示包含预编译头文件(Precompiled Header Files)相关的调试信息段;”.debug$T”表示包含类型(Type)相关的调试信息段。在”SimpleSection.obj”中,我们只看到了”.debug$S”段,也就是只有调试时的相关信息。我们可以从该段的文本信息中看到目标文件的原始路径,编译器信息等。调试信息段的具体格式被定义在PE格式文件标准中。调试段相关信息在”SimpleSection.txt”中的内容如下:

5. 大家都有符号表

 “SimpleSection.txt”的最后部分是COFF符号表(Symbol table),COFF文件的符号表包含的内容几乎跟ELF文件的符号表一样,主要就是符号名、符号的类型、所在的位置。”SimpleSection.txt”关于符号表的输出如下所示:

在输出结果的最左列是符号的编号,也就是符号在符号表中的下标。接着是符号的大小,即符号所表示的对象所占用的空间。第三列是符号所在的位置,ABS(Absolute)表示符号是个绝对值,即一个常量,它不存在于任何段中;SECT1(Section #1)表示符号所表示的对象定义在本COFF文件的第一个段中,即本例中的”.drectve”段;UNDEF(Undefined)表示符号是未定义的,即这个符号被定义在其它目标文件中。第四列是符号类型,可以看到对于C语言的符号,COFF只区分了两种,一种是变量和其它符号,类型为notype,另外一种是函数,类型为notype (),这个符号类型值可以用于其它一些需要强符号类型的语言或系统中,可以给链接器更多的信息来识别符号的类型。第五列是符号的可见范围,Static表示符号是局部变量,只有目标文件内部是可见的;External表示符号是全局变量,可以被其它目标文件引用。最后一列是符号名,对于不需要修饰的符号名,”dumpbin”直接输出原始的符号名;对于那些经过修饰的符号名,它会把修饰前和修饰后的名字都打印出来,后面括号里面的就是未修饰的符号名。

从符号表的dump输出信息中,我们可以看到”global_init_var”这个符号位于Section #3,即”.data”段。另外还有一个$SG1326的符号,其实它表示的是程序中的那个”%d\n”字符串常量。因为程序中要引用到这个字符串常量,而该字符串常量又没有名字,所以编译器自动为它生成了一个名字,并且作为符号放在符号表里面,可以看到这个符号对外都是不可见的。ELF文件中并没有为字符串常量自动生成的符号,另外所有的段名都是一个符号,”dumpbin”如果碰到某个符号是一个段的段名,那么它还会解析该符号所表示的段的基本属性,每个段名符号后面紧跟着一行就是段的基本属性,分别是段长度、重定位数、行号数和校验和。

 6. Windows下的ELF----PE

PE文件是基于COFF的扩展,它比COFF文件多了几个结构。最主要的变化有两个:第一个是文件最开始的部分不是COFF文件头,而是DOS MZ可执行文件格式的文件头和桩代码(DOS MZ File Header and Stub);第二个变化是原来的COFF文件头中的”IMAGE_FILE_HEADER”部分扩展成了PE文件文件头结构”IMAGE_NT_HEADERS”,这个结构包括了原来的”Image Header”及新增的PE扩展头部结构(PE Optional Header)。PE文件的结构如下图所示:

DOS下的可执行文件的扩展名与Windows下的可执行文件扩展名一样,都是”.exe”,但是DOS下的可执行文件格式是”MZ”格式,与Windows下的PE格式完全不同,虽然它们使用相同的扩展名。PE文件中的”Image DOS Header”和”DOS Stub”这两个结构就是为了兼容DOS系统而设计的,其中”IMAGE_DOS_HEADER”结构其实跟DOS的”MZ”可执行结构的头部完全一样,所以从某个角度看,PE文件其实也是一个”MZ”文件。”IMAGE_DOS_HEADER”的结构中有的前两个字节是”e_magic”结构,它是里面包含了”MZ”这两个字母的ASCII码;”e_cs”和”e_ip”两个成员指向程序的入口地址。

当PE可执行映像在DOS下被加载的时候,DOS系统检测该文件,发现最开始两个字节是”MZ”,于是认为它是一个”MZ”可执行文件。然后DOS系统就将PE文件当作正常的”MZ”文件开始执行。DOS系统会读取”e_cs”和”e_ip”这两个成员的值,以跳转到程序的入口地址。然而PE文件中,”e_cs”和”e_ip”这两个成员并不指向程序真正的入口地址,而是指向文件中的”DOS Stub”。”DOS Stub”是一段可以在DOS下运行的一小段代码,这段代码的唯一作用是向终端输出一行字:”This program cannot be run in DOS”,然后退出程序,表示该程序不能在DOS下运行。所以我们如果在DOS系统下运行Windows的程序就可以看到上面这句话,这是因为PE文件结构兼容DOS “MZ”可执行文件结构的缘故。

 “IMAGE_DOS_HEADER”结构也被定义在WinNT.h里面,里面的”e_lfanew”成员表明了PE文件头(IMAGE_NT_HEADERS)在PE文件中的偏移,我们需要使用这个值来定位PE文件头。这个成员在DOS的”MZ”文件格式中它的值永远为0,所以当Windows开始执行一个后缀名为”.exe”的文件时,它会判断”e_lfanew”成员是否为0。如果为0,则该”.exe”文件是一个DOS  “MZ”可执行文件,Windows会启动DOS子系统来执行它;如果不为0,那么它就是一个Windows的PE可执行文件,”e_lfanew”的值表示”IMAGE_NT_HEADERS”在文件中的偏移。

“IMAGE_NT_HEADERS”是PE真正的文件头,它包含了一个标记(Signature)和两个结构体。标记是一个常量。文件头包含的两个结构分别是映像头(Image Header)、PE扩展头部结构(Image Optional Header)。

Windows中把32位的PE文件格式叫做PE32,把64位的PE文件格式叫做PE32+。这两种格式就是ELF32和ELF64一样,都大同小异,只不过关于地址和长度的一些成员从32位扩展成了64位,还增加了若干个额外的成员之外,没有其它区别。

我们平时可以使用”IMAGE_OPTIONAL_HEADER”作为”Optional Image Header”的定义。它是一个宏,在64位的Windows下,Visual C++在编译时会定义”_WIN64”这个宏,那么”IMAGE_OPTIONAL_HEADER”就被定义成”IMAGE_OPTIONAL_HEADER64”;32位Windows下没有定义”_WIN64”这个宏,那么它就是”IMAGE_OPTIONAL_HEADER32”。

PE数据目录:在Windows系统装载PE可执行文件时,往往需要很快地找到一些装载所需要的数据结构,比如导入表、导出表、资源、重定位表等。这些常用的数据的位置和长度都被保存在了一个叫做数据目录(Data Directory)的结构里面,其实它就是”IMAGE_OPTIONAL_HEADER”结构里面的”DataDirectory”成员。这个成员是一个”IMAGE_DATA_DIRECTORY”的结构数组,相关的定义如下:

//
// Directory format.
//

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;
    DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES    16

可以看到这个数组的大小为16,IMAGE_DATA_DIRECTORY结构有两个成员,分别是虚拟地址以及长度。DataDirectory数组里面每一个元素都对应一个包含一定含义的表。”WinNT.h”里面定义了一些以”IMAGE_DIRECTORY_ENTRY_”开头的宏,数值从0到14,它们实际上就是相关的表的宏定义在数组中的下标。比如”IMAGE_DIRECTORY_ENTRY_EXPORT”被定义为0,所以这个数组的第一个元素所包含的地址和长度就是导出表(Export Table)所在的地址和长度。这个数组中还包含其它的表,不如导入表、资源表、异常表、重定位表、调试信息表、线程私有存储(TLS)等的地址和长度。这些表多数跟装载和DLL动态链接有关,与静态链接没什么关系。

GitHub: https://github.com/fengbingchun/Messy_Test 

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值