首页 -> 安全研究

安全研究

绿盟月刊
绿盟安全月刊->第35期->技术专题
期刊号: 类型: 关键词:
MS.Net CLR扩展PE结构分析 (2)

作者:Flier Lu <mailto:flier@nsfocus.com>
主页:http://www.nsfocus.com
日期:2002-09-16

6.Metadata 的表结构

  在上期中我们分析到Metadata的流结构,提到#~流中以表形式保存着几乎所有
Metadata的重要信息。这一期让我们一起来看看#~流中到底有些什么。

6.1 表的组织结构

  在分析#~流时,我们了解到#~流头中两个Int64类型字段Valid和Sortd,
以位图形式表示当前#~流中有那些类型的表存在和已排序。因而我们可以先计算
Valid有多少位被设置为1,然后计算表在#~流中的实际偏移,代码如下

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;

  实现上,对每个类型的表,可以通过ValidTableMapping数组,将之映射到一个TJclClrTableClass
的元类型数组(Delphi中通过RTTI提供的Type of Type),然后建立新的对象分析Metadata表。
如对ttMethodDef($06)映射到TJclClrTableMethodDef类,实际解析表的代码存在于
TJclClrTable的子类中。因为每个表都可以看作若干行(row)组成,所以抽象出基类TJclClrTableRow
表示#~流中每一行(row)的数据。

  ValidTableMapping: array[TJclClrTableKind] of TJclClrTableClass = (
    TJclClrTableModule,               //  $00 ttModule
    TJclClrTableTypeRef,              //  $01 ttTypeRef
    ...
    TJclClrTableTypeTyPar,            //  $2A ttTypeTyPar
    TJclClrTableMethodTyPar);         //  $2B ttMethodTyPar

下面我们来看看#~流中不同表所起的作用与意义

6.2 Assembly 相关

  在Assembly中最高级别的信息是关于Assembly的信息,相关表有ttAssembly($20),
ttAssemblyProcessor($21), ttAssemblyOS($22), ttAssemblyRef($23),
ttAssemblyRefProcessor($24), ttAssemblyRefOS($25)六种表。其中ttAssembly和
ttAssemblyRef是使用最多的,其它几种因为目前CLR不强调跨平台性,故而使用较少。
  其次是关于File和Module一级的信息。每个Assembly可以有多个File和多个Module,
File分为有Metadata和没有Metadata两类,前者如IL代码所在,后者包括资源文件等等。
  此外每个Assembly有一个唯一的Manifest,维护Assembly的命名数据列表。

6.2.1 ttAssembly($20)

  首先来看看每个Assembly必须有的ttAssembly($20)表。

constructor TJclClrTableAssemblyRow.Create(const ATable: TJclClrTable);
begin
  inherited;

  FHashAlgId       := Table.ReadDWord;

  FMajorVersion    := Table.ReadWord;
  FMinorVersion    := Table.ReadWord;
  FBuildNumber     := Table.ReadWord;
  FRevisionNumber  := Table.ReadWord;

  FFlagMask        := Table.ReadDWord;

  FPublicKeyOffset := Table.ReadIndex(hkBlob);
  FNameOffset      := Table.ReadIndex(hkString);
  FCultureOffset   := Table.ReadIndex(hkString);
end;

  每个ttAssembly表项(很多表有且只能有一项,如ttAssembly表),首先是一个双字的Hash
算法编号,如None(0), MD5($8003), SHA1($8004),此hash算法编号可通过IL汇编指令
.hash algorithm 指定,说明Assembly中hash操作使用的算法。算法的编号是MS CryptAPI
中定义的,可在较新的Platform SDK的WinCrypt.h中找到完整定义,如

#define ALG_CLASS_HASH                  (4 << 13)
#define ALG_TYPE_ANY                    (0)
#define ALG_SID_MD5                     3
#define CALG_MD5 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_MD5) // $8003

  接着的四个字表示此Assembly的版本号,如wsdl.exe的版本号为1.0.3300.0
  接着的一个双字是Assembly的全局标志,我们等会再详谈
  最后是Public Key在#Blob堆中的索引(偏移)、Assembly名字在#String流中的索引(偏移)
和Culture在#String流中的索引(偏移)。
  这里的Public Key是用于assembly的完整性及发布者验证;Name是Assembly的名字;
Culture是此Assembly所支持的语言,可为中立"neutral"或其他符合RFC1766的语言名称如"en-US"
  注意这里的三个索引都不是定长,是根据metadata相关字段推算出来的,因而使用Table.ReadIndex
函数进行读取,代码如下

function TJclClrTable.ReadIndex(const HeapKind: TJclClrHeapKind): DWORD;
begin
  if IsWideIndex(HeapKind) then // 判断堆是否使用双字做索引
    Result := ReadDWord
  else
    Result := ReadWord;
end;

function TJclClrTable.IsWideIndex(const HeapKind: TJclClrHeapKind): Boolean;
begin
  Result := Stream.BigHeap[HeapKind];
end;

function TJclClrTableStream.GetBigHeap(const AHeapKind: TJclClrHeapKind): Boolean;
const
  HeapSizesMapping: array[TJclClrHeapKind] of DWORD = (1, 2, 4);
begin
  Result := (Header.HeapSizes and HeapSizesMapping[AHeapKind]) <> 0;
end;

  上一期我们曾经提到过,TClrTableStreamHeader.BigHeap表示几个内建流的索引大小。
而对表的索引,也是采用类似的编码方式,解码代码如下

function TJclClrTable.ReadIndex(const TableKinds: array of TJclClrTableKind): DWORD;
begin
  if IsWideIndex(TableKinds) then
    Result := ReadDWord
  else
    Result := ReadWord;
end;

function TJclClrTable.IsWideIndex(const TableKinds: array of TJclClrTableKind): Boolean;
var
  I: Integer;
  ATable: TJclClrTable;
begin
  Result := False;
  for I:=Low(TableKinds) to High(TableKinds) do
    if Stream.FindTable(TableKinds[I], ATable) then
      Result := Result or (ATable.RowCount > MAXWORD);
end;

  通过检测索引可能引用的几个表的大小是否超过MAXWORD,如超过则使用DWORD作为索引。
  最后我们看看Assembly的几个标志位的意义

// Assembly attr bits, used by DefineAssembly.
typedef enum CorAssemblyFlags
{
  afPublicKey             =   0x0001,     // The assembly ref holds the full (unhashed) public key.

  afCompatibilityMask     =   0x0070,
  afSideBySideCompatible  =   0x0000,     // The assembly is side by side compatible.
  afNonSideBySideAppDomain=   0x0010,     // The assembly cannot execute with other versions if
                                            // they are executing in the same application domain.
  afNonSideBySideProcess  =   0x0020,     // The assembly cannot execute with other versions if
                                            // they are executing in the same process.
  afNonSideBySideMachine  =   0x0030,     // The assembly cannot execute with other versions if
                                            // they are executing on the same machine.

  afEnableJITcompileTracking  =   0x8000, // From "DebuggableAttribute".
  afDisableJITcompileOptimizer=   0x4000, // From "DebuggableAttribute".

} CorAssemblyFlags;

  afPublicKey定义此Assembly包含未Hash处理的完整public key
通过此public key可以对assembly的完整性和发布者进行验证。
  接下来的五个标志定义此Assembly在AppDomain中被载入时的兼容性行为。
  传统的使用DLL的程序很容易因为DLL版本号不同造成冲突,也就是我们熟知的"DLL Hell"
COM通过注册表保存IID定位,部分解决此问题,CLR则使用gac(Global Assembly Cache)
完全解决了此问题。gac将对提交的每个版本的assembly保存唯一的备份,
在使用assembly时可以静态、动态指定版本号,彻底避免不同版本之间的冲突。
同时CLR还允许多个不同版本的Assembly同时使用,也就是Side By Side模式。
Assembly的标志位定义了此Assembly在Side By Side模式运行时的限制。
afNonSideBySideMachine(0x0030)要求一台机器同时只能有一个版本执行;
afNonSideBySideProcess(0x0020)要求一个进程中同时只能有一个版本执行;
afNonSideBySideAppDomain(0x0010)要求一个AppDomain中同时只能有一个版本执行。
afSideBySideCompatible(0x0000)则说明此Assembly可任意使用于Side by Side模式。
  注意这里的AppDomain是CLR提出的一个介于Process和Thread之间的逻辑单元。
兼有Process的安全性、独立性、稳定性和Thread的轻便、资源共享的优点
Assembly在AppDomain中载入执行,一个Process可以有多个AppDomain,
逻辑线程存在于AppDomain中,物理线程则可跨AppDomain使用等等。
  剩余的两个标志用于JIT编译器调试、跟踪和优化使用,略去。
  下面是演示程序对wsdl.exe的ttAssembly表分析的结果:

Name: wsdl
Version: 1.0.3300.0
Flag: cafPublicKey cafSideBySideCompatible
Hash Algorithm: SHA1
Public Key:
00001AE5: 00 24 00 00 04 80 00 00 ; ........
...(略去)...
00001B7D: 6D C0 93 34 4D 5A D2 93 ; m..4MZ..

  或者用IL格式代码表示为

.assembly /*20000001*/ wsdl
{
  .publickey = (00 24 00 00 04 80 00 00 94 00 00 00 06 02 00 00   // .$..............
          ... (略去) ...
                26 1C 8A 12 43 65 18 20 6D C0 93 34 4D 5A D2 93 ) // &...Ce. m..4MZ..

  .hash algorithm 0x00008004
  .ver 1.0.3300.0
}


6.2.2 ttAssemblyRef($23)

  ttAssemblyRef表和ttAssembly表结构非常类似,只不过是用于Assembly引用声明
类似于原PE结构中的Import节中对DLL引用的声明。

constructor TJclClrTableAssemblyRefRow.Create(const ATable: TJclClrTable);
begin
  inherited;

  FMajorVersion           := Table.ReadWord;
  FMinorVersion           := Table.ReadWord;
  FBuildNumber            := Table.ReadWord;
  FRevisionNumber         := Table.ReadWord;

  FFlagMask               := Table.ReadDWord;

  FPublicKeyOrTokenOffset := Table.ReadIndex(hkBlob);
  FNameOffset             := Table.ReadIndex(hkString);
  FCultureOffset          := Table.ReadIndex(hkString);
  FHashValueOffsetOffset  := Table.ReadIndex(hkBlob);
end;

  首先是四个字的版本号,接着是引用的Assembly的标志,但此标志只有一位
afPublicKey(0x0001)有效,其它位应该为0。此外PublicKeyOrToken
是一个#Blob流索引,一般指向引用的Assembly的public key的hash值,
如Flag被设置afPublicKey标志则保存完整的public key(很少)。
  ttAssemblyRef表可以使用IL汇编的.assembly extern指令指定,如

.assembly extern /*23000001*/ mscorlib
{
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                         // .z\V.4..
  .ver 1:0:3300:0
}

6.2.3 ttAssemblyProcessor($21) 和 ttAssemblyRefProcessor($24)
      ttAssemblyOS($22) 和 ttAssemblyRefOS($25)

  ttAssemblyProcessor表只有一个字段Processor表示Assembly所适用的处理器类型。
  ttAssemblyRefProcessor表比ttAssemblyProcessor表多了一个AssemblyRef表的索引
表示指定AssemblyRef表项的引用Assembly所适用的处理器类型。

  ttAssemblyOS表有三个DWORD字段,分别表示此Assembly所适用的平台及平台的主副版本号。
  ttAssemblyRefOS表比ttAssemblyOS表多一个AssemblyRef表的索引
表示指定AssemblyRef表项的引用Assembly所适用的操作系统信息。

  这四个表都是为CLR以后的跨平台实现设计的,目前都没有使用到。

6.2.4 ttFile($26)

  ttFile表定义了此Assembly所包含的文件列表。
  与传统DLL不同,Assembly只是一个逻辑单位,可以有一系列物理上独立的单元组成,
如一个或多个IL代码模块,任意个资源模块等等。

constructor TJclClrTableFileRow.Create(const ATable: TJclClrTable);
begin
  inherited;

  FFlags           := Table.ReadDWord;
  FNameOffset      := Table.ReadIndex(hkString);
  FHashValueOffset := Table.ReadIndex(hkBlob);
end;

  ttFile表项包括一个双字的标志,和文件名字、Hash值偏移索引。

// File attr bits, used by DefineFile.
typedef enum CorFileFlags
{
    ffContainsMetaData      =   0x0000,     // This is not a resource file
    ffContainsNoMetaData    =   0x0001,     // This is a resource file or other non-metadata-containing file
} CorFileFlags;

  Flags标志定义此File是否包含Metadata,如有Metadata则在载入此Module时,
系统Module Loader必须完成完整性、类型安全、权限等等一系列验证,而如果没有,
则作为资源文件直接载入,载入效率大大提高。

6.2.5 ttManifestResource($28)

  ttManifestResource表定义此Assembly包含的信息列表所在。

constructor TJclClrTableManifestResourceRow.Create(
  const ATable: TJclClrTable);
begin
  inherited;

  FOffset            := Table.ReadDWord;
  FFlags             := Table.ReadDWord;
  FNameOffset        := Table.ReadIndex(hkString);
  FImplementationIdx := Table.ReadIndex([ttFile, ttAssemblyRef]);
end;

  对单个物理文件的Assembly来说,Manifest一般为空,仅有一个自动生成
或以.mresource指令定义的名称,如

.mresource /*28000001*/ public WsdlRes.resources
(
)

  对于多个物理文件的Assembly来说,Manifest可以通过ImplementationIdx指出
实际的Assembly定义信息在引用的Assembly或者在指定File的指定Offset上。

  .assembly extern <dottedname>
  .file <dottedname> at <int32>

  Manifest可以为公开public或者私有private,同时可以指定Manifest相关属性。

6.2.6 ttModule($00) 和 ttModuleRef($1a)

  ttModule定义此文件的模块名称,缺省名称与可执行文件名相同

constructor TJclClrTableModuleRow.Create(const ATable: TJclClrTable);
begin
  inherited;

  FGeneration   := Table.ReadWord;            // Generation (reserved, shall be zero)
  FNameOffset   := Table.ReadIndex(hkString); // Name (index into String heap)
  FMvidIdx      := Table.ReadIndex(hkGuid);   // Mvid (index into Guid heap)
  FEncIdIdx     := Table.ReadIndex(hkGuid);   // Mvid (index into Guid heap)
  FEncBaseIdIdx := Table.ReadIndex(hkGuid);   // Mvid (index into Guid heap)
end;

  Mvid是一个GUID表示此Module,其它域保留。反编译为IL代码为

.module wsdl.exe // MVID:{FBD1DD82-477A-4F2A-985D-347226229D8C}

  ttModuleRef则只是指定引用的Module的名称而已

constructor TJclClrTableModuleRefRow.Create(const ATable: TJclClrTable);
begin
  inherited;

  FNameOffset := Table.ReadIndex(hkString);
end;

6.3 类型定义

  Metadata中的类型一级的定义,基本上是围绕着TypeDef表的定义组织的。
每个TypeDef表项定义一个类或一个接口,每个类可以继承且仅可继承一个在
TypeDef、TypeRef或TypeSpec表中定义的父类,但可以通过InterfaceImpl
表的定义实现多个接口。

6.3.1 ttTypeDef ($02)

  ttTypeDef表是定义类型的所在。

constructor TJclClrTableTypeDefRow.Create(const ATable: TJclClrTable);
begin
  inherited;

  FFlags            := Table.ReadDWord;
  FNameOffset       := Table.ReadIndex(hkString);
  FNamespaceOffset  := Table.ReadIndex(hkString);
  FExtendsIdx       := Table.ReadIndex([ttTypeDef, ttTypeRef, ttTypeSpec]);
  FFieldListIdx     := Table.ReadIndex([ttFieldDef]);
  FMethodListIdx    := Table.ReadIndex([ttMethodDef]);

  FFields           := nil;
  FMethods          := nil;
end;

  Flags字段是类型标志所在,等会再详细解释。NameOffset和NamespaceOffset定义此类型
的名称和所在命名空间,ExtendsIdx是指向TypeDef、TypeRef和TypeSpec表的索引,表示此
类型继承自的父类,而FieldList和MethodList则是定义字段和方法。
  接下来我们来详细分析一下各个字段的意义和使用方法
  首先是Flags字段:

    tdVisibilityMask        =   0x00000007,
    tdNotPublic             =   0x00000000,     // Class is not public scope.
    tdPublic                =   0x00000001,     // Class is public scope.
    tdNestedPublic          =   0x00000002,     // Class is nested with public visibility.
    tdNestedPrivate         =   0x00000003,     // Class is nested with private visibility.
    tdNestedFamily          =   0x00000004,     // Class is nested with family visibility.
    tdNestedAssembly        =   0x00000005,     // Class is nested with assembly visibility.
    tdNestedFamANDAssem     =   0x00000006,     // Class is nested with family and assembly visibility.
    tdNestedFamORAssem      =   0x00000007,     // Class is nested with family or assembly visibility.

  CLR中的类型可见性有几个级别,private(NotPublic)、protected(Family)和public
类似于C++中的相应概念,而Assembly(internal)则是CLR中引入的仅对本Assembly中其它类型
可见的新概念,将其可见范围控制在可信的公开域中,使用非常方便。

    tdLayoutMask            =   0x00000018,
    tdAutoLayout            =   0x00000000,     // Class fields are auto-laid out
    tdSequentialLayout      =   0x00000008,     // Class fields are laid out sequentially
    tdExplicitLayout        =   0x00000010,     // Layout is supplied explicitly

  结构布局是CLR为了兼容现有代码和实现跨语言类型定义的新特性。它可以控制类型的数据成员
在内存中的物理组织布局。一般通过StructLayoutAttribute特性定义在类和类型前,限定其
布局方式。缺省状态下是auto由CLR以运行效率最高为目标自行决定;Sequential模式下将根据
StructLayoutAttribute.Pack字段指定的字节对齐方式,以定义顺序排列字段,可指定1, 2,
4, 8, 16, 32, 64或128字节对齐;Explicit模式则由开发者使用FieldOffsetAttribute特性
逐个指定字段的物理位置,如

[StructLayout(LayoutKind.Explicit)]
public struct Rect
{
   [FieldOffset(0)] public int left;
   [FieldOffset(4)] public int top;
   [FieldOffset(8)] public int right;
   [FieldOffset(12)] public int bottom;
}

  这样一来开发人员可以做到最大限度兼容现有代码,在与其它非CLR系统的语言进行通讯时也更方便。
此外通过此特性,也可以模拟诸如Pascal语言中的union之类的CLR不支持的特性。

    tdClassSemanticsMask    =   0x00000020,
    tdClass                 =   0x00000000,     // Type is a class.
    tdInterface             =   0x00000020,     // Type is an interface.

  TypeDef表的每个项目可以定义一个类Class或一个接口Interface,实现上就是通过此标志位区分

    tdAbstract              =   0x00000080,     // Class is abstract
    tdSealed                =   0x00000100,     // Class is concrete and may not be extended
    tdSpecialName           =   0x00000400,     // Class name is special.  Name describes how.

  与C++中以=0后缀定义纯虚函数不同,CLR中引入了abstract关键字,但与Object Pascal中的
abstract不同,CLR中的abstract关键字可以引用于方法和类两个层面,进而可以更灵活地控制类的性质。
Sealed关键字则是CLR引入的新关键字,限定此类不能被继承,这个关键字非常好用,不像C++中得使用
特殊技巧来实现相似的效果。

    tdImport                =   0x00001000,     // Class / interface is imported
    tdSerializable          =   0x00002000,     // The class is Serializable.

  Import类是使用诸如TlbImp.exe之类的工具,直接由COM类型库导入的包装类,其类只是作为
与COM库代码进行交互的stub存在,并不提供实现代码。可通过对ImportedFromTypeLibAttribute
特性的检测来判定。
  Serializable类则是通过SerializableAttribute特性定义的可支持序列化操作的类。
对绝大部分类来说,CLR可以做到只需定义类支持序列化,就可以自动通过Reflection提供实现。
这正是由于CLR程序中强大的Metadata提供的支持才得以实现的。

    tdStringFormatMask      =   0x00030000,
    tdAnsiClass             =   0x00000000,     // LPTSTR is interpreted as ANSI in this class
    tdUnicodeClass          =   0x00010000,     // LPTSTR is interpreted as UNICODE
    tdAutoClass             =   0x00020000,     // LPTSTR is interpreted automatically

  在与现有DLL形式代码进行交互时,可以通过DllImportAttribute特性定义导入函数,
其中DllImportAttribute.CharSet字段定义了函数中使用字符串的形式。标准的Win32 API
一般都提供了A和W两种后缀的实现,前者是ANSI或者是WIDE的缩写,表示char和wchar_t两类
字符串。当定义字符串格式为auto时,CLR会自动判断使用宽窄字符,也可以用Ansi或Unicode指定。

    tdBeforeFieldInit       =   0x00100000,     // Initialize the class any time before first static field access.
    tdRTSpecialName         =   0x00000800,     // Runtime should check name encoding.
    tdHasSecurity           =   0x00040000,     // Class has security associate with it.

  其它几个标志使用机会不多,不再罗嗦了。
  ExtendsIdx是指向TypeDef、TypeRef和TypeSpec表的索引,定义此类型的父类。
对接口来说,此字段应为空,而是通过InterfaceImpl表实现多个接口;而对于类型来说,
除了System.Object以外,所有类型都必须是从其它类型继承而来的,而最终类型树归结到
System.Object类型上,因而CLR中的类型是类似Java/Delphi中的单根组织模式。
这样的组织模式,结构跟清晰,实现诸如序列化之类的特性也更容易,是语言发展的大趋势。
  值得注意的是这里有几个特殊限制,如凡是值类型都必须继承自System.ValueType,
所有枚举类型必须直接继承自System.Enum,所有delegate类型必须继承自
System.Delegate<=System.MulticastDelegate等等。这几种特殊形式继承的深度都
不能超过一层,这是在语言一级得到保障的。
  FieldListIdx和MethodListIdx分别是一个索引标记,定义在Field和Method表中
一个终止点。在对Field和Method表的项目进行分配时,实际上是根据这些终止点将表项
分派的,上一个终止点后,此终止点前的所有项目挂接在此类型上。实现代码类似如下

procedure TJclClrTableTypeDefRow.UpdateFields;
var
  FieldTable: TJclClrTableField;
  Idx, MaxFieldListIdx: DWORD;
begin
  with Table as TJclClrTableTypeDef do
  if not Assigned(FFields) and (FieldListIdx <> 0) and
     Stream.FindTable(ttFieldDef, TJclClrTable(FieldTable)) then
  begin
    if RowCount > (Index+1) then
      MaxFieldListIdx := Rows[Index+1].FieldListIdx-1
    else
      MaxFieldListIdx := FieldTable.RowCount;
    if (FieldListIdx-1) < MaxFieldListIdx then
    begin
      FFields := TList.Create;
      for Idx:=FieldListIdx-1 to MaxFieldListIdx-1 do
      begin
        FFields.Add(FieldTable.Rows[Idx]);
        FieldTable.Rows[Idx].SetParentToken(Self);
      end;
    end;
  end;
end;

6.3.2 ttTypeRef ($01)

  与AssemblyRef表类似,TypeRef表是定义对其它类型的引用的,
每个项目定义一个类型,通过名字在目标范围中定位。

constructor TJclClrTableTypeRefRow.Create(const ATable: TJclClrTable);
begin
  inherited;

  FResolutionScopeIdx := Table.ReadIndex([ttModule, ttModuleRef, ttAssemblyRef, ttTypeRef]);
  FNameOffset         := Table.ReadIndex(hkString);
  FNamespaceOffset    := Table.ReadIndex(hkString);
end;

  ResolutionScopeIdx字段是指向ttModule, ttModuleRef,
ttAssemblyRef或ttTypeRef表的索引;Name和Namespace定义
类型的名称和命名空间。
  这里的ResolutionScopeIdx可以非常灵活地定义此引用类型的解析域。
对Token在AssemblyRef表的情况,此类型在指定的外部Assembly中实现;
对ModuleRef情况,此类型在同一Assembly但不同的Module中实现;
对Module情况,此类型在同一Assembly的同一Module中实现(很少见);
对TypeRef情况,此类型为嵌套类型,在指定的类型中被定义。最后还有
ResolutionScopeIdx为空的情况,则ExportedType表中会有一项
定义此TypeRef的类型被哪个File或Assembly实现。

6.3.3 ttExportedType ($27)

  ExportedType表用于在一个由多个Module组成的Assembly中,在一个Module里面
定义由其它Module定义、实现的公开类型。

constructor TJclClrTableExportedTypeRow.Create(const ATable: TJclClrTable);
begin
  inherited;

  FFlags               := Table.ReadDWord;
  FTypeDefIdx          := Table.ReadDWord;
  FTypeNameOffset      := Table.ReadIndex(hkString);
  FTypeNamespaceOffset := Table.ReadIndex(hkString);
  FImplementationIdx   := Table.ReadIndex([ttFile, ttExportedType]);
end;

  其Flags基本上与TypeDef的标志位相同;ImplementationIdx和TypeDefIdx组合
用于定位一个类型,前者指定类型实现的文件或再次指向一个ExportedType表项目(用于
嵌套类型定义),TypeDefIdx表示目标文件的Metadata的TypeDef表的索引号;Name和
Namespace定义名字和名称空间。

6.3.4 ttTypeSpec ($1B)

  在TypeDef表项的ExtendsIdx字段,指向的除了TypeDef和TypeRef表项,
还可以指向一个TypeSpec表项。

constructor TJclClrTableTypeSpecRow.Create(const ATable: TJclClrTable);
begin
  inherited;

  FSignatureOffset := Table.ReadIndex(hkBlob);
end;

  TypeSpec表可以说是Metadata中最简单或者说最复杂的表 :),简单是指其只有一个字段
指向一个二进制数据块;复杂则是指此数据块的定义可以根据编码非常复杂。
  这里就涉及到一个CLR中的Signature的概念。在传统语言如C++中,一个Signature往往
表示一个函数或类型,所有定义类型信息的集合,对函数而言,可能包括函数名、返回参数、
调用方式、参数个数、参数名称、参数类型等等一堆信息。在CLR中,通过将这些信息以有规律的
二进制编码进行压缩组织,组成所谓的Signature。你可以将之理解为一个小型的自有格式的
类型的Metadata信息块。
  举个简单的例子,在一个较简单的字段定义的Signature中:第一个字节是一个标志位,
表示此Signature是一个FieldSig(标志字节为 IMAGE_CEE_CS_CALLCONV_FIELD=6),
接着是两个可选字节定义CustomMod(custom modifier 等会详细介绍),然后是几个字节
定义字段的类型。
  对CustomMod来说,第一字节是CMOD_OPT($20)或CMOD_REQD($1F),前者说明此
CustomMod可忽略,后者则是必须的。然后跟着一个压缩了的Token指向TypeRef表项。
根据此表项,JIT可以做出定制的举措,如指向Microsoft.VisualC.IsConstModifier类型
表示此参数在方法中是const的;再比如可以使用此机制定义从被管理环境切换到本机代码环境后
调用方法的转换,如转为Cdecl、StdCall、ThisCall或者FastCall,JIT将根据此定义
自动生成转换调用方式的Thunk代码块。等等诸如此类应用可以无限扩充
  然后对于FieldSig中的Type部分,在一个类型字节后,根据类型可以跟不同的信息。
如对ValueType和Class类型,后面跟一个编码后的TypeDef和TypeRef表的索引值,
表示此字段实现的值类型或类的定义,而对Ptr或者FnPtr类型的定义更加复杂,这里限于
篇幅不再详述,有兴趣的朋友可以自行参看文档或来信于我探讨……:)
  这里值得一提的是,MS在文档中明确指出此TypeSpec将用于parametric polymorphism
的扩展,而且微软研究院最近也推出了基于开源CLI项目的支持泛型编程的C#编译器的原型。
因此可以预见在不远的将来,我们也可以在.Net语言中方便地使用泛型编程思想。
  注意这里的parametric polymorphism和C++中支持的Template和Preprocess技术
有所不同:预处理是语言细节无关的普遍性替换,说白了就是字符串替换,虽然可以用于实现
泛型编程思想,但因为其替换过程与语言本身脱离,造成使用的种种不便,被逐渐废弃;
模板技术现在是大放异彩,可以说是泛型编程思想的最好展示工具,但其设计思想是基于静态解析
容易造成代码容量增殖,且模板实例间联系松散,存在编写调试复杂化等等诸多问题;
而参量多态则是Java/C#此类单根语言的GP实现思路,因为其所有类型都源自Object,
因而只需编写、调试、保留一份代码,通过编译器静态或动态对不同参量类型使用相同代码,
代码大小和运行效率都得到保障。此是题外话,就此打住。 :)

6.3.5 ttInterfaceImpl ($09)

  前面在分析TypeDef表时,提到类型只能直接继承自一个父类,但可以通过接口映射表
实现多个接口;而接口类型不能直接继承其它类型,而是直接通过接口映射表实现接口。

constructor TJclClrTableInterfaceImplRow.Create(const ATable: TJclClrTable);
begin
  inherited;

  FClassIdx     := Table.ReadIndex([ttTypeDef]);
  FInterfaceIdx := Table.ReadIndex([ttTypeDef, ttTypeRef, ttTypeSpec]);
end;

  Class是TypeDef表的一个索引,而Interface则是TypeDef、TypeRef或TypeSpec表
的一个索引,这里InterfaceImpl表只是建立一个映射关系而已。

6.3.6 小结

  至此,类型一级的定义及实现基本上介绍完了,下一期将继续分析类型中的子元素,
字段(Field)、方法(Method)、属性(Property)和事件(Event)等等表的结构,
以及与之配套的其它辅助表。

待续……
版权所有,未经许可,不得转载