(翻译)《Expert .NET 2.0 IL Assembler》 第三章 使代码更简单
我并不了解你的感受,但是对我而言,这种无休止的重复输入同样的代码是一种搞笑的平均水平以下的方法。让我们看看ILAsm2.0如何使这件工作不再枯燥乏味。这里有三种有用的额外编译器语法:别名(aliasing),编译控制指令,关联当前类和它的引用项,以及用于当前类及其父类的特殊关键字。
别名
在上一章出现的Simple2.il示例中,控制台输入/输出方法被调用了四次(1次输入和3次输出)。每次我必须输入[mscorlib]System.Console::WriteLine或[mscorlib]System.Console::ReadLine。在ILAsm1.0和1.1中,我没有别的选择,但是在ILAsm2.0中,我可以使用别名(aliasing),为方法和类分配新的短名称。
清单3-1显示了带有别名的上一章的这个简单示例(源文件Simple3.il)。而且,顺便说一下,既然已经使用了这段代码,让我们省去那些不必要的默认声明。
清单3-1 带有别名的Simple3.il
.assembly extern mscorlib { auto }
.assembly OddOrEven { }
.module OddOrEven.exe
//----------- Aliasing
.typedef [mscorlib]System.Console as TTY
.typedef method void TTY::WriteLine(string) as PrintLine
//----------- Class Declaration
.class public Odd.Or.Even {
//------------ Field declaration
.field public static int32 val
//------------ Method declaration
.method public static void check( ) {
.entrypoint
.locals init(int32 Retval)
AskForNumber:
ldstr "Enter a number"
call PrintLine
.try {
// Guarded block begins
call string TTY::ReadLine()
// pop
// ldnull
ldstr "%d"
ldsflda int32 Odd.or.Even::val
call vararg int32 sscanf(string,string,,int32*)
stloc.0
leave.s DidntBlowUp
// Guarded block ends
}
catch [mscorlib]System.Exception
{ // Exception handler begins
pop
ldstr "KABOOM!"
call PrintLine
leave.s Return
} // Exception handler ends
DidntBlowUp:
ldloc.0
brfalse.s Error
ldsfld int32 Odd.or.Even::val
ldc.i4.1
and
brfalse.s ItsEven
ldstr "odd!"
br.s PrintAndReturn
ItsEven:
ldstr "even!"
br.s PrintAndReturn
Error:
ldstr "How rude!"
PrintAndReturn:
call PrintLine
ldloc.0
brtrue.s AskForNumber
Return:
ret
} // End of method
} // End of class
//------------ Calling unmanaged code
.method public static pinvokeimpl("msvcrt.dll" cdecl)
vararg int32 sscanf(string,string) { }
正好在程序头之后,我定义了类[mscorlib]System.Console的别名以及它的方法WriteLine(string):
.typedef [mscorlib]System.Console as TTY
.typedef method void TTY::WriteLine(string) as PrintLine
ILAsm中的别名由.typedef关键字引进——这有点像C语言中的typedef关键字。(注意到这和第一章提到的TypeDef没有关系——只是描述类和值类型的元数据表中的一个进入点。)别名可以用于类、方法、字段或自定义特性。(自定义特性在第16章描述)。一旦一个别名被引进,它就可以在任何地方使用以代替别名项。你可能注意到第二个别名指令使用了定义在第一个别名指令中的别名。
当一个方法被“别名化”时,它的定义开始于一个method方法并包括该方法的完整名称和签名。当别名化一个字段时,也使用同样的步骤,但是在这种情形中,主要的字段当然就是field了。别名化一个自定义特性此刻也应该不会影响到你(因为我已经解释过自定义特性是什么),所以我建议等到第16章再进行介绍。
别名的定义是模块级别的,这意味着一旦你定义了一个别名,你就可以在这个模块的任意地方使用它。这还意味着一旦你定义了一个别名,你不可能再次在这个模块中定义它。这不同于由typedef指令提供的C++别名,后者的范围是一个类或一个方法。
这些别名必须在被使用之前进行词法定义。
这些别名的名称在模块中必须是唯一的;你不能别名化两个不同的项并都命名为Foo。换句话说,你可以多次为同一个项设置别名(类、方法、字段或自定义特性)。下面代码是完全合理的:
.typedef [mscorlib]System.Console as CON
call void TTY::WriteLine(string)
call void CON::WriteLine(string)
别名的一个有趣的特性是,它们幸免于来回往返的过程(编译和反编译过程)。如果你编译Simple3.il并随后反编译Simple3.exe这个结果,你将会看到TTY和PrintLine这些别名都存在并被处理。这是因为ILAsm编译器存储了所有的别名在被发布模块的元数据中,而反编译器则寻找这些别名并使用它们。
你可能还注意到我去除了.namespace指令,并通过类的完整名称声明了Od.Or.Even类。这是ILAsm2.0的另一个特性,依我看来,使得程序员的生活更加轻松,因为这是类的定义和引用的统一方式。除此之外,使用.namespace指令导致了有趣的问题,如,“如果我在命名空间X.Y中定义了一个全局方法Z,那么我需要将这个方法写为X.Y::Z么?”(答案是不,你只需要写成Z;而命名空间只是对那些类有效。)
当然,这并不意味着,ILAsm 2.0不需要理解.namespace指令。ILAsm 2.0是完全向后兼容的,这意味着它也可以编译早期版本能够编译的源文件。
编译控制指令
有C/C++经验的程序员可能还会对那些有用的预处理指令留有怀旧感,如#include,#define,#ifdef等等。令人惊喜的是,我的朋友们,因为ILAsm 2.0支持其中一些的指令。不,它并不支持全部质量;这里有太多的方式了。
清单3-2显示了对这个示例的一些轻微改动(源文件Simple4.il)。
清单3-2 Simple4.il
// #define BLOW_UP
//----------- Program header
.assembly externmscorlib{ auto }
.assembly OddOrEven { }
.module OddOrEven.exe
//----------- Aliasing
.typedef [mscorlib]System.Console as TTY
.typedef method void TTY::WriteLine(string) as PrintLine
//----------- Class Declaration
.class public Odd.Or.Even {
//------------ Field declaration
.field public static int32 val
//------------ Method declaration
.method public static void check( ) {
.entrypoint
.locals init(int32 Retval)
AskForNumber:
ldstr "Enter a number"
call PrintLine
.try{
// Guarded block begins
call string TTY::ReadLine()
#ifdef BLOW_UP
pop
ldnull
#endif
#ifdef USE_MAPPED_FIELD
ldsflda valuetype CharArray8 Format
ldsflda int32 Odd.or.Even::val
call vararg int32 sscanf(string,int8*,,int32*)
#else
ldstr "%d"
ldsflda int32 Odd.or.Even::val
call vararg int32 sscanf(string,string,,int32*)
#endif
stloc.0
leave.s DidntBlowUp
// Guarded block ends
}
catch [mscorlib]System.Exception
{ // Exception handler begins
pop
ldstr "KABOOM!"
call PrintLine
leave.s Return
} // Exception handler ends
DidntBlowUp:
ldloc.0
brfalse.s Error
ldsfld int32 Odd.or.Even::val
ldc.i4.1
and
brfalse.s ItsEven
ldstr "odd!"
br.s PrintAndReturn
ItsEven:
ldstr "even!"
br.s PrintAndReturn
Error:
ldstr "How rude!"
PrintAndReturn:
call PrintLine
ldloc.0
brtrue.s AskForNumber
Return:
ret
} // End of method
} // End of class
#ifdef USE_MAPPED_FIELD
//----------- Global items
.field public staticvaluetype CharArray8 Format at FormatData
//----------- Data declaration
.data FormatData = bytearray(25 64 00 00 00 00 00 00)
//----------- Value type as placeholder
.class public explicit value CharArray8 { .size 8 }
//----------- Calling unmanaged code
.method public static pinvokeimpl ("msvcrt.dll" cdecl)
vararg int32 sscanf(string,int8*) { }
#else
//------------ Calling unmanaged code
.method public static pinvokeimpl("msvcrt.dll" cdecl)
vararg int32 sscanf(string,string) { }
#endif
这个示例显示了如何使用有条件的编译指令#ifdef,#else和#endif。你只需要去除对#define USE_MAPPED_FIELD指令的注释,从而跳转回——传递这个格式化字符串到sscanf作为一个字节数组(如同第一章的Simple.il示例)。而且如果你去除对#define BLOW_UP指令的注释,你就可以模拟一个非托管代码中的严重崩溃(正如在第二章Simplew.il示例中的)。
由ILAsm 2.0编译器支持的编译控制指令,包括以下内容:
#include "MyHeaderFile.il":有效地将MyHeader.dl文件的内容插入到当前的位置。我说“有效”是因为ILAsm 2.0的编译控制指令实际上并不是预处理指令,因此不会在C/C++编译期间直接创建预处理文件。取代的,ILAsm编译器,遇到了一个#include指令,挂起对当前源文件的语法分析,并跳转到这个被包含的文件。一旦处理完这个被包含的文件,编译器会继续对当前文件的语法分析。注意到被包含的文件的名称必须被包含在双引号中。#include <MyHeaderFile.il>是不被支持的。编译器会在当前目录下和被包括的路径里寻找被包含的文件,这可以在命令行选项 /INC=<include_path>中或者在环境变量ILASM_INCLUDE中详细指出。可选择的,你可以在#include指令之中详细指出被包含的文件的路径。在这种情形中,即使这个路径被详细指出,编译器也不会寻找被包括的路径。如果被包括的文件没有被找到,编译器就会放弃编译。
#define SYM1:定义了一个名为SYM1的编译控制符号。你只能在源代码中定义一次编译控制符号;这里没有命令行选项来设置这些符号(然而,我认为这是个冗长的过程)。
#define SYM2 "SomeText":定义了一个名为SYM2的代替符号,带有"SomeText"的内容。这个内容必须总是位于双引号中。编译器会替换源代码中带有SomeText的SYM2的每次出现。这个代替符号的内容可以是任何你喜欢的,但是它不能是语法单元的一部分(如一个数字,一个名称或一个关键字),只能是一个完整的语法单元或其联合。例如,你可以写出如下的代码:
ldflda MyFld
#undef SYM1:未定义一个命名为SYM1的编译控制符号或代替符号。你不能在下面的代码词法中使用SYM1这个指令。但是你可以通过另一个#define指令重新定义SYM1,并在那之后再次使用它。
#ifdef SYM:如果SYM已经定义了,就编译下面的代码。
#ifndef SYM:如果SYM还没定义,就编译下面的代码。
#else:我不认为这个指令有什么需要解释的。
#endif:结束#ifdef/#ifndef或#ifdef/#ifndef-#else块。你可以嵌套这个有条件的编译块,就像C/C++那样。
对当前类和它所相关的引用
这看上去是不是有点可笑——在Odd.Or.Even类的方法成员check中,我们不得不将这个类的真实字段成员解释为Odd.Or.Even::val?
ILAsm 2.0提供了三种特殊的关键字来指向当前的类,当前类的父类,以及当前类作为嵌套类所在的外包类(第7章介绍了更多细节)。
关键字.this表示当前的类(不是当前类的实例,就像C++/C#中的关键字this)。当在类的外部使用时,这个关键字会引起一个编译错误。在这个示例的代码中,而不是下面的代码:
你可以这样使用:
关键字.base表示当前类的父类。这个关键字也必须只能在类的范围内使用,而这个类必须有一个父类(一个显示或隐式的extends子句)。我并没有在这个示例中使用指向到父类Odd.Or.Even的引用,但是这样的引用在代码中通常是众多的。例如,Odd.Or.Even类并没有构造函数,因为它只有静态成员,因此你不需要实例化这个类。但是大多数类是有构造函数的,而且这些类的构造函数第一步要做的,是调用其父类的构造函数。即使你在类的构造函数中也不需要做什么特殊的事情,你仍然需要一个默认的构造函数,如果你打算实例化一个类。下面的示例定义了一个默认的构造函数,这在IL编程中是非常有用的:
".method public specialname void .ctor()
{ ldarg.0; call instance void .base::.ctor(); ret;}"
(你注意到这个分号了么?ILAsm 2.0允许使用分号以增强代码的可读性,就像你想把一些指令放在同一行的时候。分号并不是必须的,而除了分割作用并没有其他的角色。)
而在DFFLT_CTOR符号被定义之后,你可以使用它在每次你需要声明一个默认的某个类的构造函数时:
{
DEFLT_CTOR
}
.class public D.E extends [mscorlib]System.Object
{
DEFLT_CTOR
}
你能使用别名(.typedef指令)来定义DFFLT_CTOR么?不可以,因为DFFLT_CTOR的定义包括了.base,这就只能定义在类的范围,并意味着不同的类在不同的类的范围。这是.typedef和#define指令间主要的不同。前者为一个特定的元数据项提供了一个可选择的名称(类、方法、字段、自定义特性),而后者只提供了“一个命名的文本”,被插入到源代码中,并根据上下文来解释。
关键字.nester表示一个嵌套类的外包类。一个嵌套类是定义在另一个类的范围内的类(第7章介绍了更多细节)。这个关键字只可以使用在嵌套类的范围内。