type
Post
status
Published
slug
2022/12/10/wiki-osdev-org-pe
summary
tags
osdev
category
osdev
icon
password
new update day
Property
Oct 22, 2023 01:31 PM
created days
Last edited time
Oct 22, 2023 01:31 PM
对于 Windows 95/NT,需要一种新的可执行文件类型。于是诞生了“PE”便携式可执行文件,至今仍在使用。与其前身不同,WIN-PE 是真正的 32 位文件格式,支持可重定位代码。它明确区分了 TEXT、DATA 和 BSS。实际上,它是 COFF 格式的一个劣化版本。
如果您确实在 Windows 机器上设置了 Cygwin 环境,那么 “PE” 是 Cygwin GCC 工具链的目标格式,这使得未经意的人在试图将Cygwin下的部分与Linux或BSD下的部分连接时会遇到一些麻烦(默认使用ELF目标)。 (提示:你必须构建一个 GCC 交叉编译器)
PE 格式由 Windows 95 及更高版本、Windows NT 3.1 及更高版本、Mobius、ReactOS 和 UEFI 使用。PE 格式还被用作 Microsoft .Net CLI 和 Mono 的 .NET 程序集的容器,在这种情况下它实际上不存储可执行数据,而是存储附加了 IL 的 .Net 元数据。

1 Inside the PE file

下面将尝试解释构成 PE 文件的各种概念和部分,而不是它们内部的确切数据结构,因为这样肯定会占用太多空间。其原因在于大多数关于 PE 文件的资源往往只是将一堆数据结构摆在您面前,而没有完全解释它们的用途。因此,通过阅读以下内容并了解 PE 文件的组成,您将会更好地理解可以预期的内容和如何使用 PE 文件资源。

1.1 Overview

PE 文件由几个部分组成。它们在下面简要概括,然后更详细地介绍每个部分。以下是字段类型的定义。 PE 文件以小端顺序存储,与 x86 的字节顺序相同。
An overview of the format
An overview of the format

1.2 DOS Stub

PE文件以MS-DOS存根(头部加可执行代码)开头,使其成为有效的MS-DOS可执行文件。MS-DOS头部以0x5A4D的魔数开头,长度为64字节,后面是实模式可执行代码。几乎所有情况下使用的标准存根长度为128字节(包括头部和可执行代码),只是输出“该程序无法在DOS模式下运行。”尽管许多处理PE文件的实用程序都硬编码为期望PE头部从第128个字节开始,但这是不正确的,因为在一些连接器(包括Microsoft自己的Link)中,可以用自己选择的MS-DOS存根替换原有存根,许多旧程序都这样做,允许开发人员将MS-DOS和Windows版本结合在一个文件中。正确的方法是读取MS-DOS头部内部的一个保留的4字节地址,位置在0x3C(通常称为 e_lfanew 的字段),其中包含PE文件签名所在的地址,PE文件头部随后立即出现。通常这是一个相当标准的值(大多数情况下,该字段被默认的link.exe存根设置为0xE8)。微软似乎建议将PE头部对齐到8字节边界(https://web.archive.org/web/20160609191558/http://msdn.microsoft.com/en-us/gg463119.aspx,第10页,图1)。

1.3 PE header

PE头部包含与整个文件相关的信息,而不是单独的后面将要提到的信息。最小的头部包含一个4字节的签名(0x00004550),PE文件中的可执行代码的机器类型/架构,时间戳,符号指针,以及各种标志(文件是可执行文件、DLL、应用程序能否处理2GB以上的地址、文件是否需要在从可移动设备运行时复制到交换文件等)。除非您使用一个非常精简的静态链接的PE文件来节省内存,并用硬编码的入口点和没有资源,否则仅有PE头部是不够的。
// 1 byte aligned struct PeHeader { uint32_t mMagic; // PE\0\0 or 0x00004550 uint16_t mMachine; uint16_t mNumberOfSections; uint32_t mTimeDateStamp; uint32_t mPointerToSymbolTable; uint32_t mNumberOfSymbols; uint16_t mSizeOfOptionalHeader; uint16_t mCharacteristics; };

1.3.1 Optional header(可选头)

可选的 PE 头部直接跟在标准 PE 头部之后。它的大小在 PE 头部中指定,您也可以使用它来检查可选头部是否存在。可选的 P E头部以 2 字节的魔数开头,表示架构(PE32为0x010B,PE64为0x020B,ROM为0x0107)。可以与 PE 头部中的机器类型一起使用,以检测 PE 文件是否在兼容的系统上运行。还有一些其他有用的内存相关变量,包括代码和数据的大小和虚拟基地址,以及应用程序的版本号(完全由用户指定,一些更新工具使用它来检测是否有新版本)、入口点和目录数量(见下文)。
部分可选的头部信息是 NT 特定的。这包括子系统(控制台、驱动程序或图形用户界面应用程序),以及需要预留的堆栈和堆空间,以及最低需要的操作系统、子系统和 Windows 版本。您可以根据操作系统的需求自己设定这些值。
// 1 byte aligned struct Pe32OptionalHeader { uint16_t mMagic; // 0x010b - PE32, 0x020b - PE32+ (64 bit) uint8_t mMajorLinkerVersion; uint8_t mMinorLinkerVersion; uint32_t mSizeOfCode; uint32_t mSizeOfInitializedData; uint32_t mSizeOfUninitializedData; uint32_t mAddressOfEntryPoint; uint32_t mBaseOfCode; uint32_t mBaseOfData; uint32_t mImageBase; uint32_t mSectionAlignment; uint32_t mFileAlignment; uint16_t mMajorOperatingSystemVersion; uint16_t mMinorOperatingSystemVersion; uint16_t mMajorImageVersion; uint16_t mMinorImageVersion; uint16_t mMajorSubsystemVersion; uint16_t mMinorSubsystemVersion; uint32_t mWin32VersionValue; uint32_t mSizeOfImage; uint32_t mSizeOfHeaders; uint32_t mCheckSum; uint16_t mSubsystem; uint16_t mDllCharacteristics; uint32_t mSizeOfStackReserve; uint32_t mSizeOfStackCommit; uint32_t mSizeOfHeapReserve; uint32_t mSizeOfHeapCommit; uint32_t mLoaderFlags; uint32_t mNumberOfRvaAndSizes; };

1.3.2 Data Directories

尽管技术上属于可选头部,但在其之后是一个指向数据目录的条目列表(仅在可执行映像和DLL中)。由于可选头部的大小可能会变化,因此您只需关注存在并期望的目录,因为PE规范可能会在未来添加新的数据目录(.Net就是一个最近添加的例子)。每个数据目录都被引用为可选头部的8字节条目。前4个字节是目录的相对虚拟地址(RVA),最后4个字节是目录的大小。
条目所指向的每个数据目录都有自己的格式。数据目录用于描述动态链接的导入表、嵌入在PE文件中的资源表、调试信息(行号和断点)以及CLI .Net头。
Position (PE/PE32+)
Section
96/112
The export table address and size. Same format as .edata
104/120
The import table address and size. Same format as .idata
112/128
The resource table address and size. Same format as .rsc
120/136
The exception table address and size. Same format as .pdata
128/144
The attribute certificate table offset (not RVA) and size. See Signed PE below
136/152
The base relocation table address and size. Same format as .reloc
144/160
The debug data starting address and size. Same format as .debug
152/168
Architecture, reserved MBZ
160/176
Global Ptr, the RVA of the value to be stored in the global pointer register. The size member of this structure must be set to zero.
168/184
The thread local storage (TLS) table address and size. Same format as .tls

1.4 Sections

一个PE文件由节组成,包括名称、文件内偏移量、复制到的虚拟地址以及文件中节的大小和虚拟内存中的大小(这两者可能不同,这种情况下应该差异用 0 填充)以及相关的标志。节通常遵循通用命名(".text"、".rsrc"等),但这也可能因链接器而异,在某些情况下可能是用户定义的,因此最好依靠标志来判断节是否可执行或可写。但是话虽如此,如果您有自定义数据,希望将其嵌入可执行文件中,那么将其放置在一个节中并通过节的名称标识可以是一个好主意,因为您不会更改PE格式,您的可执行文件仍然与PE工具兼容。
相对虚拟基是PE文档中经常出现的一个概念。RVA是文件加载到内存后存在的地址,而不是文件的偏移量。要在不实际加载节的情况下计算文件地址的RVA,可以使用节条目表。通过使用每个节的虚拟地址和大小,可以找到RVA所属的节,然后减去节的虚拟地址和文件偏移量之间的差值。

1.4.1 Section header(节表头)

每个节都有一个节表头
struct IMAGE_SECTION_HEADER { // size 40 bytes char[8] mName; uint32_t mVirtualSize; uint32_t mVirtualAddress; uint32_t mSizeOfRawData; uint32_t mPointerToRawData; uint32_t mPointerToRelocations; uint32_t mPointerToLinenumbers; uint16_t mNumberOfRelocations; uint16_t mNumberOfLinenumbers; uint32_t mCharacteristics; };

1.4.2 In asm linkage

segment .code aAsmFunction: ;Do whatever mov BYTE[aData], 0 ret segment .data aData: db 0xFF
段将作为节出现。使用这个,可以将C和Asm分开,因为链接器不会自动合并.code和.text,这是C编译器的正常输出。

1.4.3 Position Independent Code

如果每个节都指定加载它的虚拟地址,那么您可能想知道如何在一个虚拟地址空间中存在多个DLL而不冲突。的确,您在PE文件中找到的大多数代码(DLL或其他)都是依赖于位置并与特定地址绑定。但是为了解决这个问题,存在一个称为重定位表的结构,它附加到每个节条目。该表基本上是一个巨大的长列表,包含该节中存储的每个地址,以便您可以将其偏移到加载该节的位置。
由于地址可以跨节边界,因此应在将每个节加载到内存后执行重定位。然后重复每个节,在重定位表中迭代每个地址,找出该RVA所在的节,并添加/减去该节的链接虚拟地址与加载该节的虚拟地址之间的偏移量。

1.5 Signed PE with Attribute Certificate Table

许多PE可执行文件(尤其是所有微软更新)都用证书进行签名。这些信息存储在由数据目录的第5个条目指向的属性证书表中。重要的是,对于属性证书表,不存储RVA,而是简单的文件偏移。格式是串连的签名,每个签名都具有以下结构:
Offset
Size
Field
Description
0
4
dwLength
Specifies the length of the attribute certificate entry.
4
2
wRevision
Contains the certificate version number, magic 0x0200 (WIN_CERT_REVISION_2_0)
6
2
wCertificateType
Specifies the type of content in bCertificate, magic 0x0002 (WIN_CERT_TYPE_PKCS_SIGNED_DATA)
8
x
bCertificate
Contains a PKCS#7 SignedData structure
对于 EFI 下的 Secure Boot,这样的签名是必须的。值得注意的是 PE 格式允许在单个 PE 文件中嵌入多个证书,但是 UEFI 固件实现通常只允许一个证书,这个证书必须由 Microsoft KEK 签名。如果固件允许安装更多的 KEK(不典型),那么您也可以使用其他证书。
bCertificate 数据是带有证书的 PKCS#7 签名,编码为 ASN.1 格式。Microsoft 使用 signtool.exe 来创建这些签名条目,但是存在一个开源解决方案,叫做 sbsigntool(也可以在 github 上找到由 debian 打包的版本)。

1.6 CLI / .Net

CLI 与 PE 格式一起工作。它不是 PE 格式的扩展,而是作为自己的格式存在于具有完全不同的存储表和值的格式中。所有 .Net 数据和标头都存在于被加载到内存中的节中(它们被加载到内存中,因为 CLI 涉及大量语言反射,需要元数据而不会磨损磁盘)。 .Net 元数据存在于节内而不是 PE 标头内的第二个原因是, PE 加载器实际上根本没有 .Net 的概念。(例外:有一个数据目录项指向 CLI 头的 RVA,因此工具可以轻松访问 .Net 数据而不必将其加载到虚拟内存中。)事实上,我非常怀疑 Windows 内核是否有任何关于 .Net 的概念。.Net 的工作方式是通过与 .Net 运行时(mscoree.dll)动态链接,并将入口点设置为指向 mscoree.dll 中位置的符号(_CorExeMain)而不是本地可执行文件。这意味着 Windows CE,WINE 和 ReactOS 都可以加载 .Net 程序集,只要可以安装 .Net 框架,而不需要任何特定的代码。

2 Loading a PE file(加载 PE 文件)

加载 PE 文件非常简单:
  1. 从标头中提取入口点,堆栈和堆栈大小。
  1. 遍历每个节并将其从文件复制到虚拟内存中(尽管不是必需的,但是最好把内存和文件中的section size的差异清0)。
  1. 通过查找符号表中的正确条目找到入口点的地址。
  1. 在该地址创建一个新线程并开始执行!
要加载需要动态 DLL 的 PE 文件,您可以采用同样的方法,但是检查导入表(由数据目录引用)以查找所需的符号和 PE 文件,导出表(也由数据目录引用) ) 在该 PE 文件中查看这些符号的位置,并在将该 PE 的部分加载到内存中(并重新定位它们!)后将它们匹配起来。而且注意您还必须递归解析每个 DLL 的导入表,并且某些 DLL 可以使用技巧来引用正在加载它的 DLL 中的符号,因此请确保您的加载器不会陷入死循环!注册已加载的符号并使它们全局化可能是一个很好的解决方案。
检查 Machine 和 Magic 字段的有效性,而不仅仅是 PE 签名,也可能是个好主意。这样,您的加载器就不会尝试将 64 位二进制文件加载到 32 位模式中(这肯定会导致异常)。

3 64 bit PE

64 位 PE 与正常 PE 非常相似,但是机器类型(如果是 AMD64)是 0x8664,而不是 0x14c。该字段直接在 PE 签名之后。魔术数也从 0x10b 变为 0x20b。魔法字段位于可选头的开头。此外,可选头的 BaseOfData 成员不存在。这是因为 ImageBase 成员被扩展为 64 位。BaseOfData 被删除,为其腾出空间。

4 See Also

  • pefile - handy python module for inspecting & manipulating PE files
  • php-winpefile - Powerful PHP command-line tool and several PHP classes for inspecting, manipulating, and even creating PE files and finding PE file artifacts.
  • Windows PE Artifact Library - Over 375 carefully curated PE file artifacts that extensively cover the PE32 and PE32+ formats (plus some DOS and Win16 NE) for multiple architectures. Useful for seeing examples of what a bound imports table looks like, if a specific flag is ever used, or pretty much any option of the PE format.
 
欢迎加入喵星计算机技术研究院,原创技术文章第一时间推送。
notion image
 
wiki.osdev.org 系列之 - Object FileseBPF 的基本概念