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 的字节顺序相同。
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 文件非常简单:
- 从标头中提取入口点,堆栈和堆栈大小。
- 遍历每个节并将其从文件复制到虚拟内存中(尽管不是必需的,但是最好把内存和文件中的section size的差异清0)。
- 通过查找符号表中的正确条目找到入口点的地址。
- 在该地址创建一个新线程并开始执行!
要加载需要动态 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
- PE Specification: latest edition, OOXML format, 1999 edition, DOC format
- Someone's Perspective On PE32 Vs ELF32. - http://forum.osdev.org/viewtopic.php?f=1&t=17686&p=133835#p133835
- http://www.ntcore.com/exsuite.php - Great tool for Windows that lets you explore inside of a PE file (and .Net metadata) to understand how it's laid out.
- 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.
欢迎加入“喵星计算机技术研究院”,原创技术文章第一时间推送。
- 作者:tangcuyu
- 链接:https://expoli.tech/articles/2022/12/10/wiki-osdev-org-pe
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
相关文章
2022-12-11
wiki.osdev.org 系列之 - Category:Bare Bones (类别:基本教程)
2022-12-11
wiki.osdev.org 系列之 - Linux Kernel Primer(Linux 内核入门)
2022-12-11
wiki.osdev.org 系列之 - OS theory
2022-12-11
wiki.osdev.org 系列之 - Symbol Table
2022-12-11
wiki.osdev.org 系列之 - System V ABI
2022-12-10
wiki.osdev.org 系列之 - Category:Object Files (类别:目标文件)