(翻译) 《C# to IL》第一章 IL入门
-1-
我们用C#、VB.NET语言编写的代码最终都会被编译成程序集或IL。因此用VB.NET编写的代码可以在C#中修改,随后在COBOL中使用。因此,理解IL是非常有必要的。
一旦熟悉了IL,理解.NET技术就不会有障碍了,因为所有的.NET语言都会编译为IL。IL是一门中性语言。IL是先发明的,随后才有了C#、VB.NET等语言。
我们将在一个短而精辟的程序中展示IL。我们还假设读者至少熟悉一门.NET语言。
a.il
{
}
随后,我们用IL编写了一个非常短小的IL程序——它显然是不能工作的,并将它命名为a.il。那么我们怎么才能把它编译为一个可执行程序呢?不需要为此而焦急,Microsoft提供了一个ilasm程序,它的唯一任务就是从IL文件中创建可执行文件。
在允许这个命令之前,要确保你的变量路径被设置为framework中的bin子目录。如果不是,请输入命令如下:
set path=c:/progra~1/microsoft.net/frameworksdk/bin;%PATH%
现在,我们使用如下命令:
c:/il>ilasm /nologo /quiet a.il
这样做会生成下面的错误:
Source file is ANSI
Error: No entry point declared for executable
***** FAILURE *****
将来,我们将不会显示由ilasm生成的输出的第一行和最后一行。我们还将移除非空白行之间的空白行。
在IL中,允许我们使用句点.作为一行的开始,这是一条指令,要求编译器执行某个功能,如创建一个函数或类,等等。任何开始于句点的语句都是一条实际俄编译器指令。
.method表示创建一个名为vijay的函数(或方法),并且这个函数返回void,即它不返回任何值。因为缺少较好的命名法则,函数名称vijay显得很随意。
汇编器显然理解不了这个程序,从而会显示“no entry point”的消息。这个错误信息的生成是因为IL文件能够包括无数的函数,而汇编器无法区分哪个会被首先被执行。
在IL中,首先被执行的函数被称为进入点(entrypoint)函数。在C#中,这个函数是Main。函数的语法是,名称之后是一对圆括号()。函数代码的开始和结束用花括号{}来表示。
a.il
{
.entrypoint
}
c:/il>ilasm /nologo /quiet a.il
Source file is ANSI
Creating PE file
Emitting members:
Global Methods: 1;
Writing PE file
Operation completed successfully
现在不会生成任何错误了。伪指令(directive)entrypoint表示程序执行必须开始于这个函数。在这个例子中,我们不得不使用这个伪指令,虽然事实上这个程序只有一个函数。当在DOS提示符中给出dir命令后,我们看到有3个文件会被创建。a.exe是一个可执行文件,现在可以执行它来看到程序的输出。
C:/il>a
Exception occurred: System.BadImageFormatException: Exception from HRESULT: 0x8007000B. Failed to load C:/IL/A.EXE.
当我们试图执行上面的程序时,我们的运气似乎不太好,因为会生成上面的运行时错误。一个可能的原因是,这个函数是不完整的,每个函数都应当具有一个“函数结束”指令在函数体中。我们匆忙之中显然没有注意到这个事实。
a.il
{
.entrypoint
ret
}
“函数结束”指令被称为ret。前面所有的函数都必须以这个指令作为结束。
Output
Exception occurred: System.BadImageFormatException: Exception from HRESULT: 0x8007000B. Failed to load C:/IL/A.EXE.
在执行这个程序时,我们再次得到了相同的错误。这次我们的问题又在哪里呢?
a.il
.method void vijay()
{
.entrypoint
ret
}
错误在于我们忘记在名称后面使用必不可少的伪指令assembly。我们将其合成在上面的代码中,并在一对空的花括号之后使用了名称mukhi。这个程序集伪指令用于给出程序的名称。它又被称为一个部署单元。
上面的代码是可以汇编而没有任何错误的最小的程序,虽然它在执行时并没有做什么有用的事情。它没有任何名为Main的函数。它只有一个带有entrypoint伪指令的函数vijay。现在汇编这个程序并运行而根本不会有任何错误。
在.NET中,程序集的概念是极其重要的,应该对其有彻底的认识。我们将在本章后半部分使用这个伪指令。
a.il
.method void vijay()
{
.entrypoint
ret
}
.method void vijay1()
{
.entrypoint
ret
}
Error
***** FAILURE *****
上面错误信息的原因是,上面的程序有2个函数,vijay和vijay1,每个函数都包括了.entrypoint伪指令。正如前面提到的那样,这个指令指定了关于那个函数会被首先执行。
因此,在功能上,它类似于C#中的Main函数。当C#代码被转换为IL代码时,在Main函数中包含的代码会被转换为IL中的函数中并包括.entrypoint伪指令。例如,如果在COBOL程序中执行的第一个函数被称为abc,那么在IL中生成的代码就会在这个函数中插入.entrypoint伪指令。
在常规的程序语言中,首先被执行的函数必须有一个特定的名称,例如Main,但是在IL中,只需要一个.entrypoint伪指令。因此,因为一个程序只能由一个开始点,所以在IL代码中只允许一个函数包括.entrypoint伪指令。
迫切地看到,没有生成任何错误消息编号或说明,使得调试这个错误非常困难。
a.il
.method void vijay()
{
ret
.entrypoint
}
.entrypoint伪指令需要被定位为函数中的第一个指令或最后一个指令。它仅出现在函数体中,从而将它的状态宣布为第一个被执行的函数。伪指令不是程序集指令,甚至可以被放置在任何ret指令之后。提醒你一下,ret表示函数代码的结束。
a.il
.method void vijay()
{
.entrypoint
call void System.Console::WriteLine()
ret
}
我们可能有一个用C#、VB.NET编写的函数,但是在IL中执行这个函数的机制是相同的。如下所示:
我们必须使用汇编指令调用。调用指令之后,按照给定的顺序,为以下详细内容:
-
- 函数的返回类型(void)
- 命名空间(System)
- 类 (Console)
- 函数名称 (WriteLine())
函数被调用但不会生成任何输出。因为,我们传递一个参数到WriteLine函数中。
a.il
.method void vijay()
{
.entrypoint
call void System.Console::WriteLine( class System.String)
ret
}
上面的代码有一处“闪光点”。当一个函数在IL中被调用时,除了它的返回类型之外,被传递的参数的数据类型,也必须被指定。我们将Writeline设置为——希望得到一个System.String类型作为参数,但是由于没有字符串被传递到这个函数中,所以它会生成一个运行时错误。
因此,在调用一个函数时,在IL和其他程序语言之间有一个明显的区别。在IL中,当我们调用一个函数,我们必须指定关于该函数我们所知道的任何内容,包括它的返回类型和它的参数的数据类型。通过在运行期间进行恰当的检查,保证了汇编器能够在语法上验证代码的有效性。
现在我们将看到如何将参数传递到一个函数中。
a.il
.method void vijay()
{
.entrypoint
ldstr " hell "
call void System.Console::WriteLine( class System.String)
ret
}
Output
hell
汇编器指令ldstr把字符串放到栈上。Ldstr的名称是文本"load a string on the stack"的缩写版本。栈是一块内存区域,它用来传递参数到函数中。所有的函数从栈上接收它们的参数。因此,像ldstr这样的指令是必不可少的。
a.il