0
点赞
收藏
分享

微信扫一扫

聊聊 C# 中的多态底层 (虚方法调用) 是怎么玩的

妖妖妈 2022-06-05 阅读 30

最近在看 ​​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

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

  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
  1. ​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
  1. ​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()​​ 方法。

  1. ​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# 中的多态底层 (虚方法调用) 是怎么玩的_字段

举报

相关推荐

0 条评论