文/玄魂
1.3.2 元数据
元数据是描述数据的数据。在CLR的上下文中,元数据表示由描述符组成的一套体系,这些操作符包括了在一个模块中被声明或引用的所有项。由于CLR模型是面向对象的,因此在元数据中描述的项是类和它们的成员,以及它们伴随着的特性、属性和关联。本节简单地介绍元数据,与原数据安全相关的内容会在后续章节中继续讲解,元数据的详细内容不在本书的论述范围之内。
元数据实际上是一块二进制数据,包含了三种表:定义表、引用表和清单表。
元数据定义表主要是模块定义、类型定义、方法定义、字段定义、事件定义、参数定义、属性定义等一系列定义表的集合。当编译器编译代码时,所有定义的内容都会生成对应的定义表。
元数据引用表用于记录编译器中源代码引用的类型、方法、字段、事件。常用的引用表如:AssemblyRef(程序集引用表)、ModuleRef(模块引用表)、TypeRef(类型引用表)等。
元数据清单表包含了组成程序集所需要的所有信息,同时包含了对其他程序集的引用信息。它明确地指出了哪些条目可以对外开放,哪些条目只可以在程序集内部进行访问。
下面通过经典的HelloWorld程序简要分析其中的元数据信息,如代码清单1-7所示。
代码清单1-7 HelloWorld程序代码
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace HelloWorld { class Program { static void Main(string[] args) { Console.WriteLine("Hello World!"); Console.Read(); } } }
下面使用反汇编工具ILDasm打开HelloWorld.exe,双击MANIFEST,图1-7为查看清单信息的截图。ILDasm的使用方法和参数说明请读者参考MSDN文档。
图1-7 查看程序清单信息
详细的清单信息如代码清单1-8所示。
代码清单1-8 HelloWorld.EXE的清单信息
// Metadata version: v4.0.21006 .assembly extern mscorlib { .publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) // .z\V.4.. .ver 4:0:0:0 } .assembly HelloWorld { ...... } .hash algorithm 0x00008004 .ver 1:0:0:0 .module HelloWorld.exe // MVID: {B8EB35DD-5AD2-402C-B422-AA63B0AACCFA} .imagebase 0x00400000 .file alignment 0x00000200 .stackreserve 0x00100000 .subsystem 0x0003 // WINDOWS_CUI .corflags 0x00000003 // ILONLY 32BITREQUIRED // Image base: 0x05C40000
程序比较简单,代码中包含了版本、外部引用等简单的信息。表1-2描述了示例中所使用的 HelloWord.exe 程序集的程序集清单中的各项指令。
表1-2 HelloWorld.exe指令说明
指令 |
说明 |
.assembly extern < assembly name > |
指定包含当前模块所引用项目的另一程序集(在此示例中为 mscorlib) |
.publickeytoken < token > |
指定所引用程序集的实际密钥的标记 |
.ver < version number > |
指定引用程序集的版本号 |
.assembly < assembly name > |
指定程序集名称 |
.hash algorithm < int32 value > |
指定使用的哈希算法 |
.module < file name > |
指定组成程序集的模块名称,在此示例中,程序集只包含一个文件 |
.subsystem < value > |
指定程序要求的应用程序环境。在此示例中,值 3 表示该可执行文件从控制台运行 |
.corflags |
当前是元数据中的一个保留字段 |
根据程序集的内容,程序集清单可包含许多不同的指令。有关程序集清单中指令的完整列表请读者参考相关文档,本例旨在抛砖引玉。若要查看完整的元数据信息,可以使用快捷键“Ctlr+M”,如图1-8所示。
图1-8 查看元数据信息
1.3.3 IL常用指令
为方便起见,还是以HelloWorld.exe为例讲解IL的相关内容。由于篇幅所限,关于IL的详细内容还请各位读者参考相关资料。图1-9为Main方法的IL代码。
图1-9 HelloWorld.exe Main方法的IL代码
在一个中间语言程序中,如果某一行以“.”开始,代表这是一个传输给汇编工具的指令;而没有以“.”开始的行是中间语言的代码。上图中.method是方法定义指令,定义了Main方法,参数在“()”中,IL代码在“{}”中。.entrypoint是入口指令,表明该方法是入口方法。.maxstack指定了最大栈的深度为8。下面的IL_n是代码标签,后面是IL代码。nop是空指令;ldstr指令向栈中压入字符串“Hello World!”;call指令调用静态方法Console.WriteLine(string)和Console.read();pop弹出栈顶的值;ret指令表示方法体的结束。IL支持“//”和“/* */”的注释方法。
提示 在中间语言中,如果需要调用一个方法,需要指定方法的全名,包括它的名称域(namespace)、类名、返回值类型和参数的数据类型。
表1-3列举了IL的其他一些常用指令,更多的指令可以查看IL指令表。
表1-3 IL常用指令
指令 |
描述 |
.assembly <程序集名称> {} |
设置程序集 |
ldc.i4.n |
把一个 32位的常量(n从0到8)装入堆栈 |
stloc.n |
把一个从堆栈中返回的值存入第n(n取0~8)个局部变量 |
add |
2个值相加。命令的参数必须在调用前装入堆栈,该函数从堆栈中移除参数并把运算后的结果压入堆栈 |
sub |
2个值相减 |
mul |
2个值相乘 |
newarr type |
生成一个元素类型为type 的数组。数组的大小必须在调用该命令前装入堆栈。该命令会把一个数组的引用装入堆栈 |
stelem.i4 |
给一个数组成员赋值。数组的引用、下标和值必须在调用该命令前装入堆栈 |
ldelema type |
把数组元素的地址装入堆栈。数组的引用和下标必须在调用该命令前装入堆栈。地址用来调用非静态函数 |
ldlen |
把数组的长度装入堆栈。数组的引用必须在调用该命令前装入堆栈 |
ldloca.s variable |
把变量的地址装入堆栈 |
ldc.i4.s value |
把一个Int32的常量装入堆栈(用于大于8位的数) |
conv.i4 |
把堆栈中值转换成Int32类型 |
call instance function(arguments) |
调用类的非静态函数 |
bge.s label |
跳转至label 如果value1≥value 2. Values 1和 2 必须在调用本命令前装入堆栈 |
br.s label |
跳转至label |
box value type |
把一个值类型转成一个Object,并把该Object的引用装入堆栈 |
blt.s label |
跳转至label 。如果value 1小于 value 2. Values 1 和 2 必须在调用本命令之前装入堆栈 |
ldelem.i4 |
把一个数组元素装入堆栈。数组引用和下标必须在调用本命令之前装入堆栈 |
ldarga.s argument |
把函数参数的地址装入堆栈 |
dup |
在堆栈上复制一个值 |
stind.i4 |
存储值的地址。地址和值必须在调用本命令之前装入堆栈 |
.field |
定义类成员。和关键字public、private、static等一起使用 |
stsfld static field |
用堆栈中的值替换静态字段的值 |
ldfld field |
把一个非静态字段装入堆栈。类实例的地址必须在调用本命令之前装入堆栈 |
ldarg.n |
把第n个参数装入堆栈。在非静态函数中,第0个参数是一个隐含的参数,代表this |
newobj constructor |
用构造函数constructor生成一个类的实例。构造函数的参数必须在调用本函数之前先装入堆栈。一个类的实例会被生成并装入堆栈 |
callvirt instance function |
调用一个对象的后期绑定方法 |
1.3.4 IL与代码验证
在将MSIL编译为本机代码的过程中,MSIL代码必须通过验证过程,除非管理员已经建立了允许代码跳过验证的安全策略。验证过程检查MSIL和元数据以确定代码是否是类型安全的,这意味着它仅访问已被授权访问的内存位置。类型安全帮助将对象彼此隔离,因而可以保护它们免遭无意或恶意的破坏。它还提供了对代码可以可靠地强制安全限制的保证。
运行库使用下列条件来验证代码是否为类型安全:
q 对类型的引用与被引用的类型严格兼容。
q 在对象上只调用正确定义的操作。
q 标识与声称的要求一致。
验证过程中检查 MSIL 代码,尝试确认该代码只能通过正确定义的类型访问内存位置和调用方法。例如,代码不允许以超出内存范围的方式来访问对象。另外,验证过程检查代码以确定 MSIL 是否已正确生成,这是因为不正确的 MSIL 会导致违反类型安全规则。验证过程通过正确定义的类型安全代码集,并且它只通过类型安全的代码。然而,由于验证过程存在一些限制,某些类型安全代码可能无法通过验证,而某些语言在设计上并不产生可验证的类型安全代码。如果安全策略要求提供类型安全代码,而该代码不能通过验证,则在运行该代码时将引发异常。
-------------------------注:本文摘抄自《.NET 安全揭秘》1.3节