CLR探索系列:托管PE/COFF文件格式侧窥

一直都想写篇文章来说说那些尘封在PE/Coff文件格式下的那些事,还有Metadata和EEClass是如何表现了一个静态的PE格式文件在内存中的映射结构。
   在这篇文章里,我不去介绍windowsPE文件的具体格式,也不去介绍一个托管或者是非托管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 Main(string[] args)

        {

            (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这个数据结构的定义。这个数据结构,定义的就是CLRheader里面内容。下面对其的定义:

 

// 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;       

    //标识这个runtimeFlags

    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,这个定义的是这个imageMethodDef的入口点。

    union {

        DWORD               EntryPointToken;

        DWORD               EntryPointRVA;

    };
           
// DDBLD - End of Added Area

   

    // Binding information

    //标识CLI资源的目录

    IMAGE_DATA_DIRECTORY    Resources;

 

    //强命名的签名文件。标识对这个PE文件计算得到的一个Hash文件的地址。这个是在CLIloader在加载一个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 HeaderCLRMetadata占据了相当大的比例,而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  阅读(2267)  评论(3编辑  收藏  举报

导航