程序集的初始化及合并

         摘要 : 这里探讨 .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

posted on 2007-04-19 23:06  Zealic  阅读(5265)  评论(4编辑  收藏  举报