为了了解在用VB.NET构造工程的过程中发生了什么事情,我们需要创建一个生成代码和程序集时使用的示例工程:打开VS.NET,新建一个Visual Basic工程,在窗体中加入一个文本标签(Label),然后把文本标签的Text属性改成“Good Bye Visual Basic 6.0”(如图1),把这个应用命名为GoodByeVB6。
图1
在深入.NET体系之前,我们需要了解一些有关.NET的边缘知识和术语。首先,IL(中间语言)并不是什么新概念,VB、C++编译器生成和利用IL已经有数年的历史,只不过很少有人公开谈论它或为它编写文档。与过去编译应用的方法相比,.NET最大的改变之一就在于编译器所生成的代码不同。除了名字之外,新的MSIL与VB6编译器的IL很少有类同之处。因此,如果你以前曾经接触过IL,现在还得从头开始学习。请参见图2,它是GoodByeVB6应用的MSIL代码片段:
图2
这个代码片段设置了一个8字节的栈,然后把this指针压栈,调用get_Label1方法。接下来,代码把要设置的标签文本压入堆栈,然后调用setText方法。
传统的CPU利用寄存器和栈完成所有工作。CLR所提供的执行引擎只有一个栈,它的操作过程非常类似于一个逆波兰表示法计算器。如果某个过程调用具有多个参数,执行引擎将在发出调用之前把参数压栈。函数调用的返回值也通过栈传递。
MSIL中的局部变量很容易识别,它们用.locals关键词声明。如果符号存在的话,你将看到变量名字;否则,你看到的将是V_1、V_2之类的变量:
.locals init ([0] int32 x,
[1] int32 y,
[2] float64 z,
[3] class System.String Vb_t_string_0)
ldarg指令把参数装入栈,ldc指令把数字常量装入栈,stloc指令把值保存到合适的局部变量:
//000064: Dim x As Integer = 100
IL_0001: ldc.i4.s 100
IL_0003: stloc.0
在这个例子中,常量100被作为4字节整数压入栈,随后这个值被保存到第1个局部变量。关于MSIL指令的完整说明,请参见IL程序员参考的ILinstrset.doc文件。
本文的所有MSIL输出都以GoodByeVB6应用的调试版本为基础。非调试版本虽然不带代码行和变量名字,但仍能够提供大量有用的信息。在查看MSIL代码的时候,调试符号虽然重要,但不是必不可少的。
当我们运行编译器时,它生成的不是我们今天熟悉的执行文件,而是一个程序集(Assembly)。程序集是一个文件的集合,程序集中的文件可以作为单一整体进行部署。在当前的Windows体系中,我们可以把单个执行文件看成一个程序集。但从更严格的意义上来说,程序集聚合了执行文件和它的所有支持文件,包括DLL、图形、资源以及帮助文件。
一般地,一个程序集至少由两个文件构成:执行部分,manifest(英文单词原意:载货清单,乘客名单)。manifest是程序集内所有文件的清单。程序集内的可执行部分又分开称为模块(Module)。从概念上说,模块对应着DLL或者EXE文件;除了父程序集所包含的元数据(Metadata)之外,每一个模块都包含元数据。程序集是当前可移植执行文件格式(Portable Executable,PE)的一个增强版本。
如图3所示,文件的开头是标准的PE头。文件内部包含了CLR头,CLR头的后面是把代码装入进程空间所必需的描述数据——即元数据。元数据为执行引擎提供了大量信息,其中包括:如何装载模块,需要哪些支持文件,如何装载支持文件,如何与COM以及.NET运行时环境交互。另外,元数据还描述了模块或者程序集所包含的方法、接口以及类。元数据所提供的信息使得JIT编译器能够编译并运行模块。同时,元数据暴露了有关应用的大量内部信息,使得从反汇编IL获取有价值的代码更加方便。
图3
使用.NET代码的核心问题在于受管理代码。受管理代码是专门为在CLR控制之下运行而编写的代码,它可以用VB.NET、C#以及C++等语言创建,但C++是唯一能够创建.NET平台非受管理代码的语言。我们无法用VB6为.NET平台创建非受管理代码,这是因为在VB6中我们把代码编译成i386指令而不是IL代码。正如使用VB.NET,如果你要使用受管理代码,你只能把代码编译成IL。
现在我们来看看使用这种新的MSIL代码有哪些优点。如果代码编译成了MSIL,我们可以在任何支持CLR的平台上安装和运行这些代码。就目前来说,这一点可能不是很吸引人,因为当前支持.NET的平台还很少:只有32位的Windows。但不久之后,64位平台和.NET for Windows CE都将提供这方面的支持。把代码编译成MSIL格式使得我们能够无缝地把应用移植到所有这些平台和未来的新平台。
MSIL的另外一个优点是:JIT编译器在安装应用的目标机器上把MSIL代码编译成机器指令,它能够利用目标机器的硬件特点,根据平台的具体情况对代码进行优化。这一点很有用,例如,它能够为目标机器的特殊寄存器优化代码,或为目标机器上带有特殊处理器的硬件设备优化操作代码。请点击VB6工程属性窗口Compile选项卡中Advanced Optimizations按钮了解更多信息。由于有了程序集中的元数据,JIT编译器知道代码做些什么以及它支持哪些平台,从而能够迅速地作出优化决定、提高代码的性能表现。
还有一个优点涉及到.NET的两个V:Validation(检验),Verification(核查)。检验是对模块进行的一系列检查,确保元数据、MSIL代码以及文件格式的一致性。不能通过这些检查的代码可能导致执行引擎或者JIT编译器崩溃。一旦模块通过了检验,则代码是正确的且可以开始运行。
JIT编译器把MSIL代码转换成机器代码时对代码进行核查,它是对元数据进行复查,保证程序不会访问它不具有相应许可的内存或其他资源。经过核查的代码是类型安全的(Type-Safe)代码。这种核查即使是在程序被直接编译成机器代码的时候也要进行,但除非由JIT编译器进行核查,否则这种核查不是100%精确无误,因为核查结果依赖于来自其他程序集的元数据。如果把源程序直接编译成机器代码,我们面临着这样一种危险:在目标机器上的其他程序集发生了变化,从而导致程序不再类型安全。
使用JIT编译器保证了检验和核查是对所有相关程序集的当前版本进行。这些操作确保执行程序总是类型安全,程序总是以合适的安全许可运行。你可以用.NET SDK的PEVerify工具自己对代码进行检验和核查。