0%

《程序员的自我修养》 Ch5 Windows PE/COFF

Windows下的可执行文件格式为PE(Portable Executable),与ELF格式同根同源,都是COFF(Common Object File Format)格式的一种扩展。所以它们之间基本结构相同,下文主要说明它两之间的差异。

我们可以用dumpbin查看obj文件的信息,输入/ALL参数可以查看所有相关信息:

dumpbin /ALL SimpleSection.obj > SimpleSection.txt

PE的前身COFF文件

5.2-COFF目标文件格式

图5-1 COFF目标文件格式

COFF与之前介绍的ELF文件的一个主要不同就是,COFF的头文件包括两部分,一个是映像头(Image Header),类比ELF中的文件头,描述文件的总体结构和属性;第二个是段表(Section Table),而在ELF中,段表不是紧挨着文件头的。这个结构是一个数组结构,数组大小等于段的数量。

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

COFF文件的映像头对应的数据结构是_IMAGE_FILE_HEADER,书上说定义在VC\PlatformSDK\include\WinNT.h中,不过这应该是在作者那个年代的VC版本,我在自己电脑上的Visual Studio 2019 community的目录找了一下,并没有找到。该文件在win10下的目录应该是C:\Program Files (x86)\Windows Kits\10\Include\10.0.18362.0\um\(版本号可能有所不同),前提是你安装了Windows SDK,文件名为winnt.h,数据结构定义如下:

1
2
3
4
5
6
7
8
9
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;

这些字段的含义看命名应该很容易理解,知道有这些东西就行,具体需要用到的时候再百度。

同时,段表对应的数据结构定义为_IMAGE_SECTION_HEADER,也位于winnt.h文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;

字段含义就不说了,理由同上,不过有一个点需要说一下,就是SizeOfRawData字段指的是该段在文件中的大小,而VirtualSize则是该段被加载到内存后的实际大小,这两个大小可能会不一样,且往往是前者比后者小。例如,bss段的SizeOfRawData为0,而VirtualSize则为bss段所含数据的大小。

链接指示信息

COFF中.drectve段的内容是编译器传递给链接器的指令(Directive),即编译器希望告诉连接器该怎样链接这个目标文件。

调试信息

COFF中所有以.debug开头的段都包含着调试信息,例如.debug$S包含的是符号(Symbol)相关的调试信息;.debug$P包含的是预编译头文件(Precompile)相关的调试信息;.debug$T包含的是类型(Type)相关的调试信息。

符号表

dumpbin输出的最后一部分是COFF的符号表,总共有6列,从左到右依次是:符号编号、符号所表示的对象所占空间、符号所在位置、符号类型、符号可见范围、符号名(修饰后和修饰前)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
COFF SYMBOL TABLE
000 010471B7 ABS notype Static | @comp.id
001 80000191 ABS notype Static | @feat.00
002 00000000 SECT1 notype Static | .drectve
Section length 18, #relocs 0, #linenums 0, checksum 0
004 00000000 SECT2 notype Static | .debug$S
Section length B8, #relocs 0, #linenums 0, checksum 0
006 00000000 SECT3 notype Static | .data
Section length C, #relocs 0, #linenums 0, checksum AC5AB941
008 00000000 SECT3 notype External | _global_init_var
009 00000004 UNDEF notype External | _global_uninit_var
00A 00000000 SECT4 notype Static | .text$mn
Section length 4E, #relocs 5, #linenums 0, checksum CC61DB94
00C 00000000 UNDEF notype () External | _printf
00D 00000000 SECT4 notype () External | _func1
00E 00000020 SECT4 notype () External | _main
00F 00000004 SECT3 notype Static | $SG6034
010 00000008 SECT3 notype Static | ?static_var@?1??main@@9@9 (`main'::`2'::static_var)
011 00000000 SECT5 notype Static | .bss
Section length 4, #relocs 0, #linenums 0, checksum 0
013 00000000 SECT5 notype Static | ?static_var2@?1??main@@9@9 (`main'::`2'::static_var2)
014 00000000 SECT6 notype Static | .chks64
Section length 30, #relocs 0, #linenums 0, checksum 0

第三列的ABS表示对象所占空间,SECTx表示对象定义在第x个段,UNDEF表示符号在本文件中未定义,被定义在其他文件。

第四列的notype表示变量和其他符号类型,notype()则表示函数类型。

第五列的Static表示符号是局部变量,仅在本目标文件内可见,External则是全局可见的,可被其他目标文件引用。

Windows下的ELF——PE

5.6-PE文件格式

图5-2 PE文件格式

PE文件在COFF的格式基础上,又有一些变化:

首先文件开头添加了DOS MZ可执行文件格式的文件头和桩代码(DOS MZ File Header and Stub);第二,COFF的IMAEG_FILE_HEADER扩展为了IMAGE_NT_HEADER,其中添加了IMAGE_OPTIONAL_HEADER32部分。

之所以要添加DOS MZ相关的头文件和桩代码,是为了使为Windows编写的程序能“兼容”原有的DOS系统,以至于在DOS系统下不会被执行而导致错误。

其中IMAGE_DOS_HEADER的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
WORD e_magic; // Magic number
WORD e_cblp; // Bytes on last page of file
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
WORD e_minalloc; // Minimum extra paragraphs needed
WORD e_maxalloc; // Maximum extra paragraphs needed
WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // Checksum
WORD e_ip; // Initial IP value
WORD e_cs; // Initial (relative) CS value
WORD e_lfarlc; // File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words
WORD e_oemid; // OEM identifier (for e_oeminfo)
WORD e_oeminfo; // OEM information; e_oemid specific
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

其中e_magic这个字段是“魔数”,简单来说就是用来区分不同文件类型的,详细可以看:https://zh.wikipedia.org/wiki/%E9%AD%94%E8%A1%93%E6%95%B8%E5%AD%97_(%E7%A8%8B%E5%BC%8F%E8%A8%AD%E8%A8%88),PE的该字段包括“MZ”这两个字母的ASCII码。

其他字段中最重要的就是e_cse_ip这两个了,这两个字段的值指向程序的入口地址。

在DOS系统下运行PE文件,会因为魔数而检测到PE文件是一个“MZ”格式的文件,然后开始读取e_cse_ip的值准备执行,但其本质还是PE文件,且PE文件中这两个字段指向的并不是真正的程序入口地址,而是指向DOS Stub的起始位置,DOS Stub是可以在DOS下运行的一小段代码,这段代码唯一的作用是向终端输出一行字符串:“This program cannot be run in DOS”,然后就退出程序,这样就防止了在DOS下执行PE可能导致的错误。

IMAGE_OPTIONAL_HEADER32字面上是可选的,但实际上对于PE来说,是必须的,因为其中包含了很多对于PE执行来说重要的信息,比如DataDirectory。但是我们不必熟知,用到的时候再查即可,具体结构就不展示了。

PE 数据目录

数据目录是一个数据结构——_IMAGE_DATA_DIRECTORY,定义为:

1
2
3
4
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

这个数据结构描述了PE文件中其他数据结构(如导入表、导出表、资源表、重定位表等)的虚拟地址和大小。

上一节最后的可选头文件中就包含了一个数据目录的数组,每一个元素对应一个表,以让系统很方便的找到所要用的表。