Don Box在《.NET本质论 第1卷:公共语言运行库》的第6章里,详细地解说了 CLR 中方法地调用机制的原理;qqchen在其 BLog 上也有一篇不错的介绍 CLR 中方法调用分类的文章《CLR Drilling Down: The Overhead of Method Calls 》。但因为他们文章的目的不同,故而没有足够深入到让我满足的内部细节,呵呵,只好自己接着分析。:D
我在《用WinDbg探索CLR世界 [3] 跟踪方法的 JIT 过程》一文中介绍了如何使用 WinDbg 跟踪 Don Box 所描述的 JIT 过程。本文中将使用前文所介绍的 WinDbg 功能进一步分析 CLR 中方法的调用机制。
首先我们来看一个简单的例子,其中有两个类和一个接口的定义,并使用了几种不同的调用类型进行方法调用:
以下为引用:
using System;
namespace flier { public interface IFoo { void CallFromIntfBase(); void CallFromIntfDerived(); }
public class Base : IFoo { public void CallFromObjBase() { System.Console.WriteLine("Base.CallFromObjBase"); }
public virtual void CallFromObjDerived() { System.Console.WriteLine("Base.CallFromObjDerived"); }
public void CallFromIntfBase() { System.Console.WriteLine("Base.IFoo.CallFromIntfBase"); } public virtual void CallFromIntfDerived() { System.Console.WriteLine("Base.IFoo.CallFromIntfDerived"); } }
public class Derived : Base, IFoo { public new void CallFromObjBase() { System.Console.WriteLine("Derived.CallFromObjBase"); }
public override void CallFromObjDerived() { System.Console.WriteLine("Derived.CallFromObjDerived"); }
public override void CallFromIntfDerived() { System.Console.WriteLine("Derived.IFoo.CallFromIntfDerived"); } }
class EntryPoint { [STAThread] static void Main(string[] args) { Base b = new Base(), d = new Derived();
b.CallFromObjBase();
d.CallFromObjBase(); d.CallFromObjDerived();
IFoo i = (IFoo) b;
i.CallFromIntfBase();
i = (IFoo)d;
i.CallFromIntfDerived(); } } }
将之编译成 CallIt.exe 后用 WinDbg 启动调试之。进入调试后,可以使用 sos 的 !name2ee 命令查看指定类型的当前状态,如: 以下为引用:
0:000> !name2ee CallIt.exe flier.Derived -------------------------------------- MethodTable: 00975288 EEClass: 06c63414 Name: flier.Derived
使用 !dumpclass 命令进一步查看类型详细信息: 以下为引用:
0:000> !dumpclass 06c63414 Class Name : flier.Derived mdToken : 02000004 () Parent Class : 06c6334c ClassLoader : 0015ee08 Method Table : 00975288 Vtable Slots : 9 Total Method Slots : b Class Attributes : 100001 : Flags : 1000003 NumInstanceFields: 0 NumStaticFields: 0 ThreadStaticOffset: 0 ThreadStaticsSize: 0 ContextStaticOffset: 0 ContextStaticsSize: 0
可以发现 Derived 类型有 11 个 Method Slot,但只有 9 个 Vtable Slot。使用 !dumpmt 进一步查看之: 以下为引用:
0:000> !dumpmt -md 00975288 EEClass : 06c63414 Module : 00167d98 Name: flier.Derived mdToken: 02000004 (D:TempCallItCallItinDebugCallIt.exe) MethodTable Flags : 80000 Number of IFaces in IFaceMap : 1 Interface Map : 009752e0 Slots in VTable : 11 -------------------------------------- MethodDesc Table Entry MethodDesc JIT Name 79b7c4eb 79b7c4f0 None [DEFAULT] [hasThis] String System.Object.ToString() 79b7c473 79b7c478 None [DEFAULT] [hasThis] Boolean System.Object.Equals(Object) 79b7c48b 79b7c490 None [DEFAULT] [hasThis] I4 System.Object.GetHashCode() 79b7c52b 79b7c530 None [DEFAULT] [hasThis] Void System.Object.Finalize() 0097525b 00975260 None [DEFAULT] [hasThis] Void flier.Derived.CallFromObjDerived() 009751ab 009751b0 None [DEFAULT] [hasThis] Void flier.Base.CallFromIntfBase() 0097526b 00975270 None [DEFAULT] [hasThis] Void flier.Derived.CallFromIntfDerived() // 以下开始为 IFoo 接口方法表 009751ab 009751b0 None [DEFAULT] [hasThis] Void flier.Base.CallFromIntfBase() 0097526b 00975270 None [DEFAULT] [hasThis] Void flier.Derived.CallFromIntfDerived() // 以下开始为非虚方法表 0097524b 00975250 None [DEFAULT] [hasThis] Void flier.Derived.CallFromObjBase() 0097527b 00975280 None [DEFAULT] [hasThis] Void flier.Derived..ctor()
可以看到正如 Don Box 在书中所说,类型的方法表是分为虚方法表和非虚方法表两部分的。前面 9 个 Method Slot 组成 Derived 的 VTable,后两个 Slot 保存非虚方法。检查 Base 类的情况也是类似: 以下为引用:
0:000> !name2ee CallIt.exe flier.Base -------------------------------------- MethodTable: 009751d8 EEClass: 06c6334c Name: flier.Base
0:000> !dumpclass 06c6334c Class Name : flier.Base mdToken : 02000003 () Parent Class : 79b7c3c8 ClassLoader : 0015ee08 Method Table : 009751d8 Vtable Slots : 7 Total Method Slots : 9 Class Attributes : 100001 : Flags : 1000003 NumInstanceFields: 0 NumStaticFields: 0 ThreadStaticOffset: 0 ThreadStaticsSize: 0 ContextStaticOffset: 0 ContextStaticsSize: 0
0:000> !dumpmt -md 009751d8 EEClass : 06c6334c Module : 00167d98 Name: flier.Base mdToken: 02000003 (D:TempCallItCallItinDebugCallIt.exe) MethodTable Flags : 80000 Number of IFaces in IFaceMap : 1 Interface Map : 00975228 Slots in VTable : 9 -------------------------------------- MethodDesc Table Entry MethodDesc JIT Name 79b7c4eb 79b7c4f0 None [DEFAULT] [hasThis] String System.Object.ToString() 79b7c473 79b7c478 None [DEFAULT] [hasThis] Boolean System.Object.Equals(Object) 79b7c48b 79b7c490 None [DEFAULT] [hasThis] I4 System.Object.GetHashCode() 79b7c52b 79b7c530 None [DEFAULT] [hasThis] Void System.Object.Finalize() 0097519b 009751a0 None [DEFAULT] [hasThis] Void flier.Base.CallFromObjDerived() // 以下开始为 IFoo 接口方法表 009751ab 009751b0 None [DEFAULT] [hasThis] Void flier.Base.CallFromIntfBase() 009751bb 009751c0 None [DEFAULT] [hasThis] Void flier.Base.CallFromIntfDerived() // 以下开始为非虚方法表 0097518b 00975190 None [DEFAULT] [hasThis] Void flier.Base.CallFromObjBase() 009751cb 009751d0 None [DEFAULT] [hasThis] Void flier.Base..ctor()
而对于每个接口,实际上 CLR 是单独维护了一个方法表的。如 Base 类的方法表中指出,地址 0x009752e0 处有一个接口方法映射表,查看其内容如下: 以下为引用:
0:000> dd 0x009752e0 009752e0 00975138 00070001 00000000 00000000
每个接口映射表表项由2个DWORD组成,头一个DWORD就是接口方法表的地址。 以下为引用:
0:000> !dumpmt -md 00975138 EEClass : 06c633b0 Module : 00167d98 Name: flier.IFoo mdToken: 02000002 (D:TempCallItCallItinDebugCallIt.exe) MethodTable Flags : 80000 Number of IFaces in IFaceMap : 0 Interface Map : 0097516c Slots in VTable : 2 -------------------------------------- MethodDesc Table Entry MethodDesc JIT Name 009750eb 009750f0 None [DEFAULT] [hasThis] Void flier.IFoo.CallFromIntfBase() 00975113 00975118 None [DEFAULT] [hasThis] Void flier.IFoo.CallFromIntfDerived()
比较一下就会发现,Base 和 Derived 类的接口映射表指向的接口方法表都是一样的。 以下为引用:
0:000> dd 009752e0 009752e0 00975138 00070001 00000000 00000000
0:000> dd 00975228 00975228 00975138 00050001 00000000 00000000
只是接口映射表表项第2个 DWORD 的高 WORD 指名此接口在原方法表中的起始索引(Base 为 5,Derived 为 7)不同。这正符合《本质论》中167页那张图所示的接口映射表结构。
在了解了方法表的物理结构后,我们接着分析方法的动态调用机制。
从方法的调用类型来分,CLR支持直接调用、间接调用和很少见的 tail call 模式。
直接调用最为常见,又可分为使用虚方法表的 callvirt 指令和不使用虚方法表的 call 和 jmp 指令。 间接调用稍微少见,通过 ldftn/calli 和 ldvirtftn/calli 两组指令,从栈中获取方法描述 (Method Desc),语义上等同于 call/callvirt 指令。 tail call 调用更为少见,类似于 jmp,但是作为前缀指令附加在 call/calli/callvirt 指令上的。
下面我们对最常见的直接调用方式做一个简单的分析,首先看看一个例子程序 Virt_not.il:
以下为引用:
.assembly extern mscorlib { } .assembly virt_not { } .module virt_not.exe
.class public A { .method public specialname void .ctor() { ret } .method public void Foo() { ldstr "A::Foo" call void [mscorlib]System.Console::WriteLine(string) ret } .method public virtual void Bar() { ldstr "A::Bar" call void [mscorlib]System.Console::WriteLine(string) ret } .method public virtual void Baz() { ldstr "A::Baz" call void [mscorlib]System.Console::WriteLine(string) ret } }
.class public B extends A { .method public specialname void .ctor() { ret } .method public void Foo() { ldstr "B::Foo" call void [mscorlib]System.Console::WriteLine(string) ret } .method public virtual void Bar() { ldstr "B::Bar" call void [mscorlib]System.Console::WriteLine(string) ret } .method public virtual newslot void Baz() { ldstr "B::Baz" call void [mscorlib]System.Console::WriteLine(string) ret } }
.method public static void Exec() { .entrypoint newobj instance void B::.ctor() // create instance of derived class castclass class A // cast it to base class
dup // we need 3 instance pointers dup // on stack for 3 calls
call instance void A::Foo() callvirt instance void A::Bar() callvirt instance void A::Baz()
ret }
上述代码是使用 IL 汇编直接编写,其 Exec 函数将被编译成 IL 代码如下:
以下为引用:
.method public static void Exec() cil managed // SIG: 00 00 01 { .entrypoint // Method begins at RVA 0x209c // Code size 28 (0x1c) .maxstack 8 IL_0000: /* 73 |
关键词: 用WinDbg探索CLR世界 [4] 办法的调用机制