程序集的初始化及合并
摘要 : 这里探讨 .Net Assembly 的初始化与合并,很多情况下开发功能库时,需要在 Assembly 加载时就进行一些初始化行为,比如说某些 .Net 混淆器,在以前通常采用在所有类中的静态构造器中插入初始化方法,而现在,就可以仅仅在程序集初始化时就直接运行方法。
当我们要编写一些组件库的时候,必须以DLL形式发布或者因为某种需求,如何在运行时初始化程序集,而不是强迫使用者调用初始化方法呢?
CLR 的加载机制估计很多人都知道:
首先加载程序集,但仅仅读取所有元数据,当第一次调用程序集某个模块的成员时,按以下程序加载:
1.调用模块的静态构造函数,并创建一个模块的实例
2.调用被调用成员的类型的静态构造函数,如果是全局方法\成员,则直接到第三步。
3.执行调用过程/返回值。
4.如果被调用的托管方法,CLR检查是否经过JIT编译,如没有编译,则编译为本地代码。
5.执行方法调用。
上面说的托管模块的入口在程序集实际上依然为一个类,如果经常使用 Reflector,则必然可以在根名称空间中看到名称为 “<Module>” 的类,在下文中,我们将把“<Module>”称为模块类。
模块类非常特殊,首先它并不是从 System.Object 派生的,因为CLR运行库同样有模块类,这也许是 .Net 唯一不从 Object 继承的类了;模块类仅仅能包含三种成员,静态构造函数、静态方法、以及静态字段,可以定义模块的构造函数,但是运行时CLR会抛出System.TypeInitializationException异常,实例方法可以定义,但是会被 ILASM 在编译时忽略。
而下面,为了演示模块构造器;我们从无所不能的 C++/CLI 入手。
#include "stdafx.h" using namespace System; int initzer() { Console::WriteLine(L"Module Initlizing..."); return 0; } int init = initor(); int main(array<System::String ^> ^args) { Console::WriteLine(L"Hello World!"); Console::ReadKey(false); return 0; } |
上面代码的结果大家应该都知道:
Module Initilizing...
Hello World!
但是,在C#、VB中,这样的执行顺序是不可能的(静态构造器除外);看出他们哪里不同没有?
对了,这个 C++/CLI 编写的 Assembly 完全没有任何类包装,而C# 和 VB 是纯粹OO,不给类容器就不能运行。
这才是真正的全局变量,无论程序如何运行,总是需要设置全局变量,C++/CLI的通用模块类构造函数如下(使用Reflector反编译):
[DebuggerStepThrough] <Module>() { LanguageSupport languageSupport; ::<CrtImplementationDetails>_LanguageSupport_{ctor}(&languageSupport); ::<CrtImplementationDetails>_LanguageSupport_Initialize(&languageSupport); ::<CrtImplementationDetails>_LanguageSupport_{dtor}(&languageSupport); } |
C++/CLI 就这样完成了运行时的初始化,恩没错,C++要间接的在模块构造时进行操作很方便。而C#、VB.Net,是不可能的,我们可以期望 Microsoft 在 .Net Framework 3.5 或更高的版本中提供一个 [module: ModuleInitializerAttribute(initType)],initType为初始化的类型(调用静态构造方法)或接口(调用接口方法),当然,在这里也有别的解决方案。
用IL实现我们的愿望,代码如下:
.assembly extern mscorlib{} .assembly Test { .ver 1:0:0:0 } .module ModA //定义模块静态构造函数 .method public hidebysig specialname rtspecialname static void .cctor() cil managed { .maxstack 8 ldstr "#Module Initializeing..." call void [mscorlib]System.Console::WriteLine(string) ret } .namespace TestA { .class private auto ansi beforefieldinit Program extends [mscorlib]System.Object { .method private hidebysig specialname rtspecialname static void .cctor() cil managed { .maxstack 8 ldstr "*Type Initlizeing..." call void [mscorlib]System.Console::WriteLine(string) ret } .method private hidebysig static void Main(string[] args) cil managed { .entrypoint .maxstack 8 ldstr "+Here is Main method, welcome!" call void [mscorlib]System.Console::WriteLine(string) ldstr "+Press any key to exit..." call void [mscorlib]System.Console::WriteLine(string) ldc.i4.1 call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey(bool) pop ret } } } |
注: 在 module 初始化时如果调用模块中某类的成员,将会导致 CLR 初始化该类,因此,在模块构造函数中是可以调用该模块中的成员的,但是不能访问调用栈的上层,否则会抛出异常,因为上层属于 CLR,并不是托管代码。
用在命令行中输入命令 ilasm ModA.il 编译上面的代码,我们将得到一个 ModA.exe。
运行后,输出结果如下:
#Module Initializeing...
*Type Initlizeing...
+Here is Main method, welcome!
+Press any key to exit...
现在我们得到我们想要的结果了,但是这只能用 IL 完成,将整个功能库用 IL 写,显然是不现实的。
幸运的是 Microsoft 为我们提供了工具 ILMerge,我们可以用它完成两个纯粹的.Net程序集的合并;于 csc 和 vbc 的 /addmodule 选项不同,ILMerge 直接把两个程序集中的所有模块合并为一个,而 /addmodule 则是将两个模块合并为一个程序集,实际上依旧是两个模块。
当然,我们不只能够合并 .Net 程序集,还能够合并 NativePE 文件,这个我们需要一个叫做 mergebin 的程序,我是在 SQLite for .Net Provider的源码包中发现它的,使用它有一些限制,要设置 data_seg 为 ‘.clr’,并且为该 section 保留足够容纳 .Net Assembly 的大小,具体可以用 mergebin 计算出来,其他的细节就不做细表。
Zealic 2007-04-19