聊聊 C# 中的多态底层 (虚方法调用) 是怎么玩的
最近在看 C++ 的虚方法调用实现原理,大概就是说在 class 的首位置存放着一个指向 vtable array 指针数组 的指针,而 vtable array 中的每一个指针元素指向的就是各自的 虚方法,实现方式很有意思,哈哈,现在我很好奇 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() 处下一个断点,观察一下它的反汇编代码:
D:\net6\ConsoleApplication2\ConsoleApp1\Program.cs @ 9:
05cf21b3 b93c5dce05 mov ecx,5CE5D3Ch (MT: ConsoleApp1.Chinese)
05cf21b8 e8030f89fa call 005830c0 (JitHelp: CORINFO_HELP_NEWSFAST)
05cf21bd 8945f4 mov dword ptr [ebp-0Ch],eax
05cf21c0 8b4df4 mov ecx,dword ptr [ebp-0Ch]
05cf21c3 e820fbffff call 05cf1ce8 (ConsoleApp1.Chinese..ctor(), mdToken: 0600000A)
05cf21c8 8b4df4 mov ecx,dword ptr [ebp-0Ch]
05cf21cb 894df8 mov dword ptr [ebp-8],ecx
D:\net6\ConsoleApplication2\ConsoleApp1\Program.cs @ 11:
>>> 05cf21ce 8b4df8 mov ecx,dword ptr [ebp-8]
05cf21d1 8b45f8 mov eax,dword ptr [ebp-8]
05cf21d4 8b00 mov eax,dword ptr [eax]
05cf21d6 8b4028 mov eax,dword ptr [eax+28h]
05cf21d9 ff5010 call dword ptr [eax+10h]
05cf21dc 90 nop
从汇编代码看,逻辑非常清晰,大体步骤如下:
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
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
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() 方法。
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 方法啦。
3. 总结
本质上来说,CoreCLR 也是 C++ 写的,所以也逃不过用 虚表 来实现多态的玩法, 不过玩法也稍微复杂了一些,希望本篇对大家有帮助。
聊聊 C# 中的多态底层 (虚方法调用) 是怎么玩的的更多相关文章
- 【原创】SystemVerilog中的多态和虚方法
封装可以隐藏实现细节,使代码模块化,继承可以扩展已经存在的代码模块,目的都是为了代码重用.多态是为了实现接口的重用.在SystemVerilog中,子类和父类之间多个子程序使用同一个名字的现象称为Sy ...
- C#-面向对象的三大特性——多态(虚方法与重写、抽象类、接口)
多态 同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果.在运行时,可以通过指向基类的指针,来调用实现派生类中的方法. 编译时的多态性:编译时的多态性是通过重载来实现的.对于非虚的成员来说 ...
- C#中的抽象方法和虚方法有什么区别?
抽象方法是只有定义.没有实际方法体的函数,它只能在抽象函数中出现,并且在子类中必须重写:虚方法则有自己的函数体,已经提供了函数实现,但是允许在子类中重写或覆盖.重写的子类虚函数就是被覆盖了.
- C#学习笔记(十四):多态、虚方法和抽象类
虚方法/非虚方法 < 实例方法 = 非静态方法 = 非类方法(非实例方法 = 静态方法 = 类方法) 函数签名(参数列表,或参数列表 + 返回类型) using System; using Sy ...
- 通过实例聊聊Java中的多态
Java中的多态允许父类指针指向子类实例.如:Father obj=new Child(); 那么不禁要发问?? 使用这个父类型的指针访问类的属性或方法时,如果父类和子类都有这个名称的属性或方法,哪 ...
- CLR 虚方法调用和接口方法调用
不知接口方法和虚方法分发有什么区别?似乎在CIL中都是callvirt指令. 对,MSIL里都是callvirt,但JIT的时候得到了不同的处理:对虚方法的分发是编译成这样: mov ecx, es ...
- C++中的多态与虚函数的内部实现
1.什么是多态 多态性可以简单概括为“一个接口,多种行为”. 也就是说,向不同的对象发送同一个消息, 不同的对象在接收时会产生不同的行为(即方法).也就是说,每个对象可 ...
- 转载总结 C# 多态(虚方法,抽象,接口实现)
前言:我们都知道面向对象的三大特性:封装,继承,多态.封装和继承对于初学者而言比较好理解,但要理解多态,尤其是深入理解,初学者往往存在有很多困惑,为什么这样就可以?有时候感觉很不可思议,由此,面向对象 ...
- C#中的抽象方法,虚方法,接口之间的对比
1.首先来看一看抽象类 抽象类是特殊的类,不能够被实例化:具有类的其他特性:抽象方法只能声明于抽象类中,且不包含任何实现 (就是不能有方法体),派生类也就是子类必须对其进行重写.另外,抽象类可以派生自 ...
随机推荐
- Java/C++实现代理模式---婚介所
婚介所其实就是找对象的一个代理,请仿照我们的课堂例子"论坛权限控制代理"完成这个实际问题,其中如果年纪小于18周岁,婚介所会提示"对不起,不能早恋!",并终止业 ...
- 大数据学习之路之ambari配置(四)
试了很多遍,内存还是不够,电脑不太行的,不建议用ambari!!! 放弃了
- rem,px,em最大的区别;
px:px像素(Pixel).相对长度单位.像素px是相对于显示器屏幕分辨率而言的.移动端的分辨率很多.所以px不适用移动端em:em的值不固定:其长度继承父级元素的字体大小rem:相对于根元素htm ...
- MySQL安装配置教程(超级详细)
一. 下载MySQL Mysql官网下载地址:https://downloads.mysql.com/archives/installer/ 1. 选择要安装的版本,本篇文章选择的是5.7.31版本, ...
- linux中sort、uniq、cut、tr、wc命令的使用
文本处理命令 1.sort命令 使用场景 : 用于将文件内容加以排序(可以和cat一起用) 参数 作用 -n 依照数值的大小排序 -r 以相反的顺序来排序(默认只比较第一个数,-rn是按所有数值比较) ...
- 2019-2020 10th BSUIR Open Programming Championship. Semifinal
2019-2020 10th BSUIR Open Programming Championship. Semifinal GYM链接https://codeforces.com/gym/103637 ...
- Java 图像处理(一)
曾几何时,Java图像处理已经被认为是太过鸡肋,就连Java的创始公司,在java图像处理方面也是浅尝辄止,可能相比较C++,Java在这方面的处理,确实差强人意. 不过Java类库中有一个叫JAI的 ...
- JavaScript深入理解系列:call与apply
定义 call和apply:函数调动call()方法在执行的时候,函数的里面的this会指向第一个参数值,除第一个参数值后面的若干支都是传进该函数,简而言之就是改变函数运行时的this指向. 使用示例 ...
- 微服务状态之python巡查脚本开发
背景 由于后端微服务架构,于是各种业务被拆分为多个服务,服务之间的调用采用RPC接口,而Nacos作为注册中心,可以监听多个服务的状态,比如某个服务是否down掉了.某个服务的访问地址是否改变.以及流 ...
- python基础练习题(题目 有序列表插入元素)
day26 --------------------------------------------------------------- 实例039:有序列表插入元素 题目 有一个已经排好序的数组. ...