深入剖析Win32PE结构文件格式

【菜科解读】
很久以前,我开始为Microsoft Systems Journal(现在的MSDN(R) Magazine)写文章,其中 有一篇名为“探索PE文件内幕——Win32可移植可执行文件格式之旅”的文章很受欢迎,大大超 出了我的意料。
直到现在,我还听说有人(甚至在Microsoft)仍然在使用那篇文章,它依旧被 收录在MSDN Library中。
不幸的是,文章的最大问题是它们是静止的。
但是Win32(R)的世界在这些 年已经发生了很大的变化,因此那篇文章已经严重过时了。
我要从本月开始用两部分系列的文章 来补救这种情况。
你可能想知道为什么要关注可执行文件的格式。
答案永远是:操作系统的可执行文件格式和 数据结构展现了操作系统内部许多信息。
通过理解 EXE和 DLL 的内部情况,你会发现你已经变成 你周围一个更优秀的程序员。
当然,通过阅读 Microsoft的 PECOFF 规范你可以获得许多我将要告诉你的内容。
但是与大 多数规范一样,它更注重完整性而不是可读性。
在本文中,我把精力集中于解释整个故事中最重 要的部分,同时填补那些并不适合出现在官方规范中的怎么样(How)以及为什么(Why)的问题。
另外,在本文中我还会讲到一些非常有用的内容,它们并未出现在任何 Microsoft官方文档中。
让我先举一些例子来说明自从1994年我写那篇文章以来有关可执行文件方面都发生了哪些 变化。
由于16位 Windows(R)已经成为历史,因此没有必要再与Win16的 NE(New Executable)格 式相比较了。
另一个已经脱离人们视野的是 Win32s(R)。
在 Windows 3.1 上运行Win32 程序非常不 稳定是最令人讨厌的事。
回到当时,Windows 95(当时代号为“Chicago”)甚至还未发行。
Windows NT(R)还是3.5 版。
Microsoft 链接器还未进行非常有效地优化。
值得一提的是当时已经在MIPS和 DEC Alpha 上实现了 Windows NT。
自从那篇文章以来都出现了什么新内容呢?64位Windows引进了它自己的变种的PE文件格 式。
Windows CE 添加了许多的新型处理器。
诸如 DLL延迟加载、节合并以及绑定之类的优化已 经铺天盖地。
有许多新东西要加入到这个故事中。
让我们不要忘了 Microsoft(R) .NET。
该把它放在什么位置呢?对于操作系统来说,.NET 可 执行文件只不过是普通的Win32 可执行文件。
但是.NET 运行时能够识别出这些可执行文件中的 数据并把它作为元数据(metadata)和中间语言(Intermediate Language,IL),它们对.NET 来说非常重要。
在本文中,我要敲开.NET 元数据结构的大门,但把对它全部光彩的彻底挖掘留 给下一篇文章。
如果Win32 世界中的所有这些加加减减还不足以成为我重新写那篇文章的理由的话,那么 我只有列出原来那篇文章中的一些令我害怕的错误了。
例如我对线程局部存储(TLS)支持情况 的描述是错误的。
同样,通篇我对日期/时间戳这个DWORD 的描述仅在太平洋时区才是精确的! 另外,有许多内容在当时是正确的,但现在已经不正确了。
我说过.rdata 节并没有太大的 作用。
今天,诚然是这样。
我也说过.idata 节是可读/可写的节,但现在却有许多试图拦截 API 的人发现它在很多情况下都是不正确的。
伴随着在这篇文章中完全更新 PE文件格式的故事,我也对用于显示 PE文件内容的PEDUMP 程序进行了彻底修改。
PEDUMP 现在可以在 x86和 IA-64 平台上编译和运行,并且能够转储32 位 和 64位PE 文件。
最重要的是,PEDUMP的源代码可以从本文开头的链接处下载。
这样,你就有 了一个用这里讲的概念和数据结构实际工作的例子。
PE 文件格式概览 Microsoft引进了PE 文件格式,更经常被称为PE 格式,作为最初的 Win32规范的一部分。
然而PE 文件源自VAX/VMS上早期的通用目标文件格式(Common Object File Format,COFF)。
这是由于许多最初的Windows NT 开发团队的成员都来自数字设备公司(Digital Equipment Corporation,DEC)。
这些开发者很自然就使用现有的代码以便快速开始新的Windows NT 平台。
之所以选择术语“可移植可执行”是打算要在所有支持的 CPU 上的所有版本的 Windows 上 使用相同的可执行文件格式。
从大的方面来说,这个目标已经实现,因为Windows NT及其后继 操作系统、Windows 95 及其后继操作系统以及Windows CE 都使用相同的可执行文件格式。
Microsoft编译器生成的OBJ 文件也使用 COFF 格式。
从COFF 格式的一些域使用的竟然是 八进制编码你就能知道它是多么老。
COFF格式的OBJ 文件中有许多数据结构和枚举类型与PE 文 件相同,后面我会提到。
64 位Windows 需要做的只是修改PE 格式的少数几个域。
这种新的格式被称为PE32+。
它并 没有增加任何新域,仅从PE 格式中删除了一个域。
其余的改变就是简单地把某些域从32 位扩展 到 64位。
在大部分情况下,你都能写出同时适用于32 位和64 位PE 文件的代码。
Windows 头文 件有这种魔力可以使这些区别对于大多数基于C++的代码都不可见。
EXE文件与 DLL 文件的区别完全是语义上的。
它们使用的是相同的PE 格式。
惟一的不同在 于一个位,这个位用来指示文件应该作为 EXE 还是DLL。
甚至DLL文件的扩展名也完全也是人为 的。
你可以给 DLL 一个完全不同的扩展名,例如.OCX 控件和控制面板小程序(.CPL)都是DLL。
PE 文件一个非常好的地方就是它的数据结构在磁盘上与在内存中一样。
加载一个可执行文 件到内存(例如通过调用LoadLibrary函数)主要就是把PE 文件中的某个部分映射到地址空间 中。
因此像IMAGE_NT_HEADERS(后面我会讲到)这样的数据结构在磁盘上和在内存中是一样的。
如果你知道如何在一个 PE文件中找到某些内容,你几乎可以确定当文件被加载进内存时可以找 到同样的信息。
注意到PE 文件并不是作为单一的内存映射文件被映射进内存的这一点非常重要。
相反, Windows 加载器查看PE 文件并确定文件中的哪些部分需要被映射。
当映射进内存时,文件中的 高偏移相对于内存中的高地址。
某项内容在磁盘文件中的偏移可能与它被加载进内存之后的偏移 不同,但是将磁盘文件中的偏移转换成内存偏移需要的所有信息都存在
深入Lazy<T>——.NETFramework4.0
我没有看过在.net framework 4.0 beta2 的 关于 Lazy 的实现,也不知道正式版与之前的版本是否有过改进(改变),我只在这里来单纯地谈在.NET Framework 4 中 Lazy 的实现。
1. Lazy 概述我们也许会遇到这样一种情况,我们有一个大家伙(大对象)需要创建,那么这个对象的创建时需要较长的时间,同时也需要在托管堆上分配较多的空间。
那么在.NET Framework 4 中提供了这样一个很聪明的方式:Lazy(我们可以称之为懒对象)。
当然,在之前,很多人也曾对其进行过自己的实现。
那么我们在这里就可以把 Lazy 的作用总结为一句话,按需延迟加载。
2. Lazy 的使用了解了Lazy的作用,让我们就来看下Lazy如何应用。
class Program{static void Main(string[] args){Lazy lazyObject = new Lazy();Console.WriteLine(lazyObject.IsValueCreated);lazyObject.Value.Test();Console.WriteLine(lazyObject.IsValueCreated);}}[Serializable]class Large{public Large() { }public void Test() {Console.WriteLine("Test");}}运行结果如下:这个例子很简单,也是Lazy最基本,也是最常用的应用方式。
3. 实现自己的Lazy在.NET Framework 4.0之前,大对象就是存在的,那么对于一个大型系统而言,怎么样对付一个大对象呢。
在我看来有两点:延迟加载和即时清理。
前者解决创建问题,后者解决回收问题。
那么在来看Lazy的.NET Framework实现之前,我们先来自己实现一个简单的Lazy吧。
class MyLazy where T : new(){private T value;private bool isLoaded;public MyLazy(){isLoaded = false;}public T Value{get{if (!isLoaded){value = new T();isLoaded = true;}return value;}}}这应该是最简单版本的Lazy了,没有线程安全检测,其实什么都没有,只有着访问时创建真实对象,可是对于我们一般的应用来说也许就已经足够了。
4. Lazy的.NET Framework实现原本还想解释下代码的,可是太多了,解释不动了 .就写些主要把。
其实.NET Framework和上面的实现大同小异,有两点主要的不同:A. 引入了Boxed内部类:[Serializable]private class Boxed{// Fieldsinternal T m_value;// Methods[TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]internal Boxed(T value){this.m_value = value;}}该内部类取代了我在上面实现中的泛型约束,使之更通用。
但是我们也应该注意到,如果T为结构体,那么由于T很大,所以装箱拆箱反而也许是个更耗费效率的事情,因此,个人建议,对值类型慎用Lazy。
B. 线程安全的控制在线程安全的控制选项中,.NET Framework为我们提供了这样的枚举选项:public enum LazyThreadSafetyMode{None,PublicationOnly,ExecutionAndPublication}不做多余解释,关于这三者的具体意思,MSDN中已经说的很清楚了,可参加 这里 。
里面的代码比较麻烦,就不多说了。
5. 完善的大对象解决方案在 Anytao 文章的回复中,我就提到了一点是:个人倒觉得Lazy+WeakReference才是实现一个大对象的完整解决之道,一个按需加载,一个不定清理..... 加到一起才完美。
但是 老赵 说:又不是所有的大对象都需要自动清理 那么我就不再重复去总结了,这两句话加到一起,我想应该可以得出一个比较完善的大对象处理方案吧。
如果这样,我们是否能对应地去创建一个单独的实现类:WeakLazy呢。
这样相当于我们有了WeakReference,有了Lazy,还有了WeakLazy,那么我们是不是就可以很完整地根据情况选择应用了呢!6. 总结原本想写一些稍微深入点的东西,可是写到最后,发现仍为皮毛而,叹乎 深入,Lazy,—,.NETFramework4.0,.ne