首页
社区
课程
招聘
[分享]转四篇关于MS.Net CLR扩展PE结构分析的文章
发表于: 2006-8-30 23:15 5496

[分享]转四篇关于MS.Net CLR扩展PE结构分析的文章

2006-8-30 23:15
5496
分析的相当详细,如其中的第一篇

MS.Net CLR扩展PE结构分析 (1)
作者:Flier Lu <flier@nsfocus.com>
主页:http://www.nsfocus.com
日期:2002-08-16
1.概述

    本文从系统底层角度,通过分析MS.Net CLR架构在Win32平台上对PE可执行文件
映像结构的扩展,解析CLR架构底层的部分运行机制,帮助读者从更深层次理解CLR架构
中某些重要概念。
    本文读者应具备基本的Win32编程经验,了解CLR中常见概念意义,并对Win32平台
之PE可执行文件映像结构有一定了解,具体说明请参看Matt Pietrek于1994年3月发表在
MSJ上的经典文章《Peering Inside the PE: A Tour of the Win32 Portable
Executable File Format》,与之重复的部分本文一概跳过。

2.前言

    对一个优秀Win32平台程序员来说,对PE结构的了解是对Win32架构了解的必经之路,
而从Chicago(Win95的开发代号,Win95正式发布以前的文档中对Win95的称呼)以来,
PE结构就相对稳定,直到MS.Net的出现,才发生了一些不大不小的变化。
    之所以说是不大不小的变化,是因为CLR并没有对PE结构进行结构上的调整,只是利用现有
PE结构的优良可扩展性,将CLR所需的信息扩展到PE映像中。具体一点说,就是利用了PE结构中的
IMAGE_OPTIONAL_HEADER.DataDirectory[IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR]
来保存服务于CLR的IMAGE_COR20_HEADER结构,其它的PE结构基本不变。

3.CLR 头信息

   与传统PE可执行文件一样,CLR程序也有一个文件头保存CLR相关的全局性信息。
但因为要保证现有PE结构的稳定性,CLR程序仅使用PE结构的一个Directory Entry
也就是前言中提到的IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR。
此入口原本是设计用于COM,但不知为何一直没有被使用。

3.1 IMAGE_COR20_HEADER

   IMAGE_COR20_HEADER结构的定义,可以在FrameworkSDK\include\CorHdr.h
头文件中找到,如下:

// CLR 2.0 header structure.
typedef struct IMAGE_COR20_HEADER
{
    // Header versioning
    ULONG                   cb;
    USHORT                  MajorRuntimeVersion;
    USHORT                  MinorRuntimeVersion;

    // Symbol table and startup information
    IMAGE_DATA_DIRECTORY    MetaData;
    ULONG                   Flags;
    ULONG                   EntryPointToken;

    // Binding information
    IMAGE_DATA_DIRECTORY    Resources;
    IMAGE_DATA_DIRECTORY    StrongNameSignature;

    // Regular fixup and binding information
    IMAGE_DATA_DIRECTORY    CodeManagerTable;
    IMAGE_DATA_DIRECTORY    VTableFixups;
    IMAGE_DATA_DIRECTORY    ExportAddressTableJumps;

    // Precompiled image info (internal use only - set to zero)
    IMAGE_DATA_DIRECTORY    ManagedNativeHeader;

} IMAGE_COR20_HEADER;

    详细的说明信息,可以在FrameworkSDK\Tool Developers Guide\docs
目录中找到。此结构虽然字段较多,但实际上其核心在于MetaData,其他信息都是围绕着
MetaData服务。

3.2 实例分析

    下面我们分析两个实际的CLR程序的Cor20Header结构,wsdl.exe是.Net Framework
提供的wsdl分析工具、MgrCpp.exe是一个简单的VC.Net编写的Managed C++和
Unmanaged C++并存的程序,MgrCpp.cpp 代码如下

#include "stdafx.h"

#using <mscorlib.dll>
#include <tchar.h>

#include <iostream>

using namespace System;

// This is the entry point for this application
int _tmain(void)
{
    // TODO: Please replace the sample code below with your own.
    Console::WriteLine(S"Hello World");

    std::cout << "Hello World" << std::endl;

    return 0;
}

wsdl.exe 可以用Dumpbin /all wsdl.exe > wsdl.dump 导出,关键数据如下

...
  500000 image base (00500000 to 00511FFF)
...
  2008 [      48] RVA [size] of COM Descriptor Directory
...
RAW DATA #1
  00502000: ?? ?? ?? ?? ?? ?? ?? ?? 48 00 00 00 02 00 00 00  àü......H.......
  00502010: F8 67 00 00 B0 74 00 00 09 00 00 00 21 00 00 06  g..°t......!...
  00502020: 09 51 00 00 8E 16 00 00 50 20 00 00 80 00 00 00  .Q......P ......
  00502030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  00502040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................

MgrCpp.exe 类似处理,关键数据如下

...
  400000 image base (00400000 to 0045CFFF)
...
  4B24C [      48] RVA [size] of COM Descriptor Directory
...
  0044B240: ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? 48 00 00 00  @.@.à#@.H...
  0044B250: 02 00 00 00 70 EB 04 00 D0 42 00 00 02 00 00 00  ....p..DB......
  0044B260: 73 00 00 06 00 00 00 00 00 00 00 00 00 00 00 00  s...............
  0044B270: 00 00 00 00 00 00 00 00 00 00 00 00 78 2E 05 00  ............x...
  0044B280: C0 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00  à...............
  0044B290: 00 00 00 00 ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ??  ....1E.@?@.....

3.3 字段分析

    cb(0x48)是IMAGE_COR20_HEADER结构的大小。

    MajorRuntimeVersion(0x02)和MinorRuntimeVersion(0x00)是版本号,
指执行此程序所需的最低CLR版本号。

    Metadata是核心数据所在,我们等会再详细分析。

    Flags是Runtime Image描述标志,描述此CLR映像的执行属性。wsdl.exe的Flags
是0x09或1001B,MgrCpp.exe的Flags是0x02或0010B,具体含义如下:

    // CLR Header entry point flags.
    COMIMAGE_FLAGS_ILONLY               =0x00000001,
    COMIMAGE_FLAGS_32BITREQUIRED        =0x00000002,
    COMIMAGE_FLAGS_IL_LIBRARY           =0x00000004,
    COMIMAGE_FLAGS_STRONGNAMESIGNED =0x00000008,
    COMIMAGE_FLAGS_TRACKDEBUGDATA       =0x00010000,

    COMIMAGE_FLAGS_ILONLY 标志位说明此CLR程序由纯IL代码组成,IL代码是类似
Java的Bytecode的中间代码,由纯IL代码组成就代表此程序可以在任何实现了CLR环境
的平台,如支持CLI的FreeBSD上执行。一般由C#之类纯.Net语言生成的程序,都会设置此位,
如wsdl.exe就设置此位,而混合编译的VC.Net程序如MgrCpp因为代码由IL和Native Code
组成,因而此位置空。
    COMIMAGE_FLAGS_32BITREQUIRED 标志位说明此CLR映像只能在32位系统上执行,
对以后的64位或嵌入式系统上的CLR无效(MS.Net很重要的一个目的就是为以后平滑过渡到64位
平台做准备,想想以前16位平台到32位平台过渡时的混乱,以及现在比以前翻了n倍的代码量就恐怖,
MS真是未雨绸缪啊,呵呵)。一般纯IL代码的程序都是具有良好移植性的,混合编码则必须32bit。
    COMIMAGE_FLAGS_IL_LIBRARY标志位说明此CLR映像是作为IL代码库存在的。
    COMIMAGE_FLAGS_STRONGNAMESIGNED说明此映像有strong name signature
这个strong name signature在CLR架构里起到了非常重要的作用。为什么这么说呢,
因为这个strong name signature起到Assembly的身份证的作用,
它关系到CLR中大量安全概念的实现,如保证代码完整性,作为代码权限获取凭证等等。
因此在发布.Net程序前,一定要加上strong name signature。
    COMIMAGE_FLAGS_TRACKDEBUGDATA标志位目前没有使用到,缺省置0。

    EntryPointToken则是指向IL程序的入口点,类似于以前PE结构中
IMAGE_OPTIONAL_HEADER.AddressOfEntryPoint的作用,只是以前的
AddressOfEntryPoint是一个RVA,直接指向程序入口代码所在地址,
(不要告诉我你不知道RVA是什么啊,呵呵,赶快去看Peering Inside the PE)
而现在EntryPointToken指向一个Token。(注意,是Token,而不是地址)
因为IL代码是JIT编译的,存在于映像中的都是IL形式的P-code(pseudo code),
在需要时才由CLR动态读取,在内存中编译展开为本机代码(Native Code),进而执行。
因此这里的程序入口只是一个MethodDef或File表的入口,一个Token而已。
如wsdl的EntryPointToken值0x06000021指向0x06号表(MethodDef)的21项
Microsoft.DevApps.WebServices.WebServiceUtil.Main方法。
   这里的MethodDef是一个MetaData表,每行定义一个函数或方法;而File表则是
每行有一个File定义的表,每个File表项包含一个外部文件的信息。也就是说,
在执行程序时可以直接编译执行此映像中的一个方法的IL代码,也可能是重定向到另一个文件,
这就是Assembly作为一个逻辑代码单元,与传统DLL之类相比一个很大的不同。
DLL是一个完整的代码实体,而Assembly则是由一组物理上可分布存在的实体组成的
逻辑单元组。
  剩下没有解说的几个字段都是IMAGE_DATA_DIRECTORY类型,这个类型是一个数据块
定义结构,类似一个数组定义。在Winnt.h中有定义

//
// Directory format.
//

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;
    DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

  Resources定义资源所在;StrongNameSignature定义刚刚提到的strong name
signature所在;此外CodeManagerTable,ExportAddressTableJumps,
MangedNativeHeader目前都没用到。VTableFixups是由使用VTable的语言如C++
使用的,类似原来PE结构中的重定向表,结构如下

  _IMAGE_COR_VTABLEFIXUP = packed record
    RVA: DWORD;     // Offset of v-table array in image.
    Count,          // How many entries at location.
    Kind: Word;     // COR_VTABLE_xxx type of entries.
  end;
  IMAGE_COR_VTABLEFIXUP = _IMAGE_COR_VTABLEFIXUP;
  TImageCorVTableFixup = _IMAGE_COR_VTABLEFIXUP;
  PImageCorVTableFixup = ^TImageCorVTableFixup;
  TImageCorVTableFixupArray = array[0..MaxWord-1] of TImageCorVTableFixup;
  PImageCorVTableFixupArray = ^TImageCorVTableFixupArray;

4.Metadata 的逻辑结构

  在分析完CLR的头信息后,我们来看看核心数据Metadata的结构

4.1 什么是 Metadata

    Metadata翻译成中文是“元数据”,可以理解为Type of Type,
实际上就是描述类型信息的类型信息。从最初级的语言层面支持的RTTI
(“近代”的编程语言基本上都提供了足够的支持,如C++,Delphi等,
部分较“落伍”的语言也通过不同形式如扩展库提供了模拟支持,
“现代”的编程语言则提供了强力的支持,如Java和C#<本质是CLR>),
到后来二进制级的COM的IDL和类型库(类型库是IDL编译后的二进制形式),
到现在的Metadata,到即将到来的WebService的SOAP/WSDL,
其实是遵循着相同的发展思路。只是出于不同的需求、设计、实现,
有这各自的优点缺点罢了。但随着语言的发展,更多的需求将集中到
灵活性方面,因而语言发展的趋势是元数据的使用越来越多、支持越来越强。
    举个最简单的例子,在IDE中,动态显示当前对象的方法、属性名列表的功能
(MS叫IntelliSense,Borland叫CodeInsight),就得宜于元类型信息。
以前在VC里实现,比较麻烦,得预编译生成专用的符号库;在VB里强一点,可以通过
COM的IDispatch,ITypeInfo,ITypeLib等接口动态获取,但编程使用复杂;
到CLR,在基础库一级直接提供支持,可以通过System.Reflection完全控制,
甚至比COM类型库更高一级地支持动态创建。
   对用户来说,可以完全了解当前程序结构、接口,有哪些Module,哪些Class,
哪些Method等等,这给开发者提供了巨大的创造空间。如DUnit(DotNet下
的XUnit单元测试平台)实现上就大量使用Reflection机制。
   以后有时间我会写一篇CLR程序的静态、动态分析,专门讨论这个问题。

4.2 Metadata在CLR中的作用

    对于CLR架构来说,Metadata可以算是核心数据所在,几乎绝大多数功能
都需要使用其数据。从静态的IL代码构成(二进制编码中直接使用Metadata里的Token)
到动态JIT编译器(使用Metadata定位IL代码及其关系);从简单的代码载入执行
(Class Loader通过Metadata定位代码入口、编译执行)到复杂的不同语言互操作
(如VB.Net继承C#的类,实际上是直接继承CLR中Metadata中的类)等等……
几乎所有地方都能看到Metadata的身影。

4.3 如何访问和使用 Metadata

   在CLR里使用Metadata,可以在三个层面进行操作。

   最简单的方法是直接通过.Net BCL基础类库提供的System.Reflection
命名空间中的辅助工具类进行访问,例如

using System.Reflection;
using System;

public class Simple
{
    public static void Main ()
    {
         Module mod = Assembly.GetExecutingAssembly().GetModules () [0];
         Console.WriteLine ("Module Name is " + mod.Name);
         Console.WriteLine ("Module FullyQualifiedName is " + mod.FullyQualifiedName);
         Console.WriteLine ("Module ScopeName is " + mod.ScopeName);
    }
}

   这种访问方式使用起来最简单,功能也足够强大,能够完成我们绝大多数的需要,
特别是在System.Reflection.Emit命名空间中,更提供了动态生成、修改代码的支持。
   不过这种方式必须有CLR环境的支持,受到库功能的限制(后面我们会看到一些
在Reflection库一级里不提供支持的信息),因此MS为工具软件开发商提供了另一套
较底层的开发库:Metadata Unmanaged API。这套支持库通过一系列COM接口,
提供了直接访问Metadata的强大支持,有兴趣的朋友可以参看
FrameworkSDK\Tool Developers Guide\docs\Metadata Unmanaged API.doc文档,
里面有详细的使用说明。如同其名字所示,它必须用Unmanaged代码来使用,如VC,Delphi等。

   可以说99%的工作,都可以通过上面两套库来完成,不过总有些象我这样的人,
喜欢对技术追根究底,想把隐藏在美好面纱下的底层架构纠出来暴露一把,呵呵
因此有了第三个层面,二进制层面的逆向工程分析。
   好在MS为了让其CLI(CLR的子集)标准化,公开了大量底层实现文档,
FrameworkSDK\Tool Developers Guide\docs\Partition II Metadata.doc
文档中对Metadata的二进制格式实现给出了比较详尽的说明,MS也公布了一个支持Win32和
FreeBSD平台的CLI实现的源代码(已移植到Linux等其它平台)。加上GNOME的mono项目已经
做了很多工作,因而对Metadata的二进制层面分析不是那么困难。
   接下去的文章中,会试图一步步将Metadata在PE中的组织结构剥离开来,
让大家能够了解这个神秘的CLR核心:Metadata到底是什么,里面隐藏了些什么,我们能够通过
他做什么,为什么要这样设计,等等……

4.4 Metadata在PE中的组织结构

   回到正体上来,谈谈Metadata在PE中的组织结构。
   上章我们提到CLR的头信息里面专门有一个字段指向Metadata数据块,
实际上这个数据块只是Metadata的一个头结构,保存有Metadata的全局信息,
而Metadata的实际数据,是通过若干不同的Heap或者说Stream保存的。
这里我统一使用Stream“流“作为他的名字,但很多文档中以Heap”堆“作为
其称呼,我们可以理解他是一个二进制流,其中数据以堆的结构进行组织。

   Metadata里最常见的有五种流,#String, #Blob, #Guid,
#US(User String)和#~流("#"是流名字的前缀)

   String流就是一个字符串堆,Metadata内部用到的所有字符串如类或方法
的名字等等都以UTF8编码保存在此堆内。而用户的字符串如字符串常量,
则以Unicode编码保存在US(User String)堆内。值得注意的是,
US流和String流在二进制结构组织上不同,我们后面将分析时会详细提及。
    Guid流是保存程序中使用到的Guid的数组,如Assembly中Module的MVID。
    Blob流是一个通用存储空间,除了Guid和字符串以外基本上所有
剩下的东西都以二进制数据形式放在里面,如PublicKey,常量的值等等。
   最重要的是#~流,这是Metadata核心信息存放的地方。#~流物理上以
若干张表(Table)的形式组织,每张表存储某一方面的Metadata信息,
如MethodDef表存储所有方法的信息。每张表又由若干的行(Row)组成
每行有n个列(Column),每列代表一种信息,如MethodDef表中每一行
都有一个方法的RVA,类型标志,名字,Signature等等信息。在其中通过
各种索引来相互关联,整个组织结构和关系数据库很相似。
   比较特殊的是,这里所有的表通过一个64bit的有效位图来表示表存在与否
每种类型的表有一个编号,如MethodDef表的编号是6,则第(1<<(6-1))位置1
因而每个表的每一行,可以使用一个唯一的Token表示。此Token是一个32bit
无符号整型数,最高一个字节表示表的序号,低三个字节表示表中的索引号。
如0x06000003表示0x06表(MethodDef)中第3行(如MyApp::Add)
这个Token概念在CLR中频繁使用,如IL代码调用函数、使用变量都是使用Token。
   与之类似的还有Coded Index,等会讲二进制实现时再说。

5.Metadata 的物理结构

5.1 Metadata Header

  前言中我们分析了CLR Header的结构,里面有一个Metadata字段

  IMAGE_DATA_DIRECTORY    MetaData;

  字段指向一个数据块,里面包含着Metadata Header,是关于
Metadata信息所在,结构定义伪代码如下

  PClrMetadataHeader = ^TClrMetadataHeader;
  TClrMetadataHeader = packed record
    Signature: DWORD;   // Magic signature for physical metadata : $424A5342.
    MajorVersion,       // Major version, 1
    MinorVersion: Word; // Minor version, 0
    Reserved,           // Reserved, always 0
    Length: DWORD;      // Length of version string in bytes, say m.
    Version: array[0..0] of Char;
    // UTF8-encoded version string of length m
    // Padding to next 4 byte boundary, say x.
    {
    Version: array[0..((m+3) and (not $3))-1] of Char;
    Flags,              // Reserved, always 0
    Streams: Word;      // Number of streams, say n.
    // Array of n StreamHdr structures.
    StreamHeaders: array[0..n-1] of TClrStreamHeader;
    }
  end;

wsdl.exe的Metadata头信息
...
  67F8 [    74B0] RVA [size] of MetaData Directory
...
  005067F0: ?? ?? ?? ?? ?? ?? ?? ?? 42 53 4A 42 01 00 01 00  l.pdb...BSJB....
  00506800: 00 00 00 00 0C 00 00 00 76 31 2E 78 38 36 72 65  ........v1.x86re
  00506810: 74 00 00 00 00 00 05 00 6C 00 00 00 64 1E 00 00  t.......l...d...
  00506820: 23 7E 00 00 D0 1E 00 00 70 1F 00 00 23 53 74 72  #~..D...p...#Str
  00506830: 69 6E 67 73 00 00 00 00 40 3E 00 00 18 13 00 00  ings....@>......
  00506840: 23 55 53 00 58 51 00 00 10 00 00 00 23 47 55 49  #US.XQ......#GUI
  00506850: 44 00 00 00 68 51 00 00 48 23 00 00 23 42 6C 6F  D...hQ..H#..#Blo
  00506860: 62 00 00 00 00 00 00 00 01 00 00 01 57 1F A2 03  b...........W.¢.
...

  首先是Signature,是一个DWORD或4字符长的标记,$424A5342或'BSJB'
可以通过此标记判断Metadata是否存在,如

function TJclPeCLRHeader.GetHasMetadata: Boolean;
const
  METADATA_SIGNATURE = $424A5342;
begin
  with Header.MetaData do
    Result := (VirtualAddress <> 0) and
      (PDWORD(FImage.RvaToVa(VirtualAddress))^ = METADATA_SIGNATURE);
end;

通过检测Metadata节是否存在,以及头一个DWORD是否为Metadata标记来判断
Metadata是否存在。

  接着的MajorVersion和MinorVersion保存Metadata格式的版本号,
一般设置为1.1。(文档中说明是1.0,实际为1.1)

  跟着的Length+Version指定了UTF8格式的编译环境版本号,这个和你的CLR的编译环境
版本相同,如.Net Framework工具的编译环境版本号为v1.x86ret,你用它编译代码
生成的版本号则是诸如v1.0.3705类型,而MS CLI实现的编译环境版本号为v1.0.0。
注意这里Version字符串是按四字节对齐的,在分析二进制流时应按(m+3) and (not $3)
来计算实际的长度。

  Streams和StreamHeaders则是Metadata流的信息数组,保存有Metadata不同类型
流的Offset, Size, Name等信息,每记录的结构如下

  PClrStreamHeader = ^TClrStreamHeader;
  TClrStreamHeader = packed record
    Offset,      // Memory offset to start of this stream from start of the metadata root
    Size: DWORD; // Size of this stream in bytes, shall be a multiple of 4.
    // Name of the stream as null terminated variable length
    // array of ASCII characters, padded with \0 characters
    Name: array[0..MaxWord] of Char;
  end;

  Offset是流实际位置相对Metadata头的偏移;Size是流的长度,以DWORD为单位;
Name是以#0字符结束的字符串,表示流的名称。
  根据这些信息,我们可以从PE映像中读取相应流进行分析。

5.2 Metadata Stream

  上一章我们曾经提过,Metadata Stream有五种常见类型,#String, #Blob,
#Guid, #US(User String)和#~流,每种类型流最多只能出现一次,#US和#Blob流
可省略。

5.2.1 #String 流

  #String流是一个字符串堆,程序用到的字符串,如类名、函数名、参数名等字符串
保存在其中。字符串以UTF8编码保存,以#0字符分隔。流首总有一个#0字符代表一个
空字符串,分析代码如下。

constructor TJclClrStringsStream.Create(
  const AMetadata: TJclPeMetadata;
  const AHeader: PClrStreamHeader);
var
  pch: PChar;
  off: DWORD;
begin
  inherited;

  FStrings := TStringList.Create;
  pch      := Data;
  off      := 0;
  while off < Size do
  begin
    if pch^ <> #0 then
      FStrings.AddObject(pch, TObject(off));
    pch := pch + StrLen(pch) + 1;
    off := DWORD(pch - Data);
  end;
end;

  如碰到#0字符,直接作为空字符串跳过之,否则,以StrLen取得字符串长度,
并将字符串保存到一个字符串列表中,使用时需要动态将UTF8解码为Unicode。

function TJclClrStringsStream.GetString(const Idx: Integer): WideString;
begin
  Result := UTF8ToWideString(FStrings.Strings[Idx]);
end;

  因为流中数据是以堆形式存放,指向#String流的索引是字符串相对偏移值。

function TJclClrStringsStream.At(const Offset: DWORD): WideString;
var
  Idx: Integer;
begin
  Idx := FStrings.IndexOfObject(TObject(Offset));
  if Idx <> -1 then
    Result := GetString(Idx)
  else
    Result := '';
end;

  CLR中包和类名的最大长度都是1024

5.2.2 #Guid 流

  #Guid流格式很简单,就是一个GUID结构的数组,以流长度除以SizeOf(TGuid)就是
数组元素个数。因为流中数据是以数组形式存放,指向#Guid流的索引是以1开始的索引值,
如索引为0则表示此项索引不存在。(注意,CLR中所有索引都是以1开始的,0表示索引值
不存在)示例代码如下:

constructor TJclClrGuidStream.Create(
  const AMetadata: TJclPeMetadata;
  const AHeader: PClrStreamHeader);
var
  I: Integer;
  pg: PGUID;
begin
  inherited;

  SetLength(FGuids, Size div SizeOf(TGuid));
  pg := Data;
  for I:=0 to GetGuidCount-1 do
  begin
    FGuids[I] := pg^;
    Inc(pg);
  end;
end;

5.2.3 #Blob 和 #US 流

  #Blob流是一个二进制数据堆,程序中的所有非字符串形式数据都堆放在这个流里面,
如常数的值,Public Key的值,方法的Signature等等。
  在每个二进制数据块头,都有一个块长度数据,但为了节约存储空间,CLR使用了比较
麻烦的编码方法。
  如果开始一个字节最高位为0,则此数据块长度为一个字节;
  如果开始一个字节最高位为10,则此数据块长度为两个字节;
  如果开始一个字节最高位为110,则此数据块长度为四个字节;
  在屏蔽标志位后,通过移位运算即可计算出数据块的实际长度值,并依据此获得数据。

constructor TJclClrBlobRecord.Create(
  const AStream: TJclClrStream;
  const APtr: PByteArray);
var
  b: Byte;
  AData: Pointer;
  ASize: DWORD;
begin
  FPtr    := APtr;
  FOffset := DWORD(FPtr) - DWORD(AStream.Data);

  b := FPtr[0];
  if b = 0 then
  begin
    AData := @FPtr[1];
    ASize := 0;
  end
  else if ((b and $C0) = $C0) and ((b and $20) = 0) then    // 110bs
  begin
    AData := @FPtr[4];
    ASize := ((b and $1F) shl 24) + (FPtr[1] shl 16) + (FPtr[2] shl 8) + FPtr[3];
  end
  else if ((b and $80) = $80) and ((b and $40) = 0) then    // 10bs
  begin
    AData := @FPtr[2];
    ASize := ((b and $3F) shl 8) + FPtr[1];
  end
  else
  begin
    AData := @FPtr[1];
    ASize := b and $7F;
  end;

  Assert(not IsBadReadPtr(AData, ASize));

  inherited Create(AData, ASize);
end;

constructor TJclClrBlobStream.Create(
  const AMetadata: TJclPeMetadata;
  const AHeader: PClrStreamHeader);
var
  ABlob: TJclClrBlobRecord;
begin
  inherited;

  FBlobs := TObjectList.Create;

  ABlob := TJclClrBlobRecord.Create(Self, Data);
  while Assigned(ABlob) do
  begin
    if ABlob.Size > 0 then
      FBlobs.Add(ABlob);
    if (Integer(ABlob.Memory) + ABlob.Size) < (Integer(Self.Data) + Integer(Self.Size)) then
      ABlob := TJclClrBlobRecord.Create(Self, Pointer(Integer(ABlob.Memory) + ABlob.Size))
    else
      ABlob := nil;
  end;
end;

  #US流是User String的存储空间,结构上和#Blob流相同,只是所有数据都是
Unicode格式的字符串。
  #Blob和#US流的索引都是数据块偏移。

5.2.4 #~ 流

  #~流是Metadata中最复杂也是最重要的信息所在,几乎所有Metadata信息
都以表的形式组织存放在#~流中。流起始处有一个#~ Stream Header如下

  PClrTableStreamHeader = ^TClrTableStreamHeader;
  TClrTableStreamHeader = packed record
    Reserved: DWORD; // Reserved, always 0
    MajorVersion,    // Major version of table schemata, always 1
    MinorVersion,    // Minor version of table schemata, always 0
    HeapSizes,       // Bit vector for heap sizes.
    Reserved2: Byte; // Reserved, always 1
    Valid,           // Bit vector of present tables, let n be the number of bits that are 1.
    Sorted: Int64;   // Bit vector of sorted tables.
    // Array of n four byte unsigned integers indicating the number of rows
    // for each present table.
    Rows: array[0..MaxWord] of DWORD;
    //Rows: array[0..n-1] of DWORD;
    //Tables: array
  end;

  字段MajorVersion.MinorVersion是表格式的版本号,一般设置为 1.0

  在介绍其他字段意义前,我们要先了解Metadata中表的表示方法。
  在Metadata中,表最多有64种,每种表有一个唯一的确定的编号,如Assembly表的
编号为$20、Module表的编号为$00等等。目前的CLR只使用到了最多$2B(43)种表,
其间还有一些表编号是被保留或非公开的。经过对多个信息来源的整理,完整对应表编号如下

  TJclClrTableKind = (
    ttModule,               //  $00
    ttTypeRef,              //  $01
    ttTypeDef,              //  $02
    ttFieldPtr,             //  $03
    ttFieldDef,             //  $04
    ttMethodPtr,            //  $05
    ttMethodDef,            //  $06
    ttParamPtr,             //  $07
    ttParamDef,             //  $08
    ttInterfaceImpl,        //  $09
    ttMemberRef,            //  $0a
    ttConstant,             //  $0b
    ttCustomAttribute,      //  $0c
    ttFieldMarshal,         //  $0d
    ttDeclSecurity,         //  $0e
    ttClassLayout,          //  $0f
    ttFieldLayout,          //  $10
    ttSignature,            //  $11
    ttEventMap,             //  $12
    ttEventPtr,             //  $13
    ttEventDef,             //  $14
    ttPropertyMap,          //  $15
    ttPropertyPtr,          //  $16
    ttPropertyDef,          //  $17
    ttMethodSemantics,      //  $18
    ttMethodImpl,           //  $19
    ttModuleRef,            //  $1a
    ttTypeSpec,             //  $1b
    ttImplMap,              //  $1c
    ttFieldRVA,             //  $1d
    ttENCLog,               //  $1e
    ttENCMap,               //  $1f
    ttAssembly,             //  $20
    ttAssemblyProcessor,    //  $21
    ttAssemblyOS,           //  $22
    ttAssemblyRef,          //  $23
    ttAssemblyRefProcessor, //  $24
    ttAssemblyRefOS,        //  $25
    ttFile,                 //  $26
    ttExportedType,         //  $27
    ttManifestResource,     //  $28
    ttNestedClass,          //  $29
    ttTypeTyPar,            //  $2a
    ttMethodTyPar);         //  $2b

  在#~流头中,使用了两个64位位图表示这些表的状态,如Valid位图中第$20位为1
则Assembly表在当前Metadata中存在,Sorted位图中第$00位为1则Module表已排序等等。

  这里就涉及到在Metadata中非常重要的一个概念Token。一个Token是在一个Metadata
中可以唯一性确定一个记录的标识符,他是一个32位无符号整型数,高8位表示表的编号
低24位表示记录在表中的索引。因此一个Token为$20000001实际上表示编号为$20的
Assembly表中第1项记录。Metadata Unmanaged API就是使用Token来表示每个记录。
  对Valid位图进行从低到高位扫描,发现一个表存在,则可到Rows中获取此表中记录
的行数,Rows数组大小为Valid位图中设置的位的数量,也就是Metadata中存在的表
的数量。而Tables则是实际数据的开始。

constructor TJclClrTableStream.Create(
  const AMetadata: TJclPeMetadata;
  const AHeader: PClrStreamHeader);

  function BitCount(const Value: Int64): Integer;
  var
    AKind: TJclClrTableKind;
  begin
    Result := 0;
    for AKind:=Low(TJclClrTableKind) to High(TJclClrTableKind) do
      if (Value and (Int64(1) shl Integer(AKind))) <> 0 then
        Inc(Result);
  end;

  procedure EnumTables;
  var
    AKind: TJclClrTableKind;
    pTable: Pointer;
  begin
    pTable      := @Header.Rows[BitCount(Header.Valid)];
    FTableCount := 0;
    for AKind:=Low(TJclClrTableKind) to High(TJclClrTableKind) do
    begin
      if (Header.Valid and (Int64(1) shl Integer(AKind))) <> 0 then
      begin
        FTables[AKind] := ValidTableMapping[AKind].Create(Self, pTable, Header.Rows[FTableCount]);
        pTable := Pointer(DWORD(pTable) + FTables[AKind].Size);
        Inc(FTableCount);
      end
      else
        FTables[AKind] := nil;
    end;
  end;
begin
  inherited;

  FHeader := Data;

  EnumTables;
end;

  这里还有一个HeapSizes,表示其他几个流中索引的大小,位$01表示#String流,
$02表示#Guid流,#04表示Blob流。如相应位设置为1,则表示对应流大于MaxWord
也就是大于2^16,因此需要以四字节来做索引,否则缺省用两字节做索引。
  我们可以看到,为了节省存储空间,Metadata中使用了相对多的编码方式来合并字段。

6.小结

  至此,MS.Net CLR对PE文件结构的扩展的分析,可以告一段落,更深入的对Metadata
表一级的分析,有兴趣的朋友可以参看FrameworkSDK\Tool Developers Guide中的文档、
mscli和mono的代码、也可向我索取为JCL项目编写的JclCLR解析代码(未完成)。
此外网上也有一些独立的开源项目完成了类似的工作。也欢迎大家来信与我讨论。

7.备注

  .Net Framework SDK

  http://msdn.microsoft.com/downloads/default.asp?url=/downloads/sample.asp?url=/msdn-files/027/000/976/msdncompositedoc.xml&frame=true

  .NET Framework Service Pack 2

  http://msdn.microsoft.com/netframework/downloads/sp/download.asp

  ms cli

  http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dndotnet/html/mssharsourcecli2.asp

  mono

  http://www.go-mono.com/index.html

  anakrino

  http://test.saurik.net/anakrino/
  
<待续>

为了论坛节省空间,给个连接:http://www.nsfocus.net/index.php?periodical=&kind_id=&keyword=MS.Net+CLR&act=magazine&do=search

[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 7
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回
//