第一章 CLR的执行模型
1.1 将源代码编译成托管代码
1.2 将托管模块合并成程序集
1.3 加载公共语言运行时
1.4 执行程序集代码
?托管模块->程序集,区别
?如何通过清单、元数据找到相应文件及方法
?win32、win64应用程序区别
1.1 将源代码编译成托管代码
返回
公共语言运行时(Common Language Runtime):是一个由多种编程语言使用的“运行时”。
CLR核心功能包括:内存管理,程序集加载,安全性,异常处理,以及线程同步。
托管模块(managed module):是标准的PE32/32+文件(微软Windows可移植可执行文件,PE32表示32位,32+表示64位),它需要CLR执行。
图1 将源代码编译成托管代码
表1托管代码的各个组成部分
组词部分
说明
PE32头或PE32+头
标准的PE文件头,类似通用对象文件格式头(Common Object File Format,COFF).
CLR头
包含托管模块的信息:需要CLR的版本,一些标志信息,MethodDef元数据(获取托管模块入口方法(Main方法)),以及模块的元数据、资源、强命名。
元数据
每一个托管模块包含元数据表。有两种主要的类型:1.描述在源码中定义成员的类型。2.描述在源码中引入的成员类型
IL(中间语言)代码
编译器编译源码产生的代码,在运行时,CLR将IL编译为本地CPU指令
元数据由来:元数据是一些老技术的超级。这些老技术包括COM的“类型库”(Type Library)和“接口定义语言”(Interface Definition Language)文件。
与老技术区别:
(1)CLR元数据比他们完整
(2)元数据总是与IL代码的文件关联
元数据的用途:
(1)编译不需要依赖于原生C/C++头及库文件,因为可以直接从托管模块读取元数据
(2)智能提示
(3)CLR用元数据进行代码审核,确保类型安全
(4)允许序列化一个对象的字段为一个内存块,发送到其他机器,然后被反序列化,重建对象状态。
(5)允许GC跟踪对象的生命周期
1.2 将托管模块合并成程序集
返回
CLR不直接运行模块,而是程序集。程序集(Assmebly)是一个抽象的概念:
(1)程序集是一个或多个模块或资源文件的逻辑分组。
(2)程序集是重用,安全性,版本控制的最小单元
根据我们使用的编译器或工具,可以生成一个或多个文件的程序集(默认情况为一个文件)。在CLR世界里,一个程序集就是我们通常所说的“组件”。
图2将托管模块合并成程序集
一些托管模块和资源文件通过一个工具进行处理,生成一个代表文件逻辑分组的PE32(+)文件。这个PE32(+)文件包含了数据块——称为清单。
清单(manifest)是有元数据表构成的另一种集合,包括(这些信息【1】使得程序集能够自描述):
(1)信息 :说明
(2)程序集名称:指定程序集名称的文本字符串。
(3)版本号:主版本号和次版本号,以及修订号和内部版本号。公共语言运行库使用这些编号来强制实施版本策略。
(4)区域性:有关该程序集支持的区域性或语言的信息。此信息只应用于将一个程序集指定为包含特定区域性或特定语言信息的附属程序集。(具有区域性信息的程序集被自动假定为附属程序集。)
(5)强名称信息:如果已经为程序集提供了一个强名称,则为来自发行者的公钥。
(6)程序集中所有文件的列表: 在程序集中包含的每一文件的散列及文件名。请注意,构成程序集的所有文件所在的目录必须是包含该程序集清单的文件所在的目录。
(7)类型引用信息: 运行库用来将类型引用映射到包含其声明和实现的文件的信息。该信息用于从程序集导出的类型。
(8)有关被引用程序集的信息: 该程序集静态引用的其他程序集的列表。如果依赖的程序集具有强名称,则每一引用均包括该依赖程序集的名称、程序集元数据(版本、区域性、操作系统等)和公钥。
1.3 加载公共语言运行时
返回
CLR管理程序集中的代码的执行。这意味着主机上必须安装.NET Framework。
查看是否安装.Net Framework?
(1) 如果机器上安装了.NET Framework,则在%SystemRoot%\System32目录下将包含MSCorEE.dll文件。
(2) 如果一台机器上可以同时安装多个.NET Framework版本,查看注册表:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\NET Framework Setup\NDP
(3) .NET Framework SDK自带的命令行工具CLRVer.exe可以显示安装的所有CLR的版本。该工具还能列出进程所使用的CLR版本。
在极少数情况下,开发者会编写在特殊版本的Windows下运行的代码。通常是在使用unsafe code或需要与面向特定CPU架构的非托管代码进行互操作时。C#编译器提供了一个/platform命令行选项。该选项允许指定结果程序集是运行在x86机器(32位Windows)还是x64机器(64位Windows),或Intel Itanium机器(64位Windows)。如果没有指定平台,默认为anycpu。Visual Studio用户可以在项目属性页中设置项目所面向的平台。
图3使用Visual Studio设置目标平台
表2 /platform开关堆生成的模块的影响以及在运行时的影响
表3 MSCorEE.dll的存储位置
Windows版本
MSCorEE.dll版本
路径
x86
x86
C:\Windows\System32
x64或IA64
x86
C:\Windows\SysWow64
x64
C:\Windows\System32(为了向后兼容)
加载CLR步骤:
(1)在Windows检查EXE文件头并决定是创建32位、64位还是WoW64位进程
(2)Windows将x86、x64或IA64版本的MSCorEE.dll加载到进程的地址空间中
(3)进程的主线程调用MSCorEE.dll中定义的方法(参考22.1 CLR寄宿)
(4)该方法初始化CLR,加载EXE程序集,然后调用它的入口点方法(Main)
(可以在代码中使用Environment的Is64BitOperatingSystem或Is64BitProcess属性来检查是否运行在64位版本的Windows或运行在64位的地址空间中)
如果一个非托管应用程序调用LoadLibrary来加载托管的程序集,Windows将加载并初始化CLR(如果还未加载的话)。当然在该场景中,进程已经创建并运行,这将限制程序集的可用性。如,使用/platform:x86编译的托管程序集不能被64位的进程加载。
1.4 执行程序集代码
返回
IL(Intermediate Language)是独立于CPU的机器语言。可以将IL看成是一门面向对象的机器语言。
从图1,我们可以得知通过高级语言编译器,可以把高级语言C#,VB等编译成IL。ildasm 可以反编译出中间文件来,再用 ilasm 可以再将中间文件编译回 dll。
另外, C#仅仅暴露CLR提供的功能的一个子集,而IL汇编语言运行开发者访问所有CLR的功能。
要执行一个方法,它的IL必须首先转换为本地CPU指令。这是CLR的JIT编译器的工作。
下图展示了一个方法第一次被调用时的情况:
图4 方法的首次调用
在Main执行之前,CLR会检测出Main的代码所引用的所有类型。这导致CLR为每个类型分配一个内部数据结构,每个类型结构,都有方法表,方法表中每个数据项代表一个方法(详情参见4.4 运行时相互关系)。如上图,CLR为Console类创建了一个内部数据结构,方法表中有个数据项为WriteLine(string),它指向一个地址,该方法未被调用之前,他被初始化指向JITCompiler函数。
Main方法首次调用WriteLine方法时,JITCompiler函数会被调用。JITCompiler函数负责将一个方法的IL代码编译成本地代码,如上图所示。
Main第二次调用WriteLine是,由于已编译成本地代码,直接执行内存中的本地代码。如下图所示。
图5 方法的第二次调用
1.4.1 IL和验证
IL是基于堆栈的,这意味着它所有的指令都是将操作数(operands)压入执行栈(execution stack)中,或从栈中弹出结果。
IL指令还是无类型的。例如,IL提供了add指令,对堆栈中最后两个操作数进行相加。没有32位和64位指令的区别。
IL的最大好处不是对底层CPU的抽象,而是对程序健壮性和安全性的改善。在将IL编译为本地CPU指令时,CLR执行一个叫做验证(verification)的过程。verification检查高级别的IL代码,并确保它做的每件事都是安全的。例如,验证过程检查每个被调用的方法的参数数量是否无误、参数类型是否正确、方法的返回值是否可用、每个方法是否都包含return语句,等等。验证过程所使用的所有方法和类型的信息,都存在于托管模块的元数据中。
在Windows中,每个进程都有虚拟的地址空间。将每个Windows进程放置于分离的内存地址中,一个进程就不可能对另一个进程产生负面影响,从而换来健壮性和稳定性。通过验证的托管代码,可确保代码不会访问不正确的内存,不会干扰到另一个应用程序的代码,这样一来,可放心的将多个托管运用程序方到一个Windows虚拟空间中,即CLR提供了一个在单独的进程中执行多个托管程序的功能(详情看22章 CLR寄宿和AppDomain),节省了系统资源。
2.2 将类型生成到模块中
返回
文件Program.cs源代码:
复制代码
1 public class Program
2 {
3 public static void Main()
4 {
5 System.Console.WriteLine(“Hi”);
6 }
7 }
复制代码
csc命令
复制代码
//1.生成programe.exe, MSCorLib.dll包含所有默认核心类型,所以C#编译器会自动引用
csc.exe /out:Program.exe /t:exe /r:MSCorLib.dll Program.cs
//2.若不希望自动引用MSCorLib.dll,使用/nostdlib开关
csc.exe /out:Program.exe /t:exe /nostdlib Program.cs
//3.使用响应文件,把需要的设置放在响应文件中
csc.exe @MyProject.rsp Program.cs
//3.1 MyProject.rsp内容:
/out:Program.exe
/t:exe
/r:MSCorLib.dll
复制代码
响应文件
C#响应文件允许同时使用多个响应文件。除了命令显示指定的文件,编译器还会自动查找两个名为CSC.rsp的文件:
在当前目录;
在csc.exe文件所在目录(安装.NET Framework时,会在%SystemRoot%\Microsoft.NET\Framework\v.X.X.X)。
csc.exe所在目录的文件CSC.rsp会引用一些常用程序集,可能对编译器的速度有些影响。但如果源代码没有引用其中任何程序集中的类型或方法,就不会影响最终生成的程序集文件,也就不会影响程序执行性能。
注意:使用/reference编译器开关引用程序集时,可以指定文件完整路径。若不指定路径,编译器会按以下顺序搜索:
工作目录
csc.exe本身所在目录
使用/lib编译器开关指定的任何目录
使用LIB环境变量指定的任何目录
2.3 元数据概述
返回
表的种类有三种:定义表、引用表和清单(manifest)表
表1 定义表
名称
说明
ModuleDef
总是包含一个标识模块的记录项[1]。该记录项包含模块的文件名和扩展名(不包含路径),以及模块版本ID(下面代码中的MVID,编译器创建的GUID)。
TypeDef
模块中的每个类型在TypeDef表中都有一个对应的记录项。每个记录项都包含类型名、基类型、标记(public、private等)以及一些索引,这些索引指向该类型在MethodDef表、FieldDef表、Property表、EventDef表所拥有的成员。
MethodDef
模块中的每个方法在MethodDef表中都有一个对应的记录项。每个记录项都包含方法名、标记(private、public、virtual、abstract、static、final等)、签名以及方法的IL代码在模块中的偏移量(下面代码中的RVA,Relative Virtual Addresses,相对虚地址)。每个记录项还包含一个指向ParamDef表对应记录项的指针,这样可以找到有关方法参数的更多信息。 (另,CallCnvntn即指CallingConventions)
FieldDef
模块中的每个字段在FieldDef表中都有一个对应的记录项。每个记录项包含标记(private、public等)、类型和名称。
ParamDef
模块中的每个参数在ParamDef表中都有一个对应的记录项。每个记录项包含标记(in、out、retval等)、类型和名称。
PropertyDef
模块中的每个属性在PropertyDef表中都有一个对应的记录项。每个记录项包含标记、类型和名称。
EventDef
模块中的每个事件在EventDef表中都有一个对应的记录项。每个记录项包含标记和名称。
表2 引用表
名称
说明
AssemblyRef
模块中的每个引用的程序集在AssemblyRef表中都有一个对应的记录项。每个记录项包含绑定程序集所必要的信息:程序集名称(不包含路径和扩展名)、版本号、语言文化(culture)和公共密钥标记(通常由程序集发布者提供的公共密钥(标识被引用的程序集的发布者)生成的小的散列值。该散列值是被引用的程序集中的位的校验和。CLR完全忽略该散列值,并在以后也将继续如此。
ModuleRef
当前模块引用的类型可能由别的PE模块实现的,所有那些模块在这个表中都有一个对应记录项。每个记录项都包含模块的文件名和扩展名(不含路径)。该表用来绑定那些实现在相同程序集不同模块中的类型。
TypeRef
该模块所引用的每个类型都在TypeRef表中有一个对应的记录项。每个记录项包含了类型的名称和一个引用(指向类型的指针)。如果类型实现在另一个类型内部,那么该指针指向一个TypeRef记录项。如果该类型实现在同一个模块中或者实现在同一程序集的其他模块中,那么该指针指向一个ModuleDef记录项。如果类型实现在不同的程序集中,那么该指针指向一个AssemblyRef记录项
MemberRef
模块中所引用的每一个成员(字段、方法、属性方法和事件方法)都在这个表中有一个对应的记录项。每个记录项都包含成员的名称、签名以及指向成员所在类型的TypeRef表中对应记录项的指针。
表3 清单表
名称
说明
AssemblyDef
如果模块标识为一个程序集,那么它将在AssemblyDef表中有一个对应的记录项。该记录项包含程序集名称(不包含路径和扩展名)、版本号(主版本号、次版本号、生成版本号和修订版本号)、语言文化、一些标记、散列算法和发布者的公有密钥(可能为null)。
FileDef
程序集中所有PE文件和资源文件都在FileDef表中有一个对应的记录项(除了清单所在的文件,因为它对应的记录项在AssemblyDef表中)。该记录项包含文件名和扩展名(不包含路径)、散列值、一些标记。如果程序集只包含一个文件(即清单所在文件),那么FileDef表中没有任何记录项。
ManifestResourceDef
程序集中所有资源都在ManifestResourceDef表中有一个对应的记录项。该记录项包含资源名称、一些标记(public表示对程序集外部可见,private表示不可见)、和一个资源文件或流所在的文件在FileDef表中的索引。如果资源文件不是一个单独的文件(如jpg、gif),那么所谓资源就是指嵌入在PE文件中的流。对于嵌入的资源,该记录项还包含表示资源流在PE文件中起始位置的偏移量。
ExportedTypesDef
程序集中所有的PE模块导出的所有公有类型(就是程序集中定义的public类型,它们在程序集的外部可见),都在ExportedTypesDef表中有一个对应的记录项。该记录项版含类型名称、到FileDef表的索引(指明哪一个程序集文件实现了该类型)、到TypeDef表的索引。注意:为节省文件空间,从包含清单的文件导出的类型不包含在该表中,因为这些类型信息可以通过元数据的TypeDef表获得。
注意: 包含清单的程序集文件还有一个AssemblyRef表。程序集的所有文件所有所引用的所有程序集在这个表中都有一个对应记录项。
2.4 将模块合并成程序集
返回
多文件程序集的三点理由:
可用单独文件对类型进行划分,允许文件以“增量”的方式景象下载。将类型划分到不同的文件,还是我们能够对购买和安装的应用程序进行部分或分批打包/部署。
可在自己的程序集中添加资源或数据文件。
程序集包含的各种类型可以用不同的编程语言来实现。不同的语言写的类型放在不同模块中,然后,可以再合并成单个程序集。
将模块合并到程序集
//将不常用的类型编译到单独的模块中,创建一个名为RUT.netmodule的模块
csc /t:module RUT.cs
//将常用类型编译到另一个模块,该模块包含清单。并将RUT.betmodule视为程序集的一部分,RUT.betmodule会添加到FileDef清单元数据表中,它的公开导出的类型添加到ExportedTypesDef清单元数据表中
csc /out:JeffTypes.dll /t:library /addmodule:RUT.netmodule FUT.cs
RUT.cs和FUT.cs内容
复制代码
//RUT.cs content
using System;
public sealed class ARarelyUsedType {
public ARarelyUsedType() {
Console.WriteLine(“A rarely used type was constructed.”);
}
}
//FUT.cs content
using System;
public sealed class AFrequentlyUsedType {
public AFrequentlyUsedType() {
Console.WriteLine(“A frequently used type was constructed.”);
}
}
复制代码
图2-1 包含两个托管模块的一个多文件程序集,清单在其中的一个模块中
注意:清单源数据信息实际并不包含从清单所在的锁在的那个PE文件导出的类型。这是一项优化措施。
可以使用ILDAsm.exe检查元数据的清单表
复制代码
Token: 0x26000001
Name : RUT.netmodule
HashValue Blob : bc e9 d8 5c 51 06 69 c9 d7 3d d9 db c9 58 54 21 51 fb 8e 79
Flags : [ContainsMetaData] (00000000)
Token: 0x27000001
Name: ARarelyUsedType
Implementation token: 0x26000001
TypeDef token: 0x02000002
Flags : [Public] [AutoLayout] [Class] [Sealed] [AnsiClass] [BeforeFieldInit] (00100101)
复制代码
可以看出,RUT.netmodule文件已被视为程序集的一部分,它的token是0x26000001。ExprotedTypeDef表中也只有RUT的公开导出类型ARarelyUsedType。
注意:元数据token是一个4字节的值。其中高位字节指明token的类型(0x01=TypeRef, 0x02=TypeDef, 0x23=AssemblyRef, 0x26=FileDef, 0x27=ExportedType)。要取完整列表,请参见.net framework SDK包含的CorHdr.h文件中的CorTokenType枚举类型。token的三个低位字节表明对应的元数据表中的行。例如:0x26000001这个实现引用的是FileDef表的第一行,对于大多数表,行都是从1开始编号。但对于TypeDef表,行号从2开始。
对于引用多文件程序集,编译生成一个新的程序集时,所引用的程序集中所有文件都必须存在;运行程序的时候,并不要求被引用的所有程序集都存在,关键看使用的类或方法在那个文件中。
3.1 两种程序集,两种部署
返回
弱命名程序集和强命名程序集在结构上完全一致:相同的PE文件格式、PE32(+)头、CLR头、元数据、清单表以及IL。
区别在于:强命名程序集使用发布者的公钥/私钥进行了签名,它唯一标识了程序集的发布者。这对秘钥允许对程序集进行唯一性的标识、保护和版本控制,并允许程序集部署到用户机器的任何地方,甚至可以部署到Internet上。
表1 弱命名和强命名程序集的部署方式
程序集种类 私有部署 全局部署
强命名 可以 可以
弱命名 可以 不可以
3.2 为程序集分配强名称
返回
以前,当多个应用程序访问的程序集放在同一个目录中,当有版本修改,或同名程序集时,便无法控制,通常最后一个安装的程序集就会覆盖原先的程序集,着就是Windows的“DLL hell”现象的根源。
显然,只根据文件名来区分程序集时不够的。CLR必须提供对程序集程序集进行唯一标识的机制,这正是“强名称程序集”的来历。如果一个程序集有一个唯一的标记,那么这个程序集就可以叫做强命名程序集。在.NET框架中是通过公钥/私钥加密来产生这个唯一标记的。
一个强命名程序集具有4个重要attributes:文件名(不计扩展名)、版本号、语言文化(culture)以及一个公钥标记(是由公钥派生的哈希值,即公钥标记)。如下图,有3个不同的程序集:
3.3 全局程序集缓存
返回
GAC作用: 是可以存放一些有很多程序都要用到的公共Assembly,例如System.Data、System.Windows.Forms等等。这样,很多程序就可以从GAC里面取得Assembly,而不需要再把所有要用到的Assembly都拷贝到应用程序的执行目录下面。
GAC(Globle Assembly Cache)位于以下目录:
.NET 3.5和以前版本: C:\Windows\Assembly
.NET 4: C:\Windows\Microsoft.NET\Assembly
.NET 4.0 has a new GAC, why?
GAC目录是结构化的:其中包括许多子目录,从而保证不同的程序集在不同的目录下,永远不要将程序集手动复制到GAC目录。在开发测试期间,安装一个强名称程序集,常用工具是GACUtil.exe
复制代码
GACUtil /i //将某个程序集安装到全局缓存中
GACUtil /u //.net 将某个程序集从全局缓存中卸载
gacutil /u <完全限定的程序集名称> //.net 4.0 将某个程序集从全局缓存中卸载
gacutil /u “Program,Version=0.0.0.0, Culture=neutral, PublicKeyToken=51C39234D5F82021”
sn -T //查看.NET程序集的PublicKeyToken(公钥标记)
复制代码
注意:将程序集文件全局部署到GAC中,是对程序集进行注册的一种手段。虽然实际上Windows注册表没有受到任何影响,但程序集安装到GAC中,会破坏我们的一个基本目标,即:简单的安装、备份、还原、移动、卸载应用程序。所以,建议尽量避免全局部署,尽量使用私有部署。
3.4 在生成的程序集中引用一个强名称程序集
返回
在生成(即编译)时,使用CSC.exe的/reference编译器开关来指定想要引用的程序集文件名。
如果文件名是一个完整路径 ,CSC.exe会加载指定的文件,并根据它的数据来生成程序集;
如果指定一个不含路径的文件名,CSC.exe会试在以下目录查找程序集:
工作目录。
包含CSC.exe本身的目录(%SystemRoot%\Microsoft.NET\Framework\v4.0.####),目录中还包含CLR的各种DLL文件。
使用/lib编译器开关指定的任何目录。
使用LIB环境变量指定的任何目录。
在运行时,不会在CSC.exe本身的目录来加载,而是从GAC加载(具体看3.8节)。
安装.NET Framework时,实际会安装Microsoft的程序集的两套拷贝。一套安装到编译器/CLR目录,另一套安装到一个GAC子目录。
CSC.exe编译器之所以不再GAC中查找引用的程序集,是因为你必须知道程序集的路径,但GAC的结构有没有正式公开。
注意:在一台机器上安装.NET Framework时,回想程序集的x86,x64或IA64版本安装到编译器/CLR目录中。在生成程序集时,可引用已安装的任何版本的文件,因为所有的版本都包含完全一致的元数据。
3.5 强名称程序集如何防范篡改
返回
用一个私钥对程序集签名,可保证程序集是由对应公钥的持有者生成的。程序集安装到GAC时,系统对包含清单的那个文件的内容进行哈希处理,并将哈希值与PE文件中嵌入的RSA数字签名进行比较(在公钥解除了对它的签名后)。如果两个值完全一致,表明文件的内容未被篡改,可保证你拿到的公钥和发布者的私钥是对应的。除此之外,系统还会对程序集的其他文件的内容进行哈希处理,并将哈希值与清单文件的FileDef表中存储的哈希值进行比较。任何一个哈希值不匹配,表明程序集至少有一个文件被篡改,程序集将无法安装到GAC。
将一个强命名程序集安装到了GAC是,系统会执行一次检查。这个检查只在安装时执行一次。相反,从非GAC的一个目录加载强命名程序集是,每次加载都会进行检查。
3.6 延迟签名
返回
当你准备好对自己的强名称程序集进行打包时,必须使用受到严密保护的私钥对它签名。然而,在开发和测试程序集时,访问这些受到严密保护的私钥可能有些费事。有鉴于此,.NET Framework提供了对延迟签名(delay signing)的支持。延迟签名只用公司的公钥来生成一个程序集。由于使用了公钥引用你的程序集的哪些程序集会在它们的AssemblyRef元数据表的记录项中嵌入正确的公钥值。另外,它还使程序集能正确存储到GAC中。
当然,不用公司的私钥对文件进行签名,变无法实现篡改保护。这是由于无法对程序集的文件进行哈希处理,无法在文件中嵌入一个数字签名。
延迟签名步骤:
复制代码
//1)生成公钥/私钥对
sn -k Program.snk
//2)提取出公钥并存放在public.snk中。
sn -p Program.snk public.snk
//3)对程序集进行延迟签名
csc /keyfile:public.snk /delaysign /t:library Program.cs
//4)让CLR信任程序集的内容,不执行签名验证 (这个命令会在注册表中添加相应项 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\StrongName\Verification,所以对同一程序集只需执行一次。至此 Program.dll就可以安装到GAC中)
sn -Vr Program.dll
//5)在最终部署前,一定要用私钥进行签名。否则,其他人可以用公钥做一个相同签名的程序集,替换掉这个程序集。
sn -R Program.dll Program.snk
//6)打开验证,在4)中的注册表项会被相应移除
sn -Vu Program.dll
复制代码
上述代码中:sn -Vr C:\Temp\Bugs\2856482\fix\32\TestDllDiffBit.dll,不执行签名验证。执行该命令,会在注册表中添加相应项(其中,‘TestDllDiffBit’是程序集名称,‘D36A8B911D28EESC’是公钥标记),如下图所示:
3.8 “运行时”如何解析引用类
返回
源代码
复制代码
1 class Program
2 {
3 static void Main(string[] args)
4 {
5 System.Console.WriteLine(“Hi”);
6 }
7 }
复制代码
对以上代码进行编译,生成程序集“Program.exe”。
查看Main()方法的IL代码(运行ILDasm.exe,选择“视图”|“显示字节”,然后双击树形视图中的Main方法)
复制代码
1 .method private hidebysig static void Main(string[] args) cil managed// SIG: 00 01 01 1D 0E
2 {
3 .entrypoint
4 // 方法在 RVA 0x2050 处开始
5 // 代码大小 13 (0xd)
6 .maxstack 8
7 IL_0000: /* 00 | */ nop
8 IL_0001: /* 72 | (70)000001 */ ldstr “Hi”
9 IL_0006: /* 28 | (0A)000011 */ call void [mscorlib]System.Console::WriteLine(string)
10 IL_000b: /* 00 | */ nop
11 IL_000c: /* 2A | */ ret
12 } // end of method Program::Main
复制代码
CLR执行Main方法过程:
(1)运行这个应用程序时,CLR会加载并初始化程序集“Program.exe”
(2)CLR读取程序集的CLR头,查找表示了入口方法(Main)的MethodDefToken。
(3)CLR检索MethodDef元数据表,找到该方法(Main)的IL代码在文件中的偏移量,把这些IL代码JIT编译成本地代码。
在第(3)步进行JIT编译时,CLR会检查对类型和成员的所有引用,并加载它们的程序集(若尚未加载)。从上述代码可以看出代码有一个堆System.Console.WriteLine()的引用,我们来看一下CLR在如何通过元数据来定位引用成员所在所在程序集:
(1)IL call指令引用了元数据token(0A)000011。这个token对应于MemberRef元数据表(表0A)的记录项11.
(2)CLR检查这个MemberRef记录项,发现它的一个字段引用了一个TypeRef表中的记录项(System.Console类型)。
(3)CLR根据TypeRef记录项,被引导至一个AssemblyRef记录项。
(4) 执行本地代码
解析一个引用类型时,CLR可能在以下三个地方找到类型:
(1) 同一个文件
(2) 不同的文件,但同一个程序集
(3) 不同的文件,不同的程序集
下图可以看到CLR如何通过元数据来定位一个类型的定义程序集。
图1 基于引用了一个方法或类型的IL代码,CLR如何利用元数据来定位一个类型的定义程序集
3.9 高级管理控制(配置)
返回
配置文件示例:
View Code
这个示例中,probing元素, 查找弱命名程序集时,检查应用程序基目录的AuxFiles和bin\subdir子目录;查找强命名程序集时,CLR先检查GAC或者有codebase元素指定的URL。只有在未指定codeBase元素的前提下,才会在查找probing指定的目录。
编译一个方法时,CLR确定它引用了哪些类型和成员。根据这些信息,“运行时”检查发出引用的那个程序集的AssemblyRef表,判断该程序集在生成时引用了哪些程序集。然后,CLR在应用程序配置文件中检查程序集版本,并进行设定的任何版本号的重定向操作。
配置文件允许使用和元数据所记录的不完全匹配的一个程序集版本。这种灵活性非常有用。
发布者策略控制
发布者策略就是由发布者设置与程序集同名的配置文件重定向程序集。下面是一个用于JeffTypes.dll程序集的示例文件(JeffTypes.config)
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="JeffTypes" publicKeyToken="32abrka3ke0adkad" culture="neutral"/>
<bindingRedirect oldVersion="1.0.0.0" newVersion="2.0.0.0"/>
<coddBase version="2.0.0.0" herf="http://www.Wintellect.com/JeffTypes.dll"/>
dependentAssembly>
assemblyBinding>
runtime>
configuration>
当然,发布者只能为自己创建的程序集设置策略。除此之外,上面展示的是发布策略配置文件中唯一能使用的元素;例如,probing和publisherPolicy元素是不能使用的。
下面要创建包含这个发布者策略配置文件的一个程序集。命令如下:
AL.exe /out:Policy.1.0.JeffTypes.dll /version:1.0.0.0 /keyfile:MyCompany.snk /linkresource:JeffTypes.config
/out 开关指示AL.exe创建一个新的PE文件,本例就是Policy.1.0.JeffTypes.dll,其中,除了一个清单外什么都没有。名称第一部分(Policy)告诉CLR该程序集包含发布者策略信息,第二和第三部分(1.0)告诉CLR这个程序集适用于major和minor为1.0的任何版本的JeffTypes程序集,第四部分(JeffTypes)指的是与发布者策略对应的程序集的名称,第五部分(dll)指的是要生成发布者策略程序集文件的扩展名。
/version 是着发布者策略程序集的版本。与JeffTypes程序集版本无关。
/keyfile 指示Al.exe使用发布者的“公钥/私钥对”对发布者策略程序集进行签名。这个密钥必须匹配于所有版本的JeffTypes程序集的密钥对。
/linkresource 这个开关告诉AL.exe将xml配置文件作为一个资源链接(而非嵌入)到程序集。最后的程序集由两个文件(Policy.1.0.JeffTypes.dll和JeffType.config)构成。
一旦生成发布者策略程序集,就可随同新的JeffTypes.dll程序集打包并部署到用户机器上。发布者程序集必须安装到GAC中。
注意:如果发布者推出一个发布者策略程序集时,新的程序集引入的bug很多,管理员可配置应用程序配置文件指示CLR忽略发布者策略程序集,配置如下: