Metadata探秘
一、初探MetaData
把支持CLR的编程语言(如C++/CLI、C#、VB等)编写的源代码文件通过微软的或者自己写的编译器可以编译为一个托管模块,它实际上是一个标准的PE文件,其结构可以参见深入了解CLR的加载过程一文。Metadata(元数据)与IL代码都存在于该PE文件的Sections中,Metadata与IL是同时生成且永远同步的,本文主要讨论Metadata的内容,并以如下代码为例:
namespace HelloWorld { using System; using System.Runtime.InteropServices; public class Hello { [DllImport("User32.dll")] public static extern int MessageBox(int a,string b,string c, int d); public static void Main() { int num = MessageBox(0, "2", "3", 4); Console.Write("Please enter your name: "); string str = Console.ReadLine(); num = MessageBox(0, str, "Welcome to use IL Assembly", 1); } } }
我认为可以将Metadata所包含的内容分为“宏观内容”和“微观内容”两部分。
宏观内容包含Manifest,它包含了如下信息:构成程序集的文件、依赖的外部Module或Assembly(包括对GAC中Assembly或者自定义Assembly的依赖,任何一个.Net程序都将依赖Mscorlib.dll,所以对该Assembly的应用一定会出现在Manifest中)、当前Module的标识(一个程序集至少要有一个Module)、程序集的public key、image base等等,如图所示:
图1 Manifest 内容
微观内容包括类型、方法、特性(Attribute),例如对于类型而言,包括类型名、类型所包含的字段和方法、每个方法的参数等等详细信息。
二、分析Metadata
从逻辑上来说,Metadata由若干Stream组成,这些Stream可以分为两类:Metadata堆和Metadata表。
Metadata堆包含:
1) String堆:存储UTF-8编码的且以0结尾的字符串,堆中第一个字符串为空字符串,所以堆的第一个字节存储的一定是0,而堆的最后一个字节也一定是0;
2) GUID堆:存储16个字节的二进制对象,因其长度固定,所以不需要额外字节来标识其长度或者结束位置;
3) Blob堆:存储任意大小二进制对象,对齐方式如下:设对象长度(是一个无符号整型)为length,如果length<=0x7F则用一个字节来存储,如果0x80<=length<=0x3FFF则用2个字节来存储,如果0x4000<=length<=0x1FFFFFFF则用4个字节来存储;
我们可以直观的从下面的图中得到Stream的信息:用ILDASM打开上述代码生成的程序集。
1、在菜单“视图(View)”选项中单击“标头(Header)”选项,如图:
图2 元数据头
2、在菜单“视图(View)”中单击“元信息(MetaInfo)”选项并在选中“原始:标头,架构,行(Raw:Header,Schema,Rows)”和“原始:堆(Raw:Heaps)”,然后单击“显示(Show!)”,如图:
图3 Metadata信息
图4 Metadata信息
3、用UltraEdit 打开上述程序集,如图
图5 PE文件信息
从图2中可以看到:Metadata头包含两个部分:Storage signature 和 Storage header。
Storage signature结构为:
1) 签名(Signature),类型为DWORD,值为0x424a5342,这个值存在哪儿呢?利用图3可以从图5找到Metadata头的起始地址为0x00001098,从0x00001098到0x0000109b可以找到这个值;
2) 主版本(Major Version),类型为WORD;
3) 次版本(Minor Version),类型为WORD;
4) Extra Data Offset,保留字段,类型为DWORD,值为0;
5) 版本字符串长度,类型为DWORD;
6) 版本字符串,类型为BYTE数组,如当前版本为v2.0.50727。
Storage header结构为:
1) Flags,保留字段,类型为BYTE值为0;
2) Stream的个数,类型为WORD,当前PE文件中共有5个Stream。
接下来就是Stream头,可以看到这个PE文件包含5个Stream:#~Stream、#Strings Stream、#US Stream、#GUID Stream、#Blob Stream:
表1 Stream头
Offset |
Size |
Name |
0x0000006C (108) |
0x00000150 (336) |
#~ |
0x000001BC (444) |
0x00000148 (328) |
#Strings |
0x00000304 (772) |
0x00000074 (116) |
#US |
0x00000378 (888) |
0x00000010 (16) |
#GUID |
0x00000388 (904) |
0x0000006C (108) |
#Blob |
1、#Strings Stream
是一个String堆,从图5我们可看到Metadata起始地址为0x00001098,由表1可知#Strings流的偏移量为0x000001BC,所以我们到0x00001254地址去查看该Stream的内容,也可以直接通过ILDASM(图4)查看Stream内容,另外发现#Strings中存储的是元数据项的名字,并且以0开始以0结束,如表2:
表2 #Strings的内容
Offset |
Data |
0 |
|
1 |
<Module> |
10 |
HelloWorld.exe |
25 |
Hello |
31 |
HelloWorld |
42 |
mscorlib |
51 |
System |
58 |
Object |
65 |
MessageBox |
76 |
Main |
81 |
.ctor |
87 |
a |
89 |
b |
91 |
c |
93 |
d |
95 |
System.Diagnostics |
114 |
DebuggableAttribute |
134 |
DebuggingModes |
149 |
System.Runtime.CompilerServices |
181 |
CompilationRelaxationsAttribute |
213 |
RuntimeCompatibilityAttribute |
243 |
System.Runtime.InteropServices |
274 |
DllImportAttribute |
293 |
User32.dll |
304 |
Console |
312 |
Write |
318 |
ReadLine |
327 |
2、#US Stream
是一个Blob堆,可以存储用户自定义字符串或者二进制对象,在地址0x0000139C处看起,有表3内容:
表3 #US的内容
Offset |
Byte Length |
Data |
0 |
0x00 (0) |
|
1 |
0x03 (3) |
2 |
5 |
0x03 (3) |
3 |
9 |
0x31 (49) |
Please enter your name: |
59 |
0x35 (53) |
Welcome to use IL Assembly |
113 |
0x00 (0) |
|
114 |
0x00 (0) |
|
115 |
0x00 (0) |
3、#GUID
是一个GUID堆,按序存储“全球唯一标识符”,在地址处查看起,有表4内容:
表4 #GUID的内容
Offset |
Data |
0 |
{ec04bb0c-8238-4d78-b80e-4415e508b3b5} |
4、#Blob Stream
是一个Blob堆,存储Metadata中的内部二进制对象,例如,图1中定义对外部程序集mscorlib.dll的引用时,.publickeytoken的默认值为(B7 7A 5C 56 19 34 E0 89),这个默认值就存储在#Blob Stream中。内容如表5:
表5 #Blob的内容
Offset |
Byte Length |
Data |
0 |
0x00 (0) |
|
1 |
0x08 (8) |
B7-7A-5C-56-19-34-E0-89 |
10 |
0x07 (7) |
00-04-08-08-0E-0E-08 |
18 |
0x03 (3) |
00-00-01 |
22 |
0x03 (3) |
20-00-01 |
26 |
0x05 (5) |
20-01-01-11-0D |
32 |
0x04 (4) |
20-01-01-08 |
37 |
0x04 (4) |
20-01-01-0E |
42 |
0x04 (4) |
00-01-01-0E |
47 |
0x03 (3) |
00-00-0E |
51 |
0x04 (4) |
07-02-08-0E |
56 |
0x08 (8) |
01-00-07-01-00-00-00-00 |
65 |
0x08 (8) |
01-00-08-00-00-00-00-00 |
74 |
0x1E (30) |
01-00-01-00-54-02-16-57-72-61-70-4E-6F-6E-45-78-63-65-70-74-69-6F-6E-54-68-72-6F-77-73-01 |
105 |
0x00 (0) |
|
106 |
0x00 (0) |
|
107 |
0x00 (0) |
5、#~ Stream、
可以划分为两个部分:头(Header)和Metadata Table。
(1) 头(Header)
从图3可以看到有这么一段:Metadata header: 2.0, heaps: 0x00, rid: 0x01, valid: 0x0000000914021547, sorted: 0x000016003301fa00,我们也可以到地址0x00001098 + 0x0000006c = 0x00001104处查看内容,如下:
图6 Header
实际上Header由以下几个部分组成:
1)、4字节大小的保留字段,值总为0;
2)、1字节大小的主版本字段(Table Schema的版本,应该是跟随着CLR的版本);
3)、1字节大小的次版本字段;
4)、1字节大小的heap sizes,为0表示堆的索引大小为2字节;
5)、1字节大小的保留字段,值总为1;
6)、8字节大小的掩码串,相应位置为1,表示该Metadata Table有效;
7)、8字节大小的掩码串,相应位置为1,表示该表为需要按照主键排序的表,说明如表6:
表6 Sorted Metadata Table
Table |
Primary key |
Secondary key |
ClassLayout |
Parent |
|
Constant |
Parent |
|
CustomAttribute |
Parent |
|
DeclSecurity |
Parent |
|
FieldLayout |
Field |
|
FieldMarshal |
Parent |
|
FieldRVA |
Field |
|
GenericParam |
Owner |
Number column |
GenericParamConstraint |
Owner |
|
ImplMap |
MemberForwarded |
|
InterfaceImpl |
Class |
Interface column |
MethodImpl |
Class |
|
MethodSemantics |
Association |
|
NestedClass |
NestedClass |
另外代码中,父类在TypeDef表中记录的索引号一定比子类在TypeDef表中记录的索引小。(父类定义在子类定义前面)
8)、n个4字节大小的无符号整型(n为有效Metadata Table的个数),表示有效Metadata Table中的记录记录数分别是多少,上述内容反映到表7中:
表7 #~ Stream的Header的内容
Field |
Value |
Reserved |
0x00000000 (0) |
Major |
0x02 (2) |
Minor |
0x00 (0) |
HeapSizes |
0x00 (0) |
Reserved |
0x01 (1) |
MaskValid |
0x0000000914021547 ( 0000 0000 0000 0000 0000 0000 0000 1001 0001 0100 0000 0010 0001 0101 0100 0111 ) |
Sorted |
0x000016003301FA00 ( 0000 0000 0000 0000 0001 0110 0000 0000 0011 0011 0000 0001 1111 1010 0000 0000 ) |
Rows |
1, 7, 2, 3, 4, 7, 3, 1, 1, 1, 1, 1 |
紧接着Header的就是Metadata Table,2.0的Metadata Table一共有45个,按先后顺序反映在表8中,详细的说明可以参考Ecma-335:
Token |
Name |
0x00 |
Module |
0x01 |
TypeRef |
0x02 |
TypeDef |
0x03 |
FieldPtr |
0x04 |
Field |
0x05 |
MethodPtr |
0x06 |
MethodDef |
0x07 |
ParamPtr |
0x08 |
Param |
0x09 |
InterfaceImpl |
0x0A |
MemberRef |
0x0B |
Constant |
0x0C |
CustomAttribute |
0x0D |
FieldMarshal |
0x0E |
DeclSecurity |
0x0F |
ClassLayout |
0x10 |
FieldLayout |
0x11 |
StandAloneSig |
0x12 |
EventMap |
0x13 |
EventPtr |
0x14 |
Event |
0x15 |
PropertyMap |
0x16 |
PropertyPtr |
0x17 |
Property |
0x18 |
MethodSemantics |
0x19 |
MethodImpl |
0x1A |
ModuleRef |
0x1B |
TypeSpec |
0x1C |
ImplMap |
0x1D |
FieldRva |
0x1E |
EncLog |
0x1F |
EncMap |
0x20 |
Assembly |
0x21 |
AssemblyProcessor |
0x22 |
AssemblyOS |
0x23 |
AssemblyRef |
0x24 |
AssemblyRefProcessor |
0x25 |
AssemblyRefOS |
0x26 |
File |
0x27 |
ExportedType |
0x28 |
ManifestResource |
0x29 |
NestedClass |
0x2A |
GenericParam |
0x2B |
MethodSpec |
0x2C |
GenericParamConstraint |
6、以Hello类型为例,分析部分关键Metadata Table间的关系:
图7 Metadata Table
三、推荐资料
1、ECMA-335:http://www.ecma-international.org/publications/standards/Ecma-355.htm;
2、《.NET IL Assembler》,作者:Serge Lidin;
3、http://msdn.microsoft.com/zh-tw/library/dd229216.aspx,作者:蔡學鏞
posted on 2009-10-18 00:42 Leo Zhang 阅读(2507) 评论(8) 编辑 收藏 举报