3.8 “运行时”如何解析类型引用
2011-12-07 10:17 iRead 阅读(502) 评论(0) 编辑 收藏 举报第2章开头展示了一下源代码:
public sealed class Program { public static void Main() { System.Console.WriteLine("Hi"); } }
编译这段代码并生成一个程序集,假定为Program.exe。运行这个应用程序时,CLR会加载并初始化它。然后,CLR读取程序集的CLR头,查找标识了应用程序入口方法(Main)的MethodDefToken。然后,CLR会检索MethodDef元数据表,找到该方法的IL代码在文件中的偏移量,把这些IL代码JIT(just-in-time,”即时”)编译成本地(native)代码。编译时会对代码进行验证以确保类型安全性。最后,将执行本地代码。下面展示了Main方法的IL代码。为了获取这个输出,请运行ILDasm.exe,选择“视图”|“显示字节”,然后双击树形视图中的Main方法。
.method public hidebysig static void Main() cil managed { .entrypoint // Code size 13 (0xd) .maxstack 8 IL_0000: nop IL_0001: ldstr "Hi" IL_0006: call void [mscorlib]System.Console::WriteLine(string) IL_000b: nop IL_000c: ret } // end of method Program::Main
对这个代码进行JIT编译时,CLR会检查对类型和成员的所有引用,并加载定义了它们的程序集(如果尚未加载)。可以看出,上述IL代码有一个对System.Console.WriteLine的引用。具体地说,IL call指令引用了元数据token 0A00003。这个token对应于MemberRef元数据表(表0A)中的记录项3.CLR检查这个MemberRef记录项,发现它的一个字段引用了一个TypeRef表中的一个记录项(System.Console类型)。根据TypeRef记录项,CLR被引导至一个AssemblyRef记录项:”mscorlib,Version=4.0.0.0,Culture=neutral,PublicKeyToken=b77a5c561934e089”。这样,CLR就知道了它需要的是哪一个程序集。接着,CLR必须定位并加载该程序集。
解析一个引用的类型时,CLR可能在以下三个地方找到类型:
- 同一个文件 编译时便能发现对相同文件中的类型的引用,这成为早期绑定(early binding)。类型将直接从文件中加载,执行将继续。
- 不同的文件,但同一个程序集 “运行时”确保被引用的文件确实在当前程序集清单的FileDef表中。然后,“运行时”检查程序集的清单文件所在的目录。文件会加载,会检查哈希值来确保文件的完整性,类型的成员会被发现,执行将继续。
- 不同的文件,不同的程序集 如果引用的类型在其他程序集的文件中,“运行时”会加载被引用的程序集的清单文件。如果需要的类型不在该文件中,就继续加载包含了类型的文件。类型的成员会被发现,执行将继续。
注意:ModuleDef,ModuleRef和FileDef元数据表使用文件名及其扩展名来引用文件。然而,AssemblyRef元数据表使用不带扩展名的文件来引用程序集。要和一个程序集绑定时,系统通过探测目录来尝试定位文件,并自动附加.dll和.exe扩展名,详见2.8节“简单管理控制(配置)”。
解析一个类型引用时,如果发生任何错误(比如找不到文件、文件无法加载、哈希值不匹配等),就会抛出一个相应的异常。
注意:可向System.AppDomain的AssemblyResolve,ReflectionOnlyAssemblyResolve和TypeResolve事件注册你写的回调方法。在回调方法中,可执行一些能够解决绑定问题的代码,使应用程序在不抛出异常的前提下继续运行。
在上例中,CLR发现System.Console是在和调用者不同的程序集中实现的。所以,CLR必须查找那个程序集,并加载包含程序集清单的PE文件。然后扫描清单,判断是哪个PE文件实现了类型。如果被引用的类型恰好在这个清单文件中,一切都很简单。如果类型在程序集的另一个文件中,CLR必须加载那个文件,并扫描其元数据来定位类型。然后,CLR创建它的内部数据结构来表示类型,JIT编译器完成Main方法的编译。最后,Main方法开始执行。图3-2演示了类型绑定过程。
图3-2 基于引用了一个方法或类型的IL代码,CLR如何利用元数据来定位一个类型的定义程序集
重要提示:
严格地说,刚才描述的例子并不是百分之百正确。如果引用的不是.NET Framework配套提供的程序集所定义的方法和类型,刚才讨论的是没有任何问题的。但是,.NET Framework程序集(包括MSCorLib.dll)和当前运行的CLR的版本是紧密绑定的。引用了.NET Framework程序集的任何一个程序集总是绑定到与CLR的版本相匹配的那个版本(的.NET Framework程序集)。这就是所谓的“统一”(Unification)。之所以要进行“统一”,是因为所有.NET Framework程序集都是针对一个特定版本的CLR来测试的。因此,通过“统一”所有代码,可以确保应用程序正确地工作。
所以,在前面的例子中,对System.Console的WriteLine方法的引用必然绑定到与当前CLR版本匹配的那个版本的MSCorLib.dll—无论程序集AssemblyRef元数据表中引用的是哪个版本的MSCorLib.dll。
还要注意的是,对于CLR来说,所有程序集都是根据名称、版本、语言文化和公钥来标识的。但是,GAC根据名称、版本、语言文化、公钥和CPU架构来标识程序集。在GAC中搜索一个程序集时,CLR判断应用程序当前在什么类型的进程中运行:32位x86(可能使用WoW64技术)、64位x64或者64位IA64。然后,CLR首先搜索程序集的这种CPU架构专用的版本。如果没有找到符合要求的程序集,就搜索不区分CPU的版本。
本节描述的是CLR定位一个程序集时的默认策略。然而,管理员或者程序集的发布者可能覆盖默认策略。在后面的两节中,将讨论如何更改CLR的默认绑定策略。
注意:
CLR提供了将一个类型(包括类、结构、枚举、接口或委托)从一个程序集移动到另一个程序集的功能。例如,在.NET 3.5中,System.TimeZoneInfo类是在System.Core.dll程序集中定义的。但在.NET 4.0中,Microsoft将这个类移动到了MSCorLib.dll程序集。将一个类型从一个程序集移动到另一个程序集,这一般情况下会造成应用程序的“中断”。但是,CLR提供了一个名为System.Runtime.CompilerServices.TypeForwardedToAttribute的attribute,可将它应用于原始程序集(比如System.Core.dll)。要向这个attribute的构造器传递一个System.Type类型的参数,它指出应用程序要使用的新类型(现在是在MSCorLib.dll中定义)。CLR的binder(绑定器)会利用这个信息。由于TypeForwardedToAttribute的构造器获取的是一个Type,所以包含这个attribute的程序集要依赖于类型的定义程序集。
要使用这个功能,还要将System.Runtime.CompilerServices.TypeForwardedFromAttribute这个attribute应用于新程序集中的类型,并向该attribute的构造器传递一个字符串来指出对类型进行了定义的新程序集的全名。这个attribute通常用于工具、实用程序和序列化。由于TypeForwardedFromAttribute的构造器获取的是一个String,所以包含这个attribute的程序集不依赖于类型的定义程序集。