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 (北联网教程,专业提供视频软件下载)
……