CLR Loader
CLR Loader可以说是整个.NET的基础元素之一,它就是用来加载托管程序集、模块、类型等的。
CLR Loader与传统的C++ Loader不同,它是按需加载,当需要的时候才加载这个程序集,这样一来就会加快程序的启动速度,降低系统要求的资源。
注意 如此一来,如果一个类型中有一个方法里引用了另外一个类型,但幸运的是,这个方法永远都没有执行过,那么被引用的类型也永远不会被加载了。 那我如果想我的程序集被加载该怎么办呢?可以在必定加载的程序集中设置一个静态字段,而这个静态字段的类型就定义在想被加载的程序集中。 |
CLR Loader加载程序集是由JIT编译器决定的,JIT编译器的编译单元是一个方法,当JIT编译一个方法的时候发现方法内引用了一个定义在别的程序集的类型,CLR Loader首先就会加载这个程序集。那CLR Loader又如何知道这个类型定义在哪个程序集中呢?又如何定位这个程序集呢?
类型处于哪个程序集?
这个问题就依赖Metadata。我们先来写个小程序看看:
这个Foo类型定义在命名空间Yuyijq.Lib下,放在Foo.dll中
using System; namespace Yuyijq.Lib { public class Foo { public void Write() { Console.WriteLine("From Foo.Write"); } f } }
下面的程序会调用Foo的Write方法,我把这个程序编译进另外一个exe中(Program.exe):
using System; using Yuyijq.Lib; namespace Yuyijq.MainApp { public class Program { static void Main() { Foo foo = new Foo(); foo.Write(); Console.ReadLine(); } } }
当程序运行Main方法之前会首先调用JIT编译器编译Main方法,但是却发现Main方法里引用了Foo类型,
Foo类型又定义在Foo.dll中,这可咋整,没加载怎么编译呢,这个时候CLR Loader闪亮登场了。
我们先来看看如何知道Foo所在的程序集,请看下面的Program反编译后的输出:
.locals init (class [Foo]Yuyijq.Lib.Foo V_0) IL_0000: nop IL_0001: newobj instance void [Foo]Yuyijq.Lib.Foo::.ctor()
在上面的代码中,出现了[Foo],这个就说明Foo这个类型啊是定义在Foo这个程序集中(这个就是传说的元数据),那关于Foo这个程序集更详细的信息呢?
.assembly extern Foo { .ver 0:0:0:0 }
这个就是传说的程序集清单。
好了,我们的CLR Loader现在要加载一个这样的程序集:名字为Foo,版本号为0:0:0:0,那CLR Loader如何定位这个程序集呢?
这个问题先留在这里,后面会继续讨论。
上面描述的场景是由运行时自动的去发现程序集,自动的加载程序集(这个就称为静态引用),那有没有手动的方式,由程序员主动去加载一个程序集(动态引用)?答案是肯定的
System.Reflection.Assembly类里就有很多这样的方法:
Load
LoadFrom
我们先来看看LoadFrom这个方法,这个方法有三个重载。该方法会接受一个字符串参数,这个参数指定程序集的文件
通过Reflctor LoadFrom的代码我们发现对于传入的这个字符串,如果以file://则要求有访问的IO权限,否则会将该字符串当作URL,这样就要求访问Web的权限。
[MethodImpl(MethodImplOptions.NoInlining)] public static Assembly LoadFrom(string assemblyFile) { StackCrawlMark lookForMyCaller = StackCrawlMark.LookForMyCaller; return InternalLoadFrom(assemblyFile, null, null, AssemblyHashAlgorithm.None, false, ref lookForMyCaller); }
private static Assembly InternalLoadFrom(string assemblyFile, Evidence securityEvidence, byte[] hashValue, AssemblyHashAlgorithm hashAlgorithm, bool forIntrospection, ref StackCrawlMark stackMark) { if (assemblyFile == null) { throw new ArgumentNullException("assemblyFile"); } AssemblyName assemblyRef = new AssemblyName(); assemblyRef.CodeBase = assemblyFile; assemblyRef.SetHashControl(hashValue, hashAlgorithm); return InternalLoad(assemblyRef, securityEvidence, ref stackMark, forIntrospection); }
internal static Assembly InternalLoad(AssemblyName assemblyRef, Evidence assemblySecurity, ref StackCrawlMark stackMark, bool forIntrospection) { if (assemblyRef == null) { throw new ArgumentNullException("assemblyRef"); } assemblyRef = (AssemblyName) assemblyRef.Clone(); if (assemblySecurity != null) { new SecurityPermission(SecurityPermissionFlag.ControlEvidence).Demand(); } string strA = VerifyCodeBase(assemblyRef.CodeBase); if (strA != null) { if (string.Compare(strA, 0, "file:", 0, 5, StringComparison.OrdinalIgnoreCase) != 0) { CreateWebPermission(assemblyRef.EscapedCodeBase).Demand(); } else { URLString str2 = new URLString(strA, true); new FileIOPermission(FileIOPermissionAccess.PathDiscovery | FileIOPermissionAccess.Read, str2.GetFileName()).Demand(); } } return nLoad(assemblyRef, strA, assemblySecurity, null, ref stackMark, true, forIntrospection); }
通过下面这个方法索取Web访问的权限(非常有意思的是,这个方法自身也用到了Load方法加载System这个程序集)。
private static IPermission CreateWebPermission(string codeBase) { Assembly assembly = Load("System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"); Type enumType = assembly.GetType("System.Net.NetworkAccess", true); IPermission permission = null; if (enumType.IsEnum && enumType.IsVisible) { object[] args = new object[2]; args[0] = (Enum) Enum.Parse(enumType, "Connect", true); if (args[0] != null) { args[1] = codeBase; enumType = assembly.GetType("System.Net.WebPermission", true); if (enumType.IsVisible) { permission = (IPermission) Activator.CreateInstance(enumType, args); } } } if (permission == null) { throw new ExecutionEngineException(); } return permission; }
我们再来看看Load方法有什么不同呢?
[MethodImpl(MethodImplOptions.NoInlining)] public static Assembly Load(string assemblyString) { StackCrawlMark lookForMyCaller = StackCrawlMark.LookForMyCaller; return InternalLoad(assemblyString, null, ref lookForMyCaller, false); }
internal static Assembly InternalLoad(string assemblyString, Evidence assemblySecurity, ref StackCrawlMark stackMark, bool forIntrospection) { if (assemblyString == null) { throw new ArgumentNullException("assemblyString"); } if ((assemblyString.Length == 0) || (assemblyString[0] == '\0')) { throw new ArgumentException(Environment.GetResourceString("Format_StringZeroLength")); } AssemblyName assemblyRef = new AssemblyName(); Assembly assembly = null; assemblyRef.Name = assemblyString; if (assemblyRef.nInit(out assembly, forIntrospection, true) == -2146234297) { return assembly; } return InternalLoad(assemblyRef, assemblySecurity, ref stackMark, forIntrospection); }
从这里可注意到,不管是LoadFrom方法还是Load方法都构造了一个AssemblyName对象,但是LoadFrom方法将传入的字符串作为AssemblyName的CodeBase属性,而Load方法内却将传入的字符串当作AssemblyName的Name属性。
从这里是不是可以判断,Load就是根据程序集的名称定位程序集,而LoadFrom是根据程序集文件路径定位程序集呢?答案是否定的。
Load方法还有一个重载:
public static Assembly Load(AssemblyName assemblyRef)
这个Load方法接受一个AssemblyName参数,如果我们想根据程序集路径,而又使用Load方法加载,那就实例化一个AssemblyName对象,然后设置该对象的CodeBase属性不就可以了~~~
那通过程序集名称和文件路径加载程序集有什么不同呢?
如果指定程序集文件路径,那么就直接定位了程序集,Assembly Loader只需要根据这个就可以加载程序集了(注意,这里的路径可以使本地操作系统的文件,也可以使用URI,如果Assembly Loader根据这个路径没有找到对应的程序集,则加载会失败,将抛出System.FileNotFoundException异常),如果是指定程序集的名称则还有一个Assembly Resolver参与进来,这个东西先根据程序集名称定位出程序集文件,这个时候就可以实施一系列程序集版本、安全策略了。
关于程序集的定位还是一个非常复杂的过程,在下一篇中会继续讨论~~