• CLR怎样实现虚方法的多态调用(1)


    最近一直对.net framework中,虚方法的调用是如何实现这个问题有些疑惑,在看了Essential .Net关于Method的那一章和Artech推荐的文章Drill Into .NET Framework Internals to See How the CLR Creates Runtime Objects以后,还是一知半解,有些疑惑得不到答案。主要有这些:
    •   父类定义的非虚方法是否在子类中有拷贝?
    •   虚方法是如何实现多态的?
    •   子类继承父类的虚方法实现是否和继承非虚方法机制相同?
    •   如果子类隐藏了父类的虚方法,这又是怎样实现的?

      当然问题不止这么多,关于接口方面还有很多很多疑惑,不过时间有限,一下也没办法全部弄清楚,有时间慢慢研究。我主要使用Windbg工具来跟踪调试,关于这个工具如何使用,Google一下就会有很多了。

      这些都是我自己研究加上参考资料所得,如果有不对的地方,希望大家讨论指出。

      首先看下面这段代码:

     public class Base
     {
         public virtual void VirtualFun1()
         {
             Console.WriteLine("Base.VirtualFun1");
         }
         public void NoneVirtualFun1()
         {
             System.Console.WriteLine("Base.NoneVirtualFun1");
         }
         public virtual void VirtualFun2()
         {
             System.Console.WriteLine("Base.VirtualFun2");
         }
         public virtual void VirtualFun3()
         {
             System.Console.WriteLine("Base.VirtualFun3");
         }
     }
    
     public class Derived : Base
     {
         public override void VirtualFun1()
         {
             Console.WriteLine("Derived.VirtualFun1");
         }
         public new virtual void VirtualFun2()
         {
             System.Console.WriteLine("Derived.VirtualFun2");
         }
         public virtual void VirtualFun4()
         {
             System.Console.WriteLine("Derived.VirtualFun4");
         }
    
     }
    

    Base类是基类,它包含三个虚方法VirtualFun1, VirtuaFun2, VirtualFun3和一个非虚方法NoneVirtualFun1。

    Derived继承Base类,它重写了VirtualFun1虚方法,隐藏了Base类的VirtualFun2虚方法,然后又增加了VirtualFun4虚方法。

    看看一个Base类的实例在内存中是怎样排布的:

    image

      Object Ref表示某Base实例的引用,它指向在GC Heap中分配的Base对象,这个对象可以分为三部分:同步块索引、类型指针和字段。主要来关注类型指针,它指向该类型的Method Table,这其实是在Load Heap中分配的Type类型对象,所有该类型的实例的类型指针都指向同一个Method Table(这里表示所有Base对象的类型指针都指向同一个Method Table)。

      Method Table里面包含很多信息,这里关注有关Method这一区域,(如果想了解更详细的method table,请参考上面的文章)。

      根据在Method Table里的信息,可以知道它包含9个Method(其实应该有个字段标示有多少个虚方法,这里就没画了)。接下来就是这些method,它分为两部分,前面一部分是所有的虚方法,后面的是非虚方法。因为所有的类型都是继承自System.Object类,所以前四个方法是Object类的虚方法(ToString, Equals, GetHashCode, Finalize),接着是Base类定义的三个虚方法(VirtualFun1, VirtualFun2, VirtualFun3),最后是Base类的非虚方法NoneVirtualFun1以及默认的构造函数。下面再来看看Derived类型的Method Table:

    image

    仔细对比一下这两个Method Table,可以发现这样几个特点:

    • Base类中的所有虚方法在Derived类的Method Table中一一对应
    • Base类中的所有非虚方法在Derived类中的Method Table并没有拷贝(这一点回答了上面的第一个问题)
    • Derived类新增的虚方法都添加到继承自Base类的虚方法的后面
    • 如果Derived类override Base类的虚方法,它就将该方法指向自身的实现
    • 如果Derived类使用new关键字隐藏了Base类虚方法的实现,它就相当于增加了一个虚方法,而不是覆盖。

    下面看看调用虚方法时如何实现多态,比如有这样一段代码

      Base b = new Derived();

      b.VirtualFun1();

    编译后在我的机器上会生成这样的汇编代码:

      mov ecx, esi

      mov eax, dword ptr[ecx]

      call dword ptr [eax + 3ch]

      现在来解释这几句代码:mov ecx, esi 是将新构造的对象的地址保存在ecx寄存器中; mov eax, dword ptr[ecx] 表示ecx的值是一个指针(根据上面的图可以知道对象的头4个字节保存的是method table的地址),它将method table的地址保存到eax寄存器中,最后call dword ptr[eax + 3ch]。3ch表示偏移量,它表示该方法相对于该method table的偏址,是在该类型加载到load heap以后确定的。这样,由method table的地址加上method相对与method table的偏移量,就可以唯一确定一个方法。

      这样在调用b.VirtualFun1(); 时,由于b是Derived类的实例,所以根据它指向的托管对象找到的method table是Derived类型的method table,就能正确调用该方法。因为Derived类中override了VirtualFun1这个虚方法,所以调用的是Derived类的实现,而如果没有override基类的虚方法,它就指向基类的该方法的实现。

      由此可以看出,CLR实现虚方法的机制主要是通过类型的method table加上该虚方法相对于method table的偏移量来确定调用具体方法的。一个虚方法在整个继承体系所有类型对应的method table中的偏移量是固定的,比如VirtualFunc1在Base类型的method table中的偏移量是3ch,它在Derived类型的method table中的偏移量也是3ch,如果还有继承自Derived类的类,也是同样,利用这种机制就实现了多态。

      结论

    •   每个类型对应一个Method Table
    •   子类的Method Table中包含父类的所有虚方法,而不包含父类的非虚方法
    •   CLR根据对象找到它对应类型的method table,然后根据该虚方法在method table中的偏移量实现多态调用。

    在上一篇文章CLR怎样实现虚方法的多态调用(1)中主要介绍了CLR怎样多态调用虚方法以及各种类型的方法在Method Table中的排布,但是没有介绍怎样调用接口方法,当某个对象向上转型为接口时进行多态调用时,CLR是怎样实现的呢?以下面这段代码为例来说明:
    namespace Demo
    {
        public interface IFoo
        {
            void Foo();
        }
    
        public class Base : IFoo
        {
            public void Foo()
            {
                Console.WriteLine("In base's Foo function");
            }
        }
    
    
        class Program
        {
            static void Main(string[] args)
            {
                IFoo i = new Base();
                i.Foo();
            }
        }
    }

      在Essential .NET中,Don Box向读者简单描述了基于接口的多态调用,在堆中有一个全局接口映射表,当某个类实现了一个接口,就会在这个接口表中增加项,而增加的这些项又指向这个具体类的Method Table中的Method,可能说的不是太清楚,就用个图来表示:

    image

     当进行方法调用的时候,首先通过对象找到该类型的Method Table,根据偏移量找到指向Interface Offset Table的指针来定位这个Interface Offset Table,然后CLR查找调用方法在这个Offset Table的偏移量,最后调用该方法。调用的汇编代码如下:

     mov ecx, esi  -- 保存对象地址到ecx中

     mov eax, dword ptr [ecx] -- 把类型的Method Table的地址保存在eax中

     mov eax, dword ptr [eax+0ch] -- 把Interface Offset Table的地址保存在eax中

     mov eax, dword ptr [eax + interface offset] -- 根据Interface在Table中的偏移量,找到其地址并保存到eax中

     call dword ptr [eax +  method offset] -- 根据该方法的偏移量定位改方法进行调用

    可以说这样的调用逻辑是很清楚容易让人理解的。

    但是当我用windbg进行跟踪的时候却发现接口方法调用机制和上面所说的不同,并没有一个查找Interface Offset Table的过程,在Main函数里是这样的调用:

     mov ecx, esi --  保存对象地址到ecx中

     call dword ptr ds:[980010h]  在数据段980010h上保存的是一个指针,实际上调用的是:

     jmp mscorwks!ResolveWorkerAsmStub

     可以看到跳转到ResolveWorkerAsmStub函数里去了。而这个函数是做什么的呢,下面的代码是从SSCLI里面找到的(有兴趣的可以看看virtualcallstubcpu.hpp):

    __declspec (naked) void ResolveWorkerAsmStub()

    {

    // 首先保存寄存器状态

    call VirtualCallStubManager::ResolveWorkerStatic //调用ResolveWorkerStatic方法
    //还原寄存器状态
    jmp eax //eax保存着实际上要调用的方法的地址,所以这里就开始了方法调用

    }

    所以猜想到在VirtualCallStubManager::ResolveWorkerStatic函数里面正确找到了方法的地址,保存在eax里。

    看来到底是怎样取到该方法地址这个问题只能等下次有时间再用windbg跟踪。如果有人了解,也希望能解释一下来帮助我解答疑惑。

  • 相关阅读:
    20191331 《信息安全专业导论》第12周学习总结
    20191331 《信息安全专业导论》第11周学习总结
    20191331 《信息安全专业导论》第10周学习总结
    20191331 《信息安全专业导论》第9周学习总结
    20191331《信息安全专业导论》第7周学习总结
    20191331 《信息安全专业导论》第6周学习总结
    20191331《信息安全专业导论》第5周学习总结
    20191331 《信息安全专业导论》第4周学习总结
    2013暑假心愿单
    怎样产生一个app的idea?
  • 原文地址:https://www.cnblogs.com/tangself/p/1623632.html
Copyright © 2020-2023  润新知