Windows下的可执行文件格式为PE(Portable Executable),与ELF格式同根同源,都是COFF(Common Object File Format)格式的一种扩展。所以它们之间基本结构相同,下文主要说明它两之间的差异。
我们可以用dumpbin查看obj文件的信息,输入/ALL
参数可以查看所有相关信息:
dumpbin /ALL SimpleSection.obj > SimpleSection.txt
PE的前身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 | typedef struct _IMAGE_FILE_HEADER { |
这些字段的含义看命名应该很容易理解,知道有这些东西就行,具体需要用到的时候再百度。
同时,段表对应的数据结构定义为_IMAGE_SECTION_HEADER
,也位于winnt.h
文件中。
1 | typedef struct _IMAGE_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 | COFF SYMBOL TABLE |
第三列的ABS表示对象所占空间,SECTx表示对象定义在第x个段,UNDEF表示符号在本文件中未定义,被定义在其他文件。
第四列的notype表示变量和其他符号类型,notype()则表示函数类型。
第五列的Static表示符号是局部变量,仅在本目标文件内可见,External则是全局可见的,可被其他目标文件引用。
Windows下的ELF——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 | typedef struct _IMAGE_DOS_HEADER { // DOS .EXE 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_cs
和e_ip
这两个了,这两个字段的值指向程序的入口地址。
在DOS系统下运行PE文件,会因为魔数而检测到PE文件是一个“MZ”格式的文件,然后开始读取e_cs
和e_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 | typedef struct _IMAGE_DATA_DIRECTORY { |
这个数据结构描述了PE文件中其他数据结构(如导入表、导出表、资源表、重定位表等)的虚拟地址和大小。
上一节最后的可选头文件中就包含了一个数据目录的数组,每一个元素对应一个表,以让系统很方便的找到所要用的表。