首页 -> 安全研究
安全研究
绿盟月刊
绿盟安全月刊->第35期->技术专题
作者: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)等等表的结构,
以及与之配套的其它辅助表。
待续……
版权所有,未经许可,不得转载