.exe文件里面有什么?

41

.exe文件是Windows操作系统可执行的文件,但它究竟包含了什么内容呢?是特定处理器的汇编语言吗?还是某种中间语句,被Windows识别后转换为特定处理器的汇编语言?当Windows“执行”该文件时,它具体做了什么呢?

5个回答

49

MSDN上有一篇文章 "深入探讨Win32可执行文件格式" ,其中描述了可执行文件的结构。

基本上,一个 .exe 文件包含多个数据块和有关这些块如何加载到内存中的指令。其中一些块包含可以执行的机器代码(其他块包含程序数据、资源、重定位信息、导入信息等)。

我建议您获取一份《Windows 内核原理》,以便全面了解运行 exe 时发生的情况。

对于本机可执行文件,其机器代码是特定于平台的。.exe 的头部指示了 .exe 所针对的平台。

当运行本机 .exe 时,会发生以下操作(大致说明):

  • 创建进程对象。
  • 将 exe 文件读取到该进程的内存中。.exe 的不同部分(代码、数据等)被单独映射并赋予不同的权限(代码是可执行的,数据是读/写的,常量是只读的)。
  • 在 .exe 中进行重定位(如果 .exe 没有加载到其首选地址,则地址会被修补)。
  • 遍历导入表并加载依赖的 DLL。
  • DLL 的映射方式与 .exe 类似,在其中进行重定位并加载其依赖的 DLL。从 DLL 中导入的函数得到解析。
  • 进程在 NTDLL 中的初始存根处开始执行。
  • 初始加载程序运行每个 DLL 的入口点,然后跳转到 .exe 的入口点。

托管可执行文件包含 MSIL (Microsoft Intermediate Language),可以编译它们以针对 CLR 支持的任何 CPU。我不太熟悉 CLR 加载器的内部工作方式(最初运行哪些本机代码来引导 CLR 并启动解释 MSIL),也许其他人可以详细说明。


17

6
可爱而有趣(我一定会关注那个链接),但与手头的问题关系不大。 - Twisol
是的,虽然这是一个有趣的片段,但它只能解释PE文件的前两个字节。从百分比上来说,这并不多。 - paxdiablo
只是出于好奇,他是为微软设计的吗? - MasterMastic

13

1和0!

这个维基百科链接会为你提供有关Windows应用程序使用的可移植可执行文件格式的所有信息。


我本来想给你的愚蠢回答点个踩,直到我看到"This"实际上是一个链接,而不是指代"1和0"。希望你不介意我编辑一下,让它更清晰明了。 - paxdiablo
7
在我年代,我们没有1,只能用0凑合。 - Martin Beckett

4

EXE文件实际上是一种名为可移植可执行文件(Portable Executable)的文件类型。它包含二进制数据,处理器可以读取并执行(基本上是x86指令)。还有很多头部数据和其他杂项内容。实际的可执行代码位于名为.text的部分中,并以机器指令(特定于处理器)的形式存储。这些代码(以及.EXE的其他部分)被放入内存中,并将CPU发送到它,从而开始执行。(请注意,实际上还发生了更多接口;这是一个简化的解释)。


-1
我只能回答关于传统DOS版本的.EXE的问题。Windows版本(便携式EXE)要复杂得多,需要更多的解释和理解。所以,我会把这个问题留给其他人来回答。
8086处理器,也就是DOS EXE所设计的处理器,将内存分为2¹⁶个段,每个段最多可以容纳2¹⁶字节,使用的是一个名义上为32位的寻址方式,其中地址的组成部分分别被称为“段”和“偏移”。具有段S和偏移O的地址会被写作S:O。
这种设计的初衷是希望一个源代码文件编译后生成一个单独的段,文件内部的访问只使用地址的“偏移”部分,因此完整的地址只在文件间访问全局对象时才需要使用。
地址通过映射到一个2²⁰的物理空间来展开,这样物理地址就是2⁴×段 + 偏移量;也就是说,段S+1的地址0与段S中地址2⁴上的“段落”边界重叠。在大多数情况下,展开的额外细节不需要知道,并且通常不会在程序中明确使用,但在EXE文件的布局中是隐含的。因此,EXE文件的格式就是在“展开后”阶段看到的程序图像。
这种8086架构基本保持不变,直到80186和80286。随着80386的出现,分段架构被重新适用和改造,尽管旧的安排在有限的上下文中保持不变。这也是从DOS到Windows EXE文件格式的转变发生的地方。
EXE文件包含了包含代码和初始化数据的段的实际二进制图像。图像本身从段0,地址0开始,并且段在文件中是展开的。
当程序加载到内存中时,"加载器"将在具有足够空闲空间的内存段中设置它的起始位置。由于8086的机器语言引用绝对地址,因此程序内部的绝对地址必须在EXE文件中列出,以便加载器可以进入程序镜像并进行适当的调整。
因此,如果一个程序要加载到段S₀,并且程序镜像中位置S₁:O₁处的引用指向段S₂(即如果存储在程序镜像中位置S₁:O₁处的2字节单词是S₂),那么它将调整其值为S₀+S₂。
这些项目被称为"重定位项",包含它们的表被称为"重定位表"。它们的存在和使用是EXE文件格式的主要要点。
调整完成后,它将根据EXE文件指定的位置设置初始堆栈。这包括堆栈段和堆栈大小。在8086上,堆栈指针通过"push"向下移动,通过"pop"向上移动,因此堆栈指针的初始值位于堆栈段的末尾之上。因此,EXE文件包括SS:SP(堆栈段+堆栈指针)的初始值。
然后它将跳转到由EXE文件指定的入口点。因此,EXE文件还包括CS:IP(代码段+指令指针)的初始值。
它还包括程序数据的最小和最大大小请求,尽管我不确定加载器如何处理这些信息,除了确定是否可以加载程序。同样,它还具有一个校验和,我认为甚至没有被遵守,以及一个“覆盖”号码,用于区分主程序和子程序(可能是Windows DLL的前身)。
EXE文件的强制部分由14个2字节的单词组成。请注意,8086中的所有2字节单词占用2个连续的8位内存地址,先是低8位,然后是高8位。
在下面的描述中,我将使用十六进制数来表示一切,因为这与需要描述的内容最直接相关。
因此,按照它们的地址标记强制性单词为W00、W02、W04、W06、W08、W0a、W0c、W0e、W10、W12、W14、W16、W18、W1a,刚才描述的信息布局如下:
(1)入口点 - CS:IP = W16:W14,
(2)堆栈段 - SS:SP = W0e:W10,
(3)可重定位表位置为W18(在可移植EXE文件格式规范中为RB),重定位数量为W06,
(4)代码镜像在文件中的起始点为:2⁴×W08,
(5)覆盖编号为W1a,对于“主”程序来说是0 - 这是我见过的唯一用例,
(6)校验和为W12,
(7)程序数据请求范围为2⁴×W0a到2⁴×W0c。我见过的唯一用例是将W0c设置为最大值ffff。
(8)程序文件大小为2⁹(W04 - 1)+ W02,如果W02 > 0;否则为2⁹ W04,如果W02 = 0。
(9)W00 = 5a4d 是文件类型的“签名”,按地址递增顺序列出了4d和5a,它们(在ASCII中)是程序员Mark Zbikowski的字母'M'和'Z',他可能是微软早期阶段所有琐碎工作的技术专家,当我看着他们在20世纪70年代车库里进行的对话和争论时,我可以感受到和听到回声。这些是他们一些旧程序的二进制代码。
重定位表的每个项目都包含要重定位的项目的S:O地址,其中O排在前面,然后是S。因此,重定位表的大小(以字节为单位)等于其条目数的四倍。

在一个实际程序的示例中,002b个条目(或00ac字节)的重定位表位于EXE文件的001e-00ca位置。程序图像的大小为548e字节,段被展开并放置在0200-548d文件中。堆栈应初始化为SS:SP = 06c1:0800,并且入口点应位于CS:IP = 0000:05d0位置。数据请求范围为2190-ffff0字节,校验和为e3d8。

因此,强制的16位字的布局如下:

W00 = 5a4d

(W02, W04) = (008e, 002b)

W06 = 002b

W08 = 0020

(W0a, W0c) = (0219, ffff)

(W0e, W10) = (06c1, 0800)

W12 = e3d8

(W14, W16) = (05d0, 0000)

W18 = 001e

W1a = 0000

作为一个字节序列,它的读取结果是: 4d, 5a, 8e, 00, 2b, 00, 2b, 00, 20, 00, 19, 02, ff, ff, c1, 06, 00, 08, d8, e3, d0, 05, 00, 00, 1e, 00, 00, 00
文件位置001c-001d存在覆盖空洞,因为重定位从文件位置001e开始。重定位表位于文件位置001e-00c9,并以以下字节序列形式呈现:
22, 00, 00, 00, 2e, 00, 00, 00, ... 63, 39, 35, 01
其中包含地址0000:0022, 0000:002e, ..., 0135:3963对应的单词0022, 0000, 002e, 0000, ..., 3963, 0135。

在重定位中列出了几个部分,包括 0000、0135、04d0、04fe、0500,这是一种获取代码镜像如何分割成片段的方法之一 - EXE 文件没有明确列出各个片段,因为它们已经被扁平化。

从文件位置 00ca 到 01ff 还有另一个覆盖范围缺失,代码镜像从文件位置 0200 开始,直到文件末尾的 548d。

刚才提到的实际片段在文件中映射如下:

0000 片段在 0200-154f

0135 片段在 1550-4eff

04d0 片段在 4f00-51df

04fe 片段在 51e0-51ff

0500 片段在 5200-548d

假设这些是程序中唯一的片段。当程序叠加到内存中时,在重新定位完成后,它将放置在物理内存中第一个可用地址的 0000-528d 区间。

所以,例如,如果它被加载器重新定位到段落077a(我的DosBox版本是如此),那么加载器首先必须进行调整。例如,代码图像中列出的单词在0000:0022处(并且在文件中位于0222-0223)包含了0135这个词(表示段落0135),必须将其增加到0135+077a=08af。段落本身也会在重定位过程中相应地增加,从0000、0135、04d0、04fe、0500(堆栈为06c1)分别增加到077a、08af、0c4a、0c78、0c7a(堆栈为0e3b)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接