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.Base0: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: 00: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 000000000:000> dd 00975228
00975228 00975138 00050001 00000000 00000000
只是接口映射表表项第2个 DWORD 的高 WORD 指名此接口在原方法表中的起始索引(Base 为 5,Derived 为 7)不同。这正符合《本质论》中167页那张图所示的接口映射表结构。