C# 中的多态底层虚方法调用详情

目录
  • 一、C# 中的多态玩法
    • 1. 一个简单的 C# 例子
    • 2. 汇编代码分析
      • (1)eax,dword ptr [ebp-8]
      • (2)eax,dword ptr [eax]
      • (3)eax,dword ptr [eax+28h]
      • (4)call dword ptr [eax+10h]
  • 三、总结

前言:

本质上来说,CoreCLR 也是 C++ 写的,所以也逃不过用 虚表 来实现多态的玩法, 不过玩法也稍微复杂了一些,希望本篇对大家有帮助。

一、C# 中的多态玩法

1. 一个简单的 C# 例子

为了方便说明,我就定义一个 Person 类和一个 Chinese 类,详细代码如下:

internal class Program
    {
        static void Main(string[] args)
        {
            Person person = new Chinese();

            person.SayHello();

            Console.ReadLine();
        }
    }

    public class Person
    {
        public virtual void SayHello()
        {
            Console.WriteLine("sayhello");
        }
    }

    public class Chinese: Person
    {
        public override void SayHello()
        {
            Console.WriteLine("chinese");
        }
    }
}

2. 汇编代码分析

接下来用 windbg 在 person.SayHello() 处下一个断点,观察一下它的反汇编代码:

internal class Program
    {
        static void Main(string[] args)
        {
            Person person = new Chinese();

            person.SayHello();

            Console.ReadLine();
        }
    }

    public class Person
    {
        public virtual void SayHello()
        {
            Console.WriteLine("sayhello");
        }
    }

    public class Chinese: Person
    {
        public override void SayHello()
        {
            Console.WriteLine("chinese");
        }
    }
}

从汇编代码看,逻辑非常清晰,大体步骤如下:

(1)eax,dword ptr [ebp-8]

从栈上(ebp-8)处获取 person 在堆上的首地址,如果不相信的话,可以用 !do 027ea88c 试试看。

0:000> dp ebp-8 L1
0057f300  027ea88c
0:000> !do 027ea88c
Name:        ConsoleApp1.Chinese
MethodTable: 05ce5d3c
EEClass:     05cd3380
Size:        12(0xc) bytes
File:        D:\net6\ConsoleApplication2\ConsoleApp1\bin\x86\Debug\net6.0\ConsoleApp1.dll
Fields:
None

(2)eax,dword ptr [eax]

如果大家了解 实例 在堆上的内存布局的话,应该知道,这个首地址存放的就是 methodtable 指针,我们可以用 !dumpmt 05ce5d3c 来验证下。

0:000> dp 027ea88c L1
027ea88c  05ce5d3c

0:000> !dumpmt 05ce5d3c
EEClass:         05cd3380
Module:          05addb14
Name:            ConsoleApp1.Chinese
mdToken:         02000007
File:            D:\net6\ConsoleApplication2\ConsoleApp1\bin\x86\Debug\net6.0\ConsoleApp1.dll
BaseSize:        0xc
ComponentSize:   0x0
DynamicStatics:  false
ContainsPointers false
Slots in VTable: 6
Number of IFaces in IFaceMap: 0

(3)eax,dword ptr [eax+28h]

那这句话是什么意思呢?如果你了解 CoreCLR 的话,你应该知道 methedtable 是由一个 class MethodTable 类来承载的,所以它取了 methodtable 偏移 0x28  位置的一个字段,那这个偏移字段是什么呢?我们先用 dt 把 methodtable 结构给导出来。

0:000> dt 05ce5d3c MethodTable
coreclr!MethodTable
   =7ad96bc8 s_pMethodDataCache : 0x00639ec8 MethodDataCache
   =7ad96bc4 s_fUseParentMethodData : 0n1
   =7ad96bcc s_fUseMethodDataCache : 0n1
   +0x000 m_dwFlags        : 0xc
   +0x004 m_BaseSize       : 0x74088
   +0x008 m_wFlags2        : 5
   +0x00a m_wToken         : 0
   +0x00c m_wNumVirtuals   : 0x5ccc
   +0x00e m_wNumInterfaces : 0x5ce
   +0x010 m_pParentMethodTable : IndirectPointer<MethodTable *>
   +0x014 m_pLoaderModule  : PlainPointer<Module *>
   +0x018 m_pWriteableData : PlainPointer<MethodTableWriteableData *>
   +0x01c m_pEEClass       : PlainPointer<EEClass *>
   +0x01c m_pCanonMT       : PlainPointer<unsigned long>
   +0x020 m_pPerInstInfo   : PlainPointer<PlainPointer<Dictionary *> *>
   +0x020 m_ElementTypeHnd : 0
   +0x020 m_pMultipurposeSlot1 : 0
   +0x024 m_pInterfaceMap  : PlainPointer<InterfaceInfo_t *>
   +0x024 m_pMultipurposeSlot2 : 0x5ce5d68
   =7ad04c78 c_DispatchMapSlotOffsets : [0]  " $ (System.Private.CoreLib.dll"
   =7ad04c70 c_NonVirtualSlotsOffsets : [0]  " $ ($((, $ (System.Private.CoreLib.dll"
   =7ad04c60 c_ModuleOverrideOffsets : [0]  " $ ($((,$((,(,,0 $ ($((, $ (System.Private.CoreLib.dll"
   =7ad12838 c_OptionalMembersStartOffsets : [0]  "(((((((,(((,(,,0(((,(,,0(,,0,004"

从 methodtable 的布局图来看, eax+28h 是 m_pMultipurposeSlot2 结构的第二个字段了,因为第一个字段是 虚方法表指针,如果要验证的话,也很简单,用 !dumpmt -md 05ce5d3c 把所有的方法给导出来,然后结合 dp 05ce5d3c 看下 0x5ce5d68 之后是不是许多的方法。

0:000> !dumpmt -md 05ce5d3c
EEClass:         05cd3380
Module:          05addb14
Name:            ConsoleApp1.Chinese
mdToken:         02000007
File:            D:\net6\ConsoleApplication2\ConsoleApp1\bin\x86\Debug\net6.0\ConsoleApp1.dll
BaseSize:        0xc
ComponentSize:   0x0
DynamicStatics:  false
ContainsPointers false
Slots in VTable: 6
Number of IFaces in IFaceMap: 0
--------------------------------------
MethodDesc Table
   Entry MethodDe    JIT Name
02610028 02605568   NONE System.Object.Finalize()
02610030 02605574   NONE System.Object.ToString()
02610038 02605580   NONE System.Object.Equals(System.Object)
02610050 026055ac   NONE System.Object.GetHashCode()
05CF1CE0 05ce5d24   NONE ConsoleApp1.Chinese.SayHello()
05CF1CE8 05ce5d30    JIT ConsoleApp1.Chinese..ctor()
0:000> dp 05ce5d3c L10
05ce5d3c  00000200 0000000c 00074088 00000005
05ce5d4c  05ce5ccc 05addb14 05ce5d7c 05cd3380
05ce5d5c  05cf1ce8 00000000 05ce5d68 02610028
05ce5d6c  02610030 02610038 02610050 05cf1ce0

仔细看输出,上面的 05ce5d68 后面的 02610028 就是 System.Object.Finalize() 方法,02610030 对应着 System.Object.ToString() 方法。

(4)call dword ptr [eax+10h]

有了前面的基础,这句话就好理解了,它是从 m_pMultipurposeSlot2 结构中找 SayHello 所在的单元指针位置,然后做 call 调用。

0:000> !U 05cf1ce0
Unmanaged code
05cf1ce0 e88f9dde74      call    coreclr!PrecodeFixupThunk (7aadba74)
05cf1ce5 5e              pop     esi
05cf1ce6 0001            add     byte ptr [ecx],al
05cf1ce8 e913050000      jmp     05cf2200
05cf1ced 5f              pop     edi
05cf1cee 0300            add     eax,dword ptr [eax]
05cf1cf0 245d            and     al,5Dh
05cf1cf2 ce              into
05cf1cf3 0500000000      add     eax,0
05cf1cf8 0000            add     byte ptr [eax],al

从汇编看,它还是一段 桩代码,言外之意就是该方法没有被 JIT 编译,如果编译完了,这里的 05CF1CE0 05ce5d24 NONE ConsoleApp1.Chinese.SayHello()  的 Entry (05CF1CE0) 也会被同步修改,验证一下很简单,我们继续 go 代码让其编译完成,然后再 dumpmt 。

0:008> !dumpmt -md 05ce5d3c
EEClass:         05cd3380
Module:          05addb14
Name:            ConsoleApp1.Chinese
mdToken:         02000007
File:            D:\net6\ConsoleApplication2\ConsoleApp1\bin\x86\Debug\net6.0\ConsoleApp1.dll
BaseSize:        0xc
ComponentSize:   0x0
DynamicStatics:  false
ContainsPointers false
Slots in VTable: 6
Number of IFaces in IFaceMap: 0
--------------------------------------
MethodDesc Table
   Entry MethodDe    JIT Name
02610028 02605568   NONE System.Object.Finalize()
02610030 02605574   NONE System.Object.ToString()
02610038 02605580   NONE System.Object.Equals(System.Object)
02610050 026055ac   NONE System.Object.GetHashCode()
05CF2270 05ce5d24    JIT ConsoleApp1.Chinese.SayHello()
05CF1CE8 05ce5d30    JIT ConsoleApp1.Chinese..ctor()

0:008> dp 05ce5d3c L10
05ce5d3c  00000200 0000000c 00074088 00000005
05ce5d4c  05ce5ccc 05addb14 05ce5d7c 05cd3380
05ce5d5c  05cf1ce8 00000000 05ce5d68 02610028
05ce5d6c  02610030 02610038 02610050 05cf2270

此时可以看到它由 05cf1ce0 变成了 05cf2270, 这个就是 JIT 编译后的方法代码,我们用 !U 反编译下。

0:008> !U 05cf2270
Normal JIT generated code
ConsoleApp1.Chinese.SayHello()
ilAddr is 05E720D5 pImport is 008F6E88
Begin 05CF2270, size 27

D:\net6\ConsoleApplication2\ConsoleApp1\Program.cs @ 28:
>>> 05cf2270 55              push    ebp
05cf2271 8bec            mov     ebp,esp
05cf2273 50              push    eax
05cf2274 894dfc          mov     dword ptr [ebp-4],ecx
05cf2277 833d74dcad0500  cmp     dword ptr ds:[5ADDC74h],0
05cf227e 7405            je      05cf2285
05cf2280 e8cb2bf174      call    coreclr!JIT_DbgIsJustMyCode (7ac04e50)
05cf2285 90              nop

D:\net6\ConsoleApplication2\ConsoleApp1\Program.cs @ 29:
05cf2286 8b0d74207e04    mov     ecx,dword ptr ds:[47E2074h] ("chinese")
05cf228c e8dffbffff      call    05cf1e70
05cf2291 90              nop

D:\net6\ConsoleApplication2\ConsoleApp1\Program.cs @ 30:
05cf2292 90              nop
05cf2293 8be5            mov     esp,ebp
05cf2295 5d              pop     ebp
05cf2296 c3              ret

终于这就是多态下的 ConsoleApp1.Chinese.SayHello 方法啦。

三、总结

到此这篇关于C# 中的多态底层虚方法调用详情的文章就介绍到这了,更多相关 C# 多态内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C# 面向对象三大特性:封装、继承、多态

    面向对象有封装.继承.多态这三个特性,面向对象编程按照现实世界的特点来管理复杂的事物,把它们抽象为对象,具有自己的状态和行为,通过对消息的反应来完成任务.这种编程方法提供了非常强大的多样性,大大增加了代码的重用机会,增加了程序开发的速度,将具备独立性特制的程序代码包装起来,修改部分程序代码时不至于会影响到程序的其他部分. 1.封装 每个对象都包含它进行操作所需要的所有信息,封装只公开代码单元的对外接口,而隐藏其具体实现,尽量不对外公开代码.使用封装有很多好处,从设计角度来讲,封装可以对外屏蔽一些

  • 详解c# 多态

    多态是同一个行为具有多个不同表现形式或形态的能力. 多态性意味着有多重形式.在面向对象编程范式中,多态性往往表现为"一个接口,多个功能". 多态性可以是静态的或动态的.在静态多态性中,函数的响应是在编译时发生的.在动态多态性中,函数的响应是在运行时发生的. 在 C# 中,每个类型都是多态的,因为包括用户定义类型在内的所有类型都继承自 Object. 多态就是同一个接口,使用不同的实例而执行不同操作,如图所示: 现实中,比如我们按下 F1 键这个动作: 如果当前在 Flash 界面下弹出

  • C#类的多态性详解

    第一种:编译时的多态性,直接这样说不知道说啥?程序执行过程主要分为三步:编译,链接,运行.在编译时体现的多态性是通过方法的重载来实现的.方法的重载我们听过,就是一个类对象调用若干同名,但参数类型.个数不同.类对象是根据参数类型,个数来决定调用哪个同名方法,实现何种操作.C#语言的方法重载与C++.java基本是一样的,但是C#还包含C++的运算符重载,java里面没有这个概念. 第二种:运行时的多态性,是在系统运行时,调用一个名字相同,参数类型也相同以及个数也完全一样的方法,但会完成不同的操作.

  • C# 最基础知识介绍--多态

    目录 一.C# 多态性 二.静态多态性 三.函数重载 四.C# 运算符重载 1.运算符重载的实现 2.可重载和不可重载运算符 五.动态多态性 前言:

  • C#多态的三种实现方式(小结)

    C#实现多态主要有3种方法,虚方法,抽象类,接口 1 虚方法 在父类的方法前面加关键字virtual, 子类重写该方法时在方法名前面加上override关键字,例如下面的Person类的SayHello方法 class Person { public Person(string name) { this.Name = name; } string _name; public string Name { get => _name; set => _name = value; } //父类方法加v

  • C#多态详解

    目录 1.定义 2.实现多态的两个因素 3.案例 4.使用多态的好处 5.string类 1.值类型 2.引用类型 6.运算符重载: 总结 1.定义 多态是同一个行为具有多个不同表现形式或形态的能力. 多态性意味着有多重形式.在面向对象编程范式中,多态性往往表现为"一个接口,多个功能". 多态性可以是静态的或动态的.在静态多态性中,函数的响应是在编译时发生的.在动态多态性中,函数的响应是在运行时发生的. 静态多态性:在编译时,方法和对象的连接机制被称为早期绑定,也被称为静态绑定.C#

  • C# 中的多态底层虚方法调用详情

    目录 一.C# 中的多态玩法 1. 一个简单的 C# 例子 2. 汇编代码分析 (1)eax,dword ptr [ebp-8] (2)eax,dword ptr [eax] (3)eax,dword ptr [eax+28h] (4)call dword ptr [eax+10h] 三.总结 前言: 本质上来说,CoreCLR 也是 C++ 写的,所以也逃不过用 虚表 来实现多态的玩法, 不过玩法也稍微复杂了一些,希望本篇对大家有帮助. 一.C# 中的多态玩法 1. 一个简单的 C# 例子 为

  • C++中的多态与虚函数的内部实现方法

    1.什么是多态 多态性可以简单概括为"一个接口,多种行为". 也就是说,向不同的对象发送同一个消息, 不同的对象在接收时会产生不同的行为(即方法).也就是说,每个对象可以用自己的方式去响应共同的消息.所谓消息,就是调用函数,不同的行为就是指不同的实现,即执行不同的函数.这是一种泛型技术,即用相同的代码实现不同的动作.这体现了面向对象编程的优越性. 多态分为两种: (1)编译时多态:主要通过函数的重载和模板来实现. (2)运行时多态:主要通过虚函数来实现. 2.几个相关概念 (1)覆盖.

  • 基于多态之虚方法、抽象类、接口详解

    虚方法: 1.在父类方法的返回值前加 virtual 关键字,标记为虚方法,表示这个方法可以被子类重写. 2.虚方法必须有方法体,方法体中可以没有任何内容. 3.子类可以根据需求选择性的是否重写虚方法.如果需要重写,在子类方法的返回值前加 override 关键字. 4.子类在重写虚方法时,可以根据需求选择性的是否使用 base 关键字调用父类中的该方法. 虚方法语法格式如下: public class Father { public virtual void Do() { //..... }

  • java中的4种循环方法示例详情

    目录 java循环结构 1.while循环 2.do-while循环 3.for循环 4.java 增强for循环 java循环结构 顺序结构的程序语句只能 被执行一次.如果你要同样的操作执行多次,就需要使用循环结构. java中有三种主要的循环结构: while 循环 do...while 循环 for 循环 在java5中引入一种主要用于数组的增强型for循环. 1.while循环 while是最基本的循环,它的结构为: package com.example.lesson1; //whil

  • C#中多态现象和多态的实现方法

    本文实例讲述了C#中多态现象和多态的实现方法.分享给大家供大家参考.具体分析如下: 面向对象的特征封装.继承和多态.Polymorphism(多态性)来源于希腊单词,指"多种形态".多态性的一个重要特征是方法的调用是在运行时确定而不是编译时.在.NET中用于实现多态性的关键词有virtual.override.abstract.interface. 一.virtual实现多态   shape类是通用的基类,draw是一个虚方法,每个派生类都可以有自己的override版本,在运行时可以

  • C# 构造函数如何调用虚方法

    谜题 在C#中,用virtual关键字修饰的方法(属性.事件)称为虚方法(属性.事件),表示该方法可以由派生类重写(override).虚方法是.NET中的重要概念,可以说在某种程度上,虚方法使得多态成为可能. 然而虚方法的使用却存在着很大学问,如果滥用的话势必对程序产生很大的负面影响.比如下面这个例子: public class Puzzle { public Puzzle() { Name = "Virtual member call in constructor"; Solve(

  • PHP5中实现多态的两种方法实例分享

    在PHP5中,变量的类型是不确定的,一个变量可以指向任何类型的数值.字符串.对象.资源等.我们无法说PHP5中多态的是变量. 我们只能说在PHP5中,多态应用在方法参数的类型提示位置.一个类的任何子类对象都可以满足以当前类型作为类型提示的类型要求.所有实现这个接口的类,都可以满足以接口类型作为类型提示的方法参数要求.简单的说,一个类拥有其父类.和已实现接口的身份. 通过实现接口实现多态 复制代码 代码如下: <?phpinterface User{ // User接口    public fun

  • C#中Abstract方法和Virtual方法的区别

    简介: c#中Abstract和Virtual比较容易混淆,都与继承有关,并且涉及override的使用.virtual可以被子类重写,而abstract必须被子类重写.virtual修饰的方法必须有实现(哪怕是仅仅添加一对大括号),而abstract修饰的方法一定不能实现.它们有一个共同点:如果用来修饰方法,前面必须添加public,要不然就会出现编译错误:虚拟方法或抽象方法是不能私有的.毕竟加上virtual或abstract就是让子类重新定义的,而private成员是不能被子类访问的.下面

  • js中实现多态采用和继承类似的方法

    多态的实现可以采用和继承类似的方法.首先定义一个抽象类,其中调用一些虚方法,虚方法在抽象类中没用定义,而是通过其具体的实现类来实现. 如下面的例子: Object.extend=function(destination,source){ for(property in source){ destination[property]=source[property]; } return destination; } //定义一个抽象基类base,无构造函数 function base(){}; ba

  • C++中关于多态实现和使用方法

    目录 赋值兼容 实例 多态 静多态 动多态 格式 实例 override 纯虚函数 含有虚函数的析构函数 注意事项 RTTI typeid typecast 多态实现 虚函数 一般继承(no override) 一般继承(override) 过程推断 都说 C++ 是面向对象的语言,其中的面向对象主要包括三部分:继承,封装,多态.继承和封装我们之前就简单介绍过,这里主要对多态的使用方法做一个简单说明. 赋值兼容 赋值兼容说的是在使用基类对象的地方可以使用公有继承类的对象来代替.赋值兼容是一种默认

随机推荐