CLR探索系列:托管PE/COFF文件格式侧窥
一直都想写篇文章来说说那些尘封在PE/Coff文件格式下的那些事,还有Metadata和EEClass是如何表现了一个静态的PE格式文件在内存中的映射结构。
在这篇文章里,我不去介绍windows下PE文件的具体格式,也不去介绍一个托管或者是非托管PE文件的加载运行方式,更加不去介绍一个PE文件里面的各个头部以及整体结构的各个部分的含义。
而是侧重于介绍,基于托管环境下,DotNet对基本的PE/CoFF文件格式做了那些扩充,CLR头部介绍,以及元数据和IL代码详析解析。主要侧重从静态文件的角度,来剖析DotNet下最基本的模块的结构,以及这样的结构如何适应一个托管的环境。
拟把PE文件格式里里外外从上到下一点一点的完全解剖一遍,当然,不可能做到很全面,不然,如果想知道PE文件的各个方面的具体的细节,可以参阅文章底部推荐的那个白皮书文档,这份文档相当详尽介绍了PE文件格式的点点滴滴。
首先,还是从一段C#代码开始:
class Program
{
public const int conField=122*1119;
public readonly int roField;
private int _property;
public int Property
{
get {return _property; }
set{_property = value; }
}
static void
{
(new Program()).Method();
}
public void Method()
{
System.Console.ReadLine();
}
}
之所以定义这么多类型和字段,主要是为了在解说托管PE文件格式的时候,元数据表中相关的表都会出现相关记录。
编译之后,得到一个叫做TestConsoleApp.exe的托管PE文件。在继续下面的叙述之间,首先先概括的说一下Metadata。不说IL语言是因为,在我以前的博文中,已经有相关的介绍。
一个托管PE文件,粗略的讲,由4个部分组成。PE32(+)的头部,CLR头部,Metadata以及IL。
首先,我们来说说CLR的头部。这个东西,是托管的PE文件所特有的东西。
我们打开DotNet Framework里面的include文件夹里面一个叫做CorHdr.h的文件,找到一个叫做IMAGE_COR20_HEADER这个数据结构的定义。这个数据结构,定义的就是CLR的header里面内容。下面对其的定义:
// COM+ 2.0 header structure.
typedef struct IMAGE_COR20_HEADER
{
// Header versioning
DWORD cb;
//CLR的主版本号
WORD MajorRuntimeVersion;
//CLR的副版本号
WORD MinorRuntimeVersion;
// Symbol table and startup information
//标识元数据在这个PE文件里面起始位置。
IMAGE_DATA_DIRECTORY MetaData;
//标识这个runtime的Flags
DWORD Flags;
// DDBLD - Added next section to replace following lin
// DDBLD - Still verifying, since not in NT SDK
// DWORD EntryPointToken;
// If COMIMAGE_FLAGS_NATIVE_ENTRYPOINT is not set, EntryPointToken represents a managed entrypoint.
// If COMIMAGE_FLAGS_NATIVE_ENTRYPOINT is set, EntryPointRVA represents an RVA to a native entrypoint.
//EnterPoint Token,这个定义的是这个image的MethodDef的入口点。
union {
DWORD EntryPointToken;
DWORD EntryPointRVA;
};
// DDBLD - End of Added Area
// Binding information
//标识CLI资源的目录
IMAGE_DATA_DIRECTORY Resources;
//强命名的签名文件。标识对这个PE文件计算得到的一个Hash文件的地址。这个是在CLI的loader在加载一个PE文件的时候,验证版本和加载的时候需要使用的。可以为空。
IMAGE_DATA_DIRECTORY StrongNameSignature;
// Regular fixup and binding information
//代码管理表的地址
IMAGE_DATA_DIRECTORY CodeManagerTable;
//这个module里面的一个包含地址的数组,数组的每一项,都包含了对一个founction的指针。
IMAGE_DATA_DIRECTORY VTableFixups;
//这个也是包含的一个数组,数组里面都是方法需要jump的地址。
IMAGE_DATA_DIRECTORY ExportAddressTableJumps;
// Precompiled image info (internal use only - set to zero)
//这个地址,保存的是这个Module对应的在本机上面的Jit过后了的本地代码的目录。
IMAGE_DATA_DIRECTORY ManagedNativeHeadesr;
} IMAGE_COR20_HEADER, *PIMAGE_COR20_HEADER;
接下来,我们就直接打开上面编译好的那个托管模块的CLR头部,看看里面有些什么:
----- CLR Header:
Header size: 0x00000048
Major runtime version: 0x0002
Minor runtime version: 0x0005
0x00002094 [0x00000660] address [size] of Metadata Directory:
Flags: 0x00000001
Entry point token: 0x06000003
0x00000000 [0x00000000] address [size] of Resources Directory:
0x00000000 [0x00000000] address [size] of Strong Name Signature:
0x00000000 [0x00000000] address [size] of CodeManager Table:
0x00000000 [0x00000000] address [size] of VTableFixups Directory:
0x00000000 [0x00000000] address [size] of Export Address Table:
0x00000000 [0x00000000] address [size] of Precompile Header:
从上面的这个托管模块的头部可以看到,这个头部里面包含的内容,和这个头部的结构体所定义的东西,是完全一致的。
几个需要说明的,一个是Major runtime version,以及Minor runtime version标识的是不同的runtime的版本。在上面的头部中,主要是一个文件的偏移的offset,就是Entry point token的地址。
这里要特别提出来一点,这里的Entry point token表示的是入口点,是MethodDef的入口点。
而不是整个托管PE文件的入口点。整个PE文件的入口点,在这里,用PEID打开可以看到,是
Addr. of entry point: 0x000027be
这个EnterPoint是在一个32位的PE Optional Header里面定义的。这个入口点,才是整个应用程序的入口点。
这个入口点里面,我们使用IDA之类的逆向工程工具可以看到,托管PE模块在这个地址上面的代码:
004027BE: FF 25 00 20 40 00 jmp dword ptr ds:[402000] ; _CorExeMain
在这里,我们看到了熟悉的CorExeMain这个入口函数 ^_^ 关于这个函数,我就不多说了,在前面的博文里面有详析的分析。参见那篇探索托管模块加载过程的文章。
接下来,我们介绍下元数据表,以及一些对这个PE文件的统计信息,首先查看这个托管PE文件的统计信息,使用的还是ildasm工具,我的最爱:
File size : 16384
PE header size : 4096 (496 used) (25.00%)
PE additional info : 1075 ( 6.56%)
Num.of PE sections : 3
CLR header size : 72 ( 0.44%)
CLR meta-data size : 1632 ( 9.96%)
CLR additional info : 0 ( 0.00%)
CLR method headers : 16 ( 0.10%)
Managed code : 49 ( 0.30%)
Data : 8192 (50.00%)
Unaccounted : 1252 ( 7.64%)
Num.of PE sections : 3
.text - 4096
.rsrc - 4096
.reloc - 4096
CLR meta-data size : 1632
Module - 1 (10 bytes)
TypeDef - 2 (28 bytes) 0 interfaces, 0 explicit layout
TypeRef - 18 (108 bytes)
MethodDef - 5 (70 bytes) 0 abstract, 0 native, 5 bodies
FieldDef - 3 (18 bytes) 0 constant
MemberRef - 17 (102 bytes)
ParamDef - 2 (12 bytes)
Constant - 1 (6 bytes)
CustomAttribute- 13 (78 bytes)
StandAloneSig - 1 (2 bytes)
PropertyMap - 1 (4 bytes)
Property - 1 (6 bytes)
MethodSemantic- 2 (12 bytes)
Assembly - 1 (22 bytes)
AssemblyRef - 1 (20 bytes)
Strings - 680 bytes
Blobs - 236 bytes
UserStrings - 8 bytes
Guids - 16 bytes
Uncategorized - 194 bytes
CLR method headers : 16
Num.of method bodies - 5
Num.of fat headers - 1
Num.of tiny headers - 4
Managed code : 49
Ave method size - 9
可以看到,在上面的统计信息中,显示了一个托管的PE模块的各个部分的组成。从各个部分的统计的大小里面,PE Header和CLR的Metadata占据了相当大的比例,而IL代码,仅仅占据了整个托管模块大小的0.3%。只有49个字节。
顺便提一下,如果Unaccounted显示的是负数,是不能相信的,那是以前的版本存在的一个bug。
就写到这里吧,如果觉得看了上面的东西还是不知所云或者觉得不完整,那我推荐一本MS的白皮技术文档:Visual Studio, Microsoft Portable Executable and Common Object File Format Specification
可以在msdn上面下载到,它完整的讲述了PE文件格式的各个部分的细节。
接下来的一篇博文,就说说元数据以及元数据表的内存结构,逻辑结构和在SSCLI中的设计和实现。
最后做个广告:
欢迎园子里面的朋友加入SSCLI团队,这里,我们致力于对.Net底层核心技术及其实现的研究。如果你想真正的了解.Net最核心的实现,我们热忱的欢迎你的加入:
圈子地址:http://sscli.cnblogs.com
加入地址:http://www.cnblogs.com/lbq1221119/archive/2008/03/10/1097627.html
圈子刚刚建立,希望园子里的朋友多多支持,:)
posted on 2008-03-10 10:02 lbq1221119 阅读(2281) 评论(3) 编辑 收藏 举报