跟小D每日学口语

用ILDasm.exe深入理解委托

.Net框架SDK中提供的IL(Intermediate Language:中间语言)反汇编工具(ILDasm.exe)来查看得到的托管PE文件中的元数据和IL代码。.Net控件的核心就是公共语言运行时 (Common Language Runtime,简称CLR)。CLR在运行时对编程语言是一无所知的,因为我们在开发时所用的编程语言无论使用何种语言都要最终生成一个托管模块 (managed module)。托管模块是一个需要CLR才能执行的标准Windows可移植可执行(portable executable,简称PE)文件。
通过查看程序的IL代码和元数据可以了解我们自己编写的代码究竟 都干了些什么。特别是在调试我们的程序时就会真正体现出它的威力。同时在我们日常学习时也不妨多用用ILDasm.exe工具,在.Net学习中有时它会 给我们意想不到的收获。
首先让我们来认识一下什么是ILdasm.exe,及其使用方 法。
可以按如下程序操作以在.Net中调用ILdasm。点击“开 始”→“程序”→“ Microsoft Visual Studio .NET 2003” →“Visual Studio .NET 工具” →“Visual Studio .NET 2003 命令提示”,这时会出现一个窗口,输入命令:ILdasm,按回车即可调出ILdasm工具。当然比较方便的是直接将它(命令提示)拖放到任务栏上,再输 入ILdasm就好了。
在打开ILdasm工具后,你可以看到三个工具栏:文件,视图, 帮助。点击文件下拉菜单打开一个你编译生成的.exe或.dll文件等PE文件后你会看到如下图的一个界面:

这是我用ILdasm打开Visual Studio .NET自带的C#教程版本控制程序(做了小幅改动)调试后生成的version.exe文件后的树视图显示。这些红红蓝蓝的符号是什么意思呢?
查看帮助后我们知道这些是树视图图标,含义如下图:
?
不用说,你一定看出来了这里有两个类。MANIFEST是清单, 它显示了一些程序集的版本、发布者等等信息。不过暂时对我们调试程序还没有什么帮助,我们可以先不去管他。
现在我们来看看这个程序的源程序:
public class MyBase
{
   public virtual string Meth1()
   {
       return "MyBase-Meth1";
   }
   public virtual string Meth2()
   {
       return "MyBase-Meth2";
   }
   public virtual string Meth3()
   {
       return "MyBase-Meth3";
   }
}
class MyDerived : MyBase
{
   // 使用 override 关键字重写虚方法 Meth1:
   public override string Meth1()
   {
       return "MyDerived-Meth1";
   }
   // 使用 new 关键字显式隐藏
   // 虚方法 Meth2:
   public new string Meth2()
   {
       return "MyDerived-Meth2";
   }
   // 由于下面声明中没有指定任何关键字
   // 因此将发出一个警告来提醒程序员
   // 此方法隐藏了继承的成员 MyBase.Meth3():
   public string Meth3()
   {
       return "MyDerived-Meth3";
   }
   public static void Main()
   {
      MyDerived mD = new MyDerived();
       MyBase mB = (MyBase)mD;
       System.Console.WriteLine(mD.Meth1());
       System.Console.WriteLine(mD.Meth2());
       System.Console.WriteLine(mD.Meth3());
       System.Console.WriteLine("以上为类 MyDerived的显示结果!\n");
       System.Console.WriteLine(mB.Meth1());
       System.Console.WriteLine(mB.Meth2());
       System.Console.WriteLine(mB.Meth3());
       System.Console.WriteLine("以上为 MyBase的显示结果!\n");
       System.Console.WriteLine("按任意键 退出...");
      System.Console.ReadLine();
   }
}
希望这段程序没有让你厌烦,它是使用 override 和 new 关键字来演示 C# 中的版本控制的一段程序。可以在.Net下编译出如下所示的结果:
为什么基类MyBase和派生类MyDerived的方法 Meth1显示完全相同呢?而且基类MyBase的方法Meth1不显示“MyBase-Meth1”呢?这里面到底发生了什么?
C# 语言被设计为不同库中的基类和派生类之间的版本控制可以衍生,并保持向后兼容。例如,这意味着在基类中引入与派生类中的某个成员名称相同的新成员不是错 误。它还意味着类必须显式声明某方法是要重写一个继承方法,还是一个仅隐藏具有类似名称的继承方法的新方法。
在 C# 中,默认情况下方法不是虚拟的。若要使方法成为虚拟方法,必须在基类的方法声明中使用 virtual 修饰符。然后,派生类可以使用 override 关键字重写基虚拟方法,或使用 new 关键字隐藏基类中的虚拟方法。如果 override 关键字和 new 关键字均未指定,编译器将发出警告,并且派生类中的方法将隐藏基类中的方法。
所以我们看到派生类MyDerived用override关键字 重写虚方法 Meth1,导致基类MyBase的方法Meth1改变。而MyDerived其他的两个方法的重写没有给基类带来任何影响。这一点我们将在ILdasm 中清楚地看到。
.ctor (对象构造器 object constructor),这里就是类的构造器,可以不去看。我们先来看看MyDerived类的main方法的IL代码。双击main:viod()就 可以弹出该方法的IL代码如下:
.method public hidebysig static void Main() cil managed
{
.entrypoint
// 代码大小 111 (0x6f)
.maxstack 1
.locals init ([0] class MyDerived mD,
[1] class MyBase mB)
IL_0000: newobj instance void MyDerived::.ctor()
IL_0005: stloc.0
IL_0006: ldloc.0
IL_0007: stloc.1
IL_0008: ldloc.0
IL_0009: callvirt instance string MyBase::Meth1()
IL_000e: call void [mscorlib]System.Console::WriteLine(string)
IL_0013: ldloc.0
IL_0014: callvirt instance string MyDerived::Meth2()
IL_0019: call void [mscorlib]System.Console::WriteLine(string)
IL_001e: ldloc.0
IL_001f: callvirt instance string MyDerived::Meth3()
IL_0024: call void [mscorlib]System.Console::WriteLine(string)
IL_0029: ldstr bytearray (E5 4E 0A 4E 3A 4E 7B 7C 4D 00 79 00 44 00 65 00 // .N.N:N{|M.y.D.e.
72 00 69 00 76 00 65 00 64 00 84 76 3E 66 3A 79 // r.i.v.e.d..v>f:y
D3 7E 9C 67 01 FF 0A 00 ) // .~.g....
IL_002e: call void [mscorlib]System.Console::WriteLine(string)
IL_0033: ldloc.1
IL_0034: callvirt instance string MyBase::Meth1()
IL_0039: call void [mscorlib]System.Console::WriteLine(string)
IL_003e: ldloc.1
IL_003f: callvirt instance string MyBase::Meth2()
IL_0044: call void [mscorlib]System.Console::WriteLine(string)
IL_0049: ldloc.1
IL_004a: callvirt instance string MyBase::Meth3()
IL_004f: call void [mscorlib]System.Console::WriteLine(string)
IL_0054: ldstr bytearray (E5 4E 0A 4E 3A 4E 4D 00 79 00 42 00 61 00 73 00 // .N.N:NM.y.B.a.s.
65 00 84 76 3E 66 3A 79 D3 7E 9C 67 01 FF 0A 00 ) // e..v>f:y.~.g....
IL_0059: call void [mscorlib]System.Console::WriteLine(string)
IL_005e: ldstr bytearray (09 63 FB 4E 0F 61 2E 95 00 90 FA 51 2E 00 2E 00 // .c.N.a.....Q....
2E 00 ) // ..
IL_0063: call void [mscorlib]System.Console::WriteLine(string)
IL_0068: call string [mscorlib]System.Console::ReadLine()
IL_006d: pop
IL_006e: ret
} // end of method MyDerived::Main
对于IL代码指令的具体含义请看\Microsoft Visual Studio .NET 2003\SDK\v1.1\Tool Developers Guide\docs下的Common Language Infrastructure规范的Partition III。我们先回头看看源程序处的main内部的“System.Console.WriteLine(mD.Meth1());”语句对应上面的 IL_0009处的“callvirt instance string MyBase::Meth1()”和IL_0034处的一模一样,原来它执行的是MyBase类的Meth1虚方法。而Meth1方法已经在 MyDerived类中重写了,所以这两个类的对应的方法1在本质说上都一样了。
通过看这个例子,我们能更加深入地理解override的功能 了。看来ILdasm的确厉害,与其你想半天不如调IL代码看看,很多问题就会迎刃而解了,拨云见日啊!
下面我们切入正题:委托。
在非托管C/C++中,函数地址其实就是个内存地址,由于该地址 不会携带任何其他信息,如函数期望的参数个数、参数类型、返回值类型等,所以这时的回调函数是非类型安全的。.Net框架为回调函数提供了称为委托 (delegate)的类型安全的机制。
我们来看看这样一段程序:
using System;
namespace MultiDelegate
{
class IntOperations //定义一个类,该类包含两个静态方法
{
public void Twice(int num) //求整数的倍数
{
Console.WriteLine("整数{0}的倍数是 {1}", num,num*2);
}
public static void Square(int num) //求整数的平方
{
Console.WriteLine("整数{0}的平方是 {1}\n", num,num*num);
}
}
delegate void IntOp(int x); //定义一个委托
///
/// Class1 的摘要说明。
///
class MainProgram
{
///
/// 应用程序的主入口点。
///
[STAThread]
static void Main(string[] args)
{
IntOperations mo= new IntOperations();//实例化一个
//IntOperations对象
IntOp operations=new IntOp(mo.Twice);//创建Twice方法
//的委托对象
operations+=new IntOp(IntOperations.Square);
//创建并增加Square方法的委托对象
operations(5);
operations(8);
operations-=new IntOp(IntOperations.Square);//创建并移除Square方法的委托对象
operations(5);
operations(8);
Console.WriteLine("按任意键退 出...");
Console.ReadLine(); //让屏幕暂停,以方便观察结果
}
}
}
这段代码的结构比较简单:首先定义了一个包含两个静态方法的类 IntOperations,然后定义了一个委托IntOp,最后用写了一个类MainProgram来演示结果。
先调试编译,看看结果:

然 后祭出ILdasm.exe,可以看到以下效果:

虽 然从源代码上看,定义委托比较简单,但是通过ILDASM.exe可以看到编译器为委托产生的元数据。
在本例中,编译器实际上是定义了一个名为InOp的类, extends [mscorlib]System.MulticastDelegate 表示它继承于System.MulticastDelegate类。它还有四个方法:构造器 (.ctor),BeginInvoke,EndInvoke,Invoke。
再让我们看看MainProgram类的静态方法Main的反汇 编代码,来看看到底委托是如何运作的:
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
.custom instance void [mscorlib]System.STAThreadAttribute::.ctor() = ( 01 00 00 00 )
// 代码大小 112 (0x70)
.maxstack 4
.locals init ([0] class MultiDelegate.IntOperations mo,
[1] class MultiDelegate.IntOp operations)
IL_0000: newobj instance void MultiDelegate.IntOperations::.ctor()
IL_0005: stloc.0
IL_0006: ldloc.0
IL_0007: ldftn instance void MultiDelegate.IntOperations::Twice(int32)
IL_000d: newobj instance void MultiDelegate.IntOp::.ctor(object, native int)
IL_0012: stloc.1
IL_0013: ldloc.1
IL_0014: ldnull
IL_0015: ldftn void MultiDelegate.IntOperations::Square(int32)
IL_001b: newobj instance void MultiDelegate.IntOp::.ctor(object, native int)
IL_0020: call class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(class[mscorlib]System.Delegate, class [mscorlib]System.Delegate)
IL_0025: castclass MultiDelegate.IntOp
IL_002a: stloc.1
IL_002b: ldloc.1
IL_002c: ldc.i4.5
IL_002d: callvirt instance void MultiDelegate.IntOp::Invoke(int32)
IL_0032: ldloc.1
IL_0033: ldc.i4.8
IL_0034: callvirt instance void MultiDelegate.IntOp::Invoke(int32)
IL_0039: ldloc.1
IL_003a: ldnull
IL_003b: ldftn void MultiDelegate.IntOperations::Square(int32)
IL_0041: newobj instance void MultiDelegate.IntOp::.ctor(object, native int)
IL_0046: call class[mscorlib]System.Delegate[mscorlib]System.Delegate::Remove(class[mscorlib]System.Delegate,class [mscorlib]System.Delegate)
IL_004b: castclass MultiDelegate.IntOp
IL_0050: stloc.1
IL_0051: ldloc.1
IL_0052: ldc.i4.5
IL_0053: callvirt instance void MultiDelegate.IntOp::Invoke(int32)
IL_0058: ldloc.1
IL_0059: ldc.i4.8
IL_005a: callvirt instance void MultiDelegate.IntOp::Invoke(int32)
IL_005f: ldstr bytearray (09 63 FB 4E 0F 61 2E 95 00 90 FA 51 2E 00 2E 00 // .c.N.a.....Q....
2E 00 ) // ..
IL_0064: call void [mscorlib]System.Console::WriteLine(string)
IL_0069: call string [mscorlib]System.Console::ReadLine()
IL_006e: pop
IL_006f: ret
} // end of method MainProgram::Main
源代码中的“operations+=new IntOp(IntOperations.Square);”对应于IL代码中的IL_0020行,就是调用System.Delegate类的 Combine方法,它将一个委托对象组合到一个委托链中去(关于委托链请参见:参考文献1的P377页),委托链上增加了方法Square。不过只有相 同类型的委托才可以组合。同理,“operations-=new IntOp(IntOperations.Square);”对应于代码IL_0046行,调用System.Delegate类的Remove方法从委 托链上移除找到的委托对象。
当然,如果把委托链上所有的方法都移出去,那么委托就没有可以调 用的方法。这个时候如果你在引用这个委托的话那么肯定不能通过编译,因为编译器没有方法可以处理对象。
如果从类的角度考虑委托,那么就会容易理解一些。当然,如果你不 用ILdasm反汇编一下,就看不到背后的秘密了。
所以,蔡学镛说:.Net程序员可以不会用IL Assembly写程序,但是至少要看得懂反汇编出来的IL Assembly Code。
参考文献:
1、 Microsoft .Net框架程序设计(修订版) 李建中(译)
2、 编程技巧与维护 2003年第三期 如何理解和使用C#中的“委托” 刘一丁
3、 Visual Studio .NET 帮助文件
4、 《程序员》杂志03年第2期 .NET中间语言 蔡学镛
posted @ 2012-08-25 11:24  Danny Chen  阅读(1030)  评论(0编辑  收藏  举报