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# 例子
    为了方便说明,我就定义一个 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 L10057f300027ea88c0:000> !do 027ea88cName:ConsoleApp1.ChineseMethodTable: 05ce5d3cEEClass:05cd3380Size:12(0xc) bytesFile:D:\net6\ConsoleApplication2\ConsoleApp1\bin\x86\Debug\net6.0\ConsoleApp1.dllFields:None


    (2)eax,dword ptr [eax] 如果大家了解 实例 在堆上的内存布局的话,应该知道,这个首地址存放的就是 methodtable 指针,我们可以用 !dumpmt 05ce5d3c 来验证下。
    0:000> dp 027ea88c L1027ea88c05ce5d3c0:000> !dumpmt 05ce5d3cEEClass:05cd3380Module:05addb14Name:ConsoleApp1.ChinesemdToken:02000007File:D:\net6\ConsoleApplication2\ConsoleApp1\bin\x86\Debug\net6.0\ConsoleApp1.dllBaseSize:0xcComponentSize:0x0DynamicStatics:falseContainsPointers falseSlots in VTable: 6Number of IFaces in IFaceMap: 0


    (3)eax,dword ptr [eax+28h] 那这句话是什么意思呢?如果你了解 CoreCLR 的话,你应该知道 methedtable 是由一个 class MethodTable 类来承载的,所以它取了 methodtable 偏移 0x28位置的一个字段,那这个偏移字段是什么呢?我们先用 dt 把 methodtable 结构给导出来。
    0:000> dt 05ce5d3c MethodTablecoreclr!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+0x014 m_pLoaderModule: PlainPointer+0x018 m_pWriteableData : PlainPointer+0x01c m_pEEClass: PlainPointer+0x01c m_pCanonMT: PlainPointer+0x020 m_pPerInstInfo: PlainPointer *>+0x020 m_ElementTypeHnd : 0+0x020 m_pMultipurposeSlot1 : 0+0x024 m_pInterfaceMap: PlainPointer+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 05ce5d3cEEClass:05cd3380Module:05addb14Name:ConsoleApp1.ChinesemdToken:02000007File:D:\net6\ConsoleApplication2\ConsoleApp1\bin\x86\Debug\net6.0\ConsoleApp1.dllBaseSize:0xcComponentSize:0x0DynamicStatics:falseContainsPointers falseSlots in VTable: 6Number of IFaces in IFaceMap: 0--------------------------------------MethodDesc TableEntry MethodDeJIT Name02610028 02605568NONE System.Object.Finalize()02610030 02605574NONE System.Object.ToString()02610038 02605580NONE System.Object.Equals(System.Object)02610050 026055acNONE System.Object.GetHashCode()05CF1CE0 05ce5d24NONE ConsoleApp1.Chinese.SayHello()05CF1CE8 05ce5d30JIT ConsoleApp1.Chinese..ctor()0:000> dp 05ce5d3c L1005ce5d3c00000200 0000000c 00074088 0000000505ce5d4c05ce5ccc 05addb14 05ce5d7c 05cd338005ce5d5c05cf1ce8 00000000 05ce5d68 0261002805ce5d6c02610030 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 05cf1ce0Unmanaged code05cf1ce0 e88f9dde74callcoreclr!PrecodeFixupThunk (7aadba74)05cf1ce5 5epopesi05cf1ce6 0001addbyte ptr [ecx],al05cf1ce8 e913050000jmp05cf220005cf1ced 5fpopedi05cf1cee 0300addeax,dword ptr [eax]05cf1cf0 245dandal,5Dh05cf1cf2 ceinto05cf1cf3 0500000000addeax,005cf1cf8 0000addbyte ptr [eax],al

    从汇编看,它还是一段 桩代码,言外之意就是该方法没有被 JIT 编译,如果编译完了,这里的 05CF1CE0 05ce5d24 NONE ConsoleApp1.Chinese.SayHello()的 Entry (05CF1CE0) 也会被同步修改,验证一下很简单,我们继续 go 代码让其编译完成,然后再 dumpmt 。
    0:008> !dumpmt -md 05ce5d3cEEClass:05cd3380Module:05addb14Name:ConsoleApp1.ChinesemdToken:02000007File:D:\net6\ConsoleApplication2\ConsoleApp1\bin\x86\Debug\net6.0\ConsoleApp1.dllBaseSize:0xcComponentSize:0x0DynamicStatics:falseContainsPointers falseSlots in VTable: 6Number of IFaces in IFaceMap: 0--------------------------------------MethodDesc TableEntry MethodDeJIT Name02610028 02605568NONE System.Object.Finalize()02610030 02605574NONE System.Object.ToString()02610038 02605580NONE System.Object.Equals(System.Object)02610050 026055acNONE System.Object.GetHashCode()05CF2270 05ce5d24JIT ConsoleApp1.Chinese.SayHello()05CF1CE8 05ce5d30JIT ConsoleApp1.Chinese..ctor()0:008> dp 05ce5d3c L1005ce5d3c00000200 0000000c 00074088 0000000505ce5d4c05ce5ccc 05addb14 05ce5d7c 05cd338005ce5d5c05cf1ce8 00000000 05ce5d68 0261002805ce5d6c02610030 02610038 02610050 05cf2270

    此时可以看到它由 05cf1ce0 变成了 05cf2270, 这个就是 JIT 编译后的方法代码,我们用 !U 反编译下。
    0:008> !U 05cf2270Normal JIT generated codeConsoleApp1.Chinese.SayHello()ilAddr is 05E720D5 pImport is 008F6E88Begin 05CF2270, size 27D:\net6\ConsoleApplication2\ConsoleApp1\Program.cs @ 28:>>> 05cf2270 55pushebp05cf2271 8becmovebp,esp05cf2273 50pusheax05cf2274 894dfcmovdword ptr [ebp-4],ecx05cf2277 833d74dcad0500cmpdword ptr ds:[5ADDC74h],005cf227e 7405je05cf228505cf2280 e8cb2bf174callcoreclr!JIT_DbgIsJustMyCode (7ac04e50)05cf2285 90nopD:\net6\ConsoleApplication2\ConsoleApp1\Program.cs @ 29:05cf2286 8b0d74207e04movecx,dword ptr ds:[47E2074h] ("chinese")05cf228c e8dffbffffcall05cf1e7005cf2291 90nopD:\net6\ConsoleApplication2\ConsoleApp1\Program.cs @ 30:05cf2292 90nop05cf2293 8be5movesp,ebp05cf2295 5dpopebp05cf2296 c3ret

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

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

      推荐阅读