CSDN博客

img flier_lu

用WinDbg探索CLR世界 [4] 方法的调用机制

发表于2004/7/7 19:56:00  754人阅读

http://www.blogcn.com/user8/flier_lu/index.html?id=1745355
http://www.blogcn.com/user8/flier_lu/index.html?id=1745373
http://www.blogcn.com/user8/flier_lu/index.html?id=1745407


    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   | (06)000006       */ newobj     instance void B::.ctor()
   IL_0005:  /* 74   | (1B)000001       */ castclass  class A
   IL_000a:  /* 25   |                  */ dup
   IL_000b:  /* 25   |                  */ dup
   IL_000c:  /* 28   | (06)000003       */ call       instance void A::Foo()
   IL_0011:  /* 6F   | (06)000004       */ callvirt   instance void A::Bar()
   IL_0016:  /* 6F   | (06)000005       */ callvirt   instance void A::Baz()
   IL_001b:  /* 2A   |                  */ ret
 } // end of method 'Global Functions'::Exec
 



     可以看到直接调用时 call 和 callvirt 指令,都是以方法的 Token 为参数的。但不同之处在于实现上,call指令使用类型的方法表,而 callvirt 使用对象的方法表。
     在 WinDbg 载入 Virt_not.exe 后,可以在 Exec 被 JIT 编译后,使用 !ip2md 命令查看其方法描述信息,如
 

以下为引用:

 0:000> g; !clrstack
 Breakpoint 0 hit
 Thread 0
 ESP       EIP
 0012f694  791d6a4a [FRAME: PrestubMethodFrame] [DEFAULT] [hasThis] Void A.Foo()
 0012f6a4  06d90088 [DEFAULT] Void Exec()
 0012f9b0  791da717 [FRAME: GCFrame]
 0012fa94  791da717 [FRAME: GCFrame]

 0:000> !ip2md 06d90088
 MethodDesc: 0x00975070
 Jitted by normal JIT
 Method Name : [DEFAULT] Void Exec()
 MethodTable 975078
 Module: 15cd20
 mdToken: 06000001 (C:DevelopMS.NetBooksInside Microsoft .NET IL Assembler CodeVirt_not.EXE)
 Flags : 10
 Method VA : 06d90058
 



     反汇编 Exec 方法的代码如下:
 

以下为引用:

 0:000> u 06d90058
 06d90058 55               push    ebp
 06d90059 8bec             mov     ebp,esp

 // newobj instance void B::.ctor()
 06d9005b 56               push    esi
 06d9005c b9a8519700       mov     ecx,0x9751a8 // 类 B 的方法表地址
 06d90061 e8b21fbdf9       call    00962018
 06d90066 8bf0             mov     esi,eax

 06d90068 8bce             mov     ecx,esi
 06d9006a ff15ec519700     call    dword ptr [009751ec]

 // castclass class A
 06d90070 8bd6             mov     edx,esi
 06d90072 b900519700       mov     ecx,0x975100 // 类 A 的方法表地址
 06d90077 e8a00b4672       call    mscorwks!JIT_ChkCastClass (791f0c1c)

 06d9007c 8bf0             mov     esi,eax      // 对象地址
 06d9007e 90               nop
 06d9007f 90               nop

 // call       instance void A::Foo()
 06d90080 8bce             mov     ecx,esi
 06d90082 ff1544519700     call    dword ptr [00975144]

 // callvirt   instance void A::Bar()
 06d90088 8bce             mov     ecx,esi
 06d9008a 8b01             mov     eax,[ecx]
 06d9008c ff5038           call    dword ptr [eax+0x38]

 // callvirt   instance void A::Baz()
 06d9008f 8bce             mov     ecx,esi
 06d90091 8b01             mov     eax,[ecx]
 06d90093 ff503c           call    dword ptr [eax+0x3c]

 06d90096 90               nop
 06d90097 5e               pop     esi
 06d90098 5d               pop     ebp
 06d90099 c3               ret
 



     可以看到 call 指令是通过一个绝对地址的间接寻址调用函数的,此调用指向代码如下:
 

以下为引用:

 0:000> dd 00975144
 00975144  009750d3 00000000 00000000 00000000

 0:000> u 009750d3
 009750d3 e808857dff       call    0014d5e0

 0:000> u 0014d5e0
 0014d5e0 52               push    edx
 0014d5e1 68f0301b79       push    0x791b30f0
 0014d5e6 55               push    ebp
 0014d5e7 53               push    ebx
 0014d5e8 56               push    esi
 0014d5e9 57               push    edi
 0014d5ea 8d742410         lea     esi,[esp+0x10]
 0014d5ee 51               push    ecx
 0014d5ef 52               push    edx
 0014d5f0 648b1d2c0e0000   mov     ebx,fs:[00000e2c]
 0014d5f7 8b7b08           mov     edi,[ebx+0x8]
 0014d5fa 897e04           mov     [esi+0x4],edi
 0014d5fd 897308           mov     [ebx+0x8],esi
 0014d600 56               push    esi
 0014d601 e844940879       call    mscorwks!PreStubWorker (791d6a4a)
 0014d606 897b08           mov     [ebx+0x8],edi
 



     呵呵,这不正是上次分析的调用JIT的包装代码吗?

     在进行了 JIT 之后,上面的 Exec 代码调用 A::Foo 方法体被JIT修改为:
 

以下为引用:

 0:000> dd 975144
 00975144  009750d3 00000000 00000000 00000000

 0:000> u 009750d3
 009750d3 e9f8af4106       jmp     06d900d0

 0:000> !ip2md 06d900d0
 MethodDesc: 0x009750d8
 Jitted by normal JIT
 Method Name : [DEFAULT] [hasThis] Void A.Foo()
 MethodTable 975100
 Module: 15cd20
 mdToken: 06000003 (C:DevelopMS.NetBooksInside Microsoft .NET IL Assembler CodeVirt_not.EXE)
 Flags : 0
 Method VA : 06d900d0
 



     也就是说 call 指令实际上是直接对 JIT 后的 A::Foo 方法体的代码进行了调用。

     而 callvirt 指令则使用两段的间接寻址来调用方法。
 

以下为引用:

 // callvirt   instance void A::Bar()
 06d90088 8bce             mov     ecx,esi
 06d9008a 8b01             mov     eax,[ecx]
 06d9008c ff5038           call    dword ptr [eax+0x38]
 



     这里的 esi 是指向对象的指针,而对象结构的第一个 DWORD 保存指向实际类型方法表的指针,也就是《本质论》中所说的 RuntimeTypeHandle (具体分析请参看我以前的一篇文章《Type, RuntimeType and RuntimeTypeHandle 》)。而方法表的 0x38 偏移处内容如下:
 

以下为引用:

 0:000> !dumpmt -md 00975100
 EEClass : 06c63344
 Module : 0015cd20
 Name: A
 mdToken: 02000002  (C:DevelopMS.NetBooksInside Microsoft .NET IL Assembler CodeVirt_not.EXE)
 MethodTable Flags : 80000
 Number of IFaces in IFaceMap : 0
 Interface Map : 0097514c
 Slots in VTable : 8
 --------------------------------------
 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()
 009750e3 009750e8    None   [DEFAULT] [hasThis] Void A.Bar()
 009750f3 009750f8    None   [DEFAULT] [hasThis] Void A.Baz()
 009750c3 009750c8    None   [DEFAULT] [hasThis] Void A..ctor()
 009750d3 009750d8    None   [DEFAULT] [hasThis] Void A.Foo()

 0:000> dd 00975100
 00975100  00080000 0000000c 06c63344 00000000
 00975110  00120000 0015cd20 0006ffff 0097514c
 00975120  00000000 00000008 79b7c4eb 79b7c473
 00975130  79b7c48b 79b7c52b 009750e3 009750f3
 00975140  009750c3 009750d3 00000000 00000000
 



     可以看到 00975100+0x38 正好是 A.Bar() 方法的入口地址
 

以下为引用:

 0:000> u 009750e3
 009750e3 e8f8847dff       call    0014d5e0

 0:000> u 14d5e0
 0014d5e0 52               push    edx
 ...
 0014d600 56               push    esi
 0014d601 e844940879       call    mscorwks!PreStubWorker (791d6a4a)
 0014d606 897b08           mov     [ebx+0x8],edi

 0:000> !dumpmd 009750e8
 Method Name : [DEFAULT] [hasThis] Void A.Bar()
 MethodTable 975100
 Module: 15cd20
 mdToken: 06000004 (C:DevelopMS.NetBooksInside Microsoft .NET IL Assembler CodeVirt_not.EXE)
 Flags : 0
 IL RVA : 0000205e
 



     因此 callvirt 指令实际上是使用变量实际保存对象的类型的方法表在进行调用,也就是我们所说的虚函数语义。

 


    再回头看前面那个 C# 代码的例子,在 JIT 完成之后:
 

以下为引用:

 .method private hidebysig static void  Main(string[] args) cil managed
 // SIG: 00 01 01 1D 0E
 {
   .entrypoint
   .custom instance void [mscorlib]System.STAThreadAttribute::.ctor() = ( 01 00 00 00 )
   // Method begins at RVA 0x2120
   // Code size       47 (0x2f)
   .maxstack  1
   .locals init ([0] class flier.Base b,
            [1] class flier.Base d,
            [2] class flier.IFoo i)
   IL_0000:  /* 73   | (06)000007       */ newobj     instance void flier.Base::.ctor()
   IL_0005:  /* 0A   |                  */ stloc.0
   IL_0006:  /* 73   | (06)00000B       */ newobj     instance void flier.Derived::.ctor()
   IL_000b:  /* 0B   |                  */ stloc.1
   IL_000c:  /* 06   |                  */ ldloc.0
   IL_000d:  /* 6F   | (06)000003       */ callvirt   instance void flier.Base::CallFromObjBase()
   IL_0012:  /* 07   |                  */ ldloc.1
   IL_0013:  /* 6F   | (06)000003       */ callvirt   instance void flier.Base::CallFromObjBase()
   IL_0018:  /* 07   |                  */ ldloc.1
   IL_0019:  /* 6F   | (06)000004       */ callvirt   instance void flier.Base::CallFromObjDerived()
   IL_001e:  /* 06   |                  */ ldloc.0
   IL_001f:  /* 0C   |                  */ stloc.2
   IL_0020:  /* 08   |                  */ ldloc.2
   IL_0021:  /* 6F   | (06)000001       */ callvirt   instance void flier.IFoo::CallFromIntfBase()
   IL_0026:  /* 07   |                  */ ldloc.1
   IL_0027:  /* 0C   |                  */ stloc.2
   IL_0028:  /* 08   |                  */ ldloc.2
   IL_0029:  /* 6F   | (06)000002       */ callvirt   instance void flier.IFoo::CallFromIntfDerived()
   IL_002e:  /* 2A   |                  */ ret
 } // end of method EntryPoint::Main

 0:000> !ip2md 06d900a7
 MethodDesc: 0x00975070
 Jitted by normal JIT
 Method Name : [DEFAULT] Void flier.EntryPoint.Main(SZArray String)
 MethodTable 975088
 Module: 167d98
 mdToken: 0600000c (D:TempCallItCallItinDebugCallIt.exe)
 Flags : 10
 Method VA : 06d90058

 0:000> u 06d90058
 06d90058 55               push    ebp
 06d90059 8bec             mov     ebp,esp
 06d9005b 83ec10           sub     esp,0x10
 06d9005e 57               push    edi
 06d9005f 56               push    esi
 06d90060 53               push    ebx
 06d90061 894dfc           mov     [ebp-0x4],ecx
 06d90064 c745f800000000   mov     dword ptr [ebp-0x8],0x0
 06d9006b 33f6             xor     esi,esi
 06d9006d 33ff             xor     edi,edi

 // newobj     instance void flier.Base::.ctor()
 06d9006f b9d8519700       mov     ecx,0x9751d8          // 类 flier.Base 的方法表
 06d90074 e89f1fbdf9       call    00962018
 06d90079 8bd8             mov     ebx,eax
 06d9007b 8bcb             mov     ecx,ebx
 06d9007d ff1520529700     call    dword ptr [00975220]  // call flier.Base::.ctor()
 06d90083 895df8           mov     [ebp-0x8],ebx         // stloc.0

 // newobj     instance void flier.Derived::.ctor()
 06d90086 b988529700       mov     ecx,0x975288          // 类 flier.Derived 的方法表
 06d9008b e8881fbdf9       call    00962018
 06d90090 8bd8             mov     ebx,eax
 06d90092 8bcb             mov     ecx,ebx
 06d90094 ff15d8529700     call    dword ptr [009752d8]  // call flier.Derived::.ctor()
 06d9009a 8bf3             mov     esi,ebx               // stloc.1

 06d9009c 8b4df8           mov     ecx,[ebp-0x8]         // ldloc.0
 06d9009f 3909             cmp     [ecx],ecx
 06d900a1 ff151c529700     call    dword ptr [0097521c]  // callvirt   instance void flier.Base::CallFromObjBase()

 06d900a7 8bce             mov     ecx,esi               // ldloc.1
 06d900a9 3909             cmp     [ecx],ecx
 06d900ab ff151c529700     call    dword ptr [0097521c]  // callvirt   instance void flier.Base::CallFromObjBase()

 06d900b1 8bce             mov     ecx,esi               // ldloc.1
 06d900b3 8b01             mov     eax,[ecx]
 06d900b5 ff5038           call    dword ptr [eax+0x38]  // callvirt   instance void flier.Base::CallFromObjDerived()

 06d900b8 8b7df8           mov     edi,[ebp-0x8]         // ldloc.0
 06d900bb 8bcf             mov     ecx,edi               // stloc.2
 06d900bd 8b01             mov     eax,[ecx]
 06d900bf 8b400c           mov     eax,[eax+0xc]
 06d900c2 8b402c           mov     eax,[eax+0x2c]
 06d900c5 ff10             call    dword ptr [eax]       // callvirt   instance void flier.IFoo::CallFromIntfBase()

 06d900c7 8bfe             mov     edi,esi               // ldloc.1
 06d900c9 8bcf             mov     ecx,edi               // stloc.2
 06d900cb 8b01             mov     eax,[ecx]
 06d900cd 8b400c           mov     eax,[eax+0xc]
 06d900d0 8b402c           mov     eax,[eax+0x2c]
 06d900d3 ff5004           call    dword ptr [eax+0x4]   // callvirt   instance void flier.IFoo::CallFromIntfDerived()

 06d900d6 90               nop
 06d900d7 5b               pop     ebx
 06d900d8 5e               pop     esi
 06d900d9 5f               pop     edi
 06d900da 8be5             mov     esp,ebp
 06d900dc 5d               pop     ebp
 06d900dd c3               ret
 


 

     除了刚刚分析过的 call 和对虚函数的 callvirt 指令外,这里又多出一种对接口虚函数进行调用的操作。
 

以下为引用:

 06d900bb 8bcf             mov     ecx,edi               // stloc.2
 06d900bd 8b01             mov     eax,[ecx]             // 载入对象地址指向对象结构头部(04aa1b4c)字段指向的类型信息地址
 06d900bf 8b400c           mov     eax,[eax+0xc]         // 载入全局接口偏移量表基址
 06d900c2 8b402c           mov     eax,[eax+0x2c]        // 获取 IFoo 接口映射表偏移量
 06d900c5 ff10             call    dword ptr [eax]       // callvirt   instance void flier.IFoo::CallFromIntfBase()
 



     使用 WinDbg 动态跟踪到上述指令处
 

以下为引用:

 0:000> !dumpstackobjects
 ESP/REG  Object   Name
 ebx      04aa1b74 flier.Derived
 ecx      04aa2804 System.IO.TextWriter/SyncTextWriter
 esi      04aa1b74 flier.Derived
 edi      04aa1b68 flier.Base
 0012f6a0 04aa1b68 flier.Base
 0012f6a4 04aa1b4c System.Object[]
 0012f6d8 04aa1b4c System.Object[]
 0012f928 04aa1b4c System.Object[]
 0012f92c 04aa1b4c System.Object[]
 



     edi 指向 flier.Base 类型的对象实例(0x04aa1b68)
 

以下为引用:

 0:000> !dumpobj 04aa1b68
 Name: flier.Base
 MethodTable 0x009751d8
 EEClass 0x06c6334c
 Size 12(0xc) bytes
 mdToken: 02000003  (D:TempCallItCallItinDebugCallIt.exe)

 0:000> dd 04aa1b68
 04aa1b68  009751d8 00000000 00000000 00975288
 04aa1b78  00000000 80000000 79b7daf8 00000015
 



     而此对象的偏移 0 处保存着此对象的类型信息地址(0x009751d8)
 

以下为引用:

 0:000> !dumpmt 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

 0:000> dd 009751d8
 009751d8  00080000 0000000c 06c6334c 0097bff0
 009751e8  00120001 00167d98 0008ffff 00975228
 


 

     类型信息的 0xC 偏移处是全局接口偏移量表的入口基址 (0x0097bff0)
 

以下为引用:

 0:000> dd 0097bff0
 0097bff0  ???????? ???????? ???????? ????????
 0097c000  00000000 0097c000 00004000 00000000
 0097c010  00000000 000003e8 00000001 00975214
 0097c020  009752cc 00000000 00000000 00000000
 



     而 IFoo 接口的物理地址就在此偏移量表的 0x2C 偏移处(0x00975214)。这个地址是直接指向 flier.Base 类的虚方法表。
 

以下为引用:

 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()
 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()

 0:000> dd 009751d8
 009751d8  00080000 0000000c 06c6334c 0097bff0
 009751e8  00120001 00167d98 0008ffff 00975228
 009751f8  00000000 00000009 79b7c4eb 79b7c473
 00975208  79b7c48b 79b7c52b 0097519b 009751ab
 00975218  009751bb 0097518b 009751cb 00000000
 00975228  00975138 00050001 00000000 00000000
 00975238  00975288 00000000 00000003 00000000
 00975248  e8000008 ff7d9110 00000009 c00020c4
 


 

     0x0097519b 就是最后 flier.Base.CallFromObjDerived() 函数的入口地址。因此对于接口进行调用的 callvirt 指令,实际上是遵循以下的 dispatch 路线完成调用的:

     ObjectPtr -> Object -> Class -> Global Interface Map Table -> Class Method Table

     具体的结构图请参考《本质论》167面的图 (6.5 - 0.1), -_-b

     至此,CLR 中最常见的三种函数调用方式就大致分析完毕,以后有机会在继续分析其他的如jmp、间接调用和 tail call等方式的实现。

0 0

相关博文

我的热门文章

img
取 消
img