CLR运行时细节 - 继承多态的实现

关于多态不多解释了,在运行时决定和调用具体的实现,是面向对象的基础 设计模式的基础.
准备把继承多态和接口多态分开,因为从CLR实现的角度继承多态相比于接口多态要简单得多,也更容易理解,本篇只讨论继承多态, .NET Framework 2.0 和 4.0 这两个版本在实现上稍微有点区别(这里先忽略方法Jit编译的过程,只关注实现的方式).

废话不多,先看代码: C# Polymorphism01.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
using System;
using System.Runtime.CompilerServices;
public class Program
{
public static void Main(string[] args)
{
Console.WriteLine("Polymorphism01 demo");
BaseClass bc = new BaseClass();
BaseClass bc1 = new ChlidClass();
BaseClass bc2 = new BrotherClass();
BaseClass bc3 = new DerivedOfBrotherClass();
BrotherClass bc4 = new DerivedOfBrotherClass();
bc.VirtualFun1();
bc1.VirtualFun1();
bc2.VirtualFun1();
bc3.VirtualFun1();
bc3.VirtualFun2();
bc4.VirtualFun3();
Console.ReadLine();
}
}
public class BaseClass
{
[MethodImpl(MethodImplOptions.NoInlining)]
public virtual void VirtualFun1()
{
Console.WriteLine("BaseClass VirtualFun1");
}
[MethodImpl(MethodImplOptions.NoInlining)]
public virtual void VirtualFun2()
{
Console.WriteLine("BaseClass VirtualFun2");
}
}
public class ChlidClass : BaseClass
{
[MethodImpl(MethodImplOptions.NoInlining)]
public override void VirtualFun1()
{
Console.WriteLine("ChlidClass VirtualFun1");
}
[MethodImpl(MethodImplOptions.NoInlining)]
public override void VirtualFun2()
{
Console.WriteLine("ChlidClass VirtualFun2");
}
}
public class BrotherClass : BaseClass
{
[MethodImpl(MethodImplOptions.NoInlining)]
public override void VirtualFun1()
{
Console.WriteLine("BrotherClass VirtualFun1");
}
[MethodImpl(MethodImplOptions.NoInlining)]
public override void VirtualFun2()
{
Console.WriteLine("BrotherClass VirtualFun2");
}
[MethodImpl(MethodImplOptions.NoInlining)]
public virtual void VirtualFun3()
{
Console.WriteLine("BrotherClass VirtualFun3");
}
}
public class DerivedOfBrotherClass : BrotherClass
{
[MethodImpl(MethodImplOptions.NoInlining)]
public override void VirtualFun1()
{
Console.WriteLine("DerivedOfBrotherClass VirtualFun1");
}
[MethodImpl(MethodImplOptions.NoInlining)]
public override void VirtualFun2()
{
Console.WriteLine("DerivedOfBrotherClass VirtualFun2");
}
[MethodImpl(MethodImplOptions.NoInlining)]
public override void VirtualFun3()
{
Console.WriteLine("DerivedOfBrotherClass VirtualFun3");
}
}
  • 编译代码 先用 .Net Framework 2.0 编译:

    1
    2
    %windir%\Microsoft.NET\Framework\v2.0.50727\csc.exe /debug /target:exe /out:e:\temp\Polymorphism01_2.0.exe e:\temp\Polymorphism01.cs
    pause
  • 运行 Polymorphism01_2.0.exe

  • 启动windbg 附加进程 加载SOS

  • 查找对应的模块:
    !Name2EE *!Polymorphism01_2.0.exe

1
2
3
4
0:004> !Name2EE *!Polymorphism01_2.0.exe
Module: 790c1000 (mscorlib.dll)
--------------------------------------
Module: 00af2c5c (Polymorphism01_2.0.exe)
  • 根据模块查找方法表:
    !DumpModule -mt 00af2c5c

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    0:004> !DumpModule -mt 00af2c5c
    Name: E:\temp\Polymorphism01_2.0.exe
    Attributes: PEFile
    Assembly: 00167720
    LoaderHeap: 00000000
    TypeDefToMethodTableMap: 00af00c0
    TypeRefToMethodTableMap: 00af00dc
    MethodDefToDescMap: 00af0100
    FieldDefToDescMap: 00af0144
    MemberRefToDescMap: 00af0148
    FileReferencesMap: 00af016c
    AssemblyReferencesMap: 00af0170
    MetaData start address: 00402170 (1756 bytes)
    Types defined in this module
    MT TypeDef Name
    ------------------------------------------------------------------------------
    00af302c 0x02000002 Program
    00af3098 0x02000003 BaseClass
    00af310c 0x02000004 ChlidClass
    00af3188 0x02000005 BrotherClass
    00af3208 0x02000006 DerivedOfBrotherClass
    Types referenced in this module
    MT TypeRef Name
    ------------------------------------------------------------------------------
    793308f8 0x01000001 System.Object
    79334648 0x01000006 System.Console
  • 先分别看下 BaseClass BrotherClass DerivedOfBrotherClass 这3个继承关系类的方法表(MethodTable)

  • 可以看到第一个虚方法(ToString)的入口都是在方法表偏移28h的位置,其顺序是先父类,再子类,这样的安排让所有同一个家族(继承关系)的类型继承虚方法的顺序是一样的,并且偏移量是一样的,所有的类型(除了接口类型)的父类都是(或者间接是)System.Object,所以前4个虚方法肯定是Object里的4个虚方法(ToString Equals GetHashCode Finalize)
  • 通过Program 的方法表(MethodTable)找到Main方法的入口地址:
    !DumpMT -md 00af302c

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    0:004> !DumpMT -md 00af302c
    EEClass: 00af12f4
    Module: 00af2c5c
    Name: Program
    mdToken: 02000002 (E:\temp\Polymorphism01_2.0.exe)
    BaseSize: 0xc
    ComponentSize: 0x0
    Number of IFaces in IFaceMap: 0
    Slots in VTable: 6
    --------------------------------------
    MethodDesc Table
    Entry MethodDesc JIT Name
    79286aa0 79104960 PreJIT System.Object.ToString()
    79286ac0 79104968 PreJIT System.Object.Equals(System.Object)
    79286b30 79104998 PreJIT System.Object.GetHashCode()
    792f76d0 791049bc PreJIT System.Object.Finalize()
    00afc015 00af3024 NONE Program..ctor()
    01010070 00af3018 JIT Program.Main(System.String[])
  • Main方法已经Jit编译,看看被编译成啥样子:
    !u 01010070

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    0:004> !u 01010070
    Normal JIT generated code
    Program.Main(System.String[])
    Begin 01010070, size 10a
    >>> 01010070 55 push ebp
    01010071 8bec mov ebp,esp
    01010073 83ec2c sub esp,2Ch
    01010076 894dfc mov dword ptr [ebp-4],ecx
    01010079 833d142eaf0000 cmp dword ptr ds:[0AF2E14h],0
    01010080 7405 je 01010087
    01010082 e832ff0d79 call mscorwks!JIT_DbgIsJustMyCode (7a0effb9)
    01010087 33d2 xor edx,edx
    01010089 8955ec mov dword ptr [ebp-14h],edx
    0101008c 33d2 xor edx,edx
    0101008e 8955f0 mov dword ptr [ebp-10h],edx
    01010091 33d2 xor edx,edx
    01010093 8955f4 mov dword ptr [ebp-0Ch],edx
    01010096 33d2 xor edx,edx
    01010098 8955f8 mov dword ptr [ebp-8],edx
    0101009b 33d2 xor edx,edx
    0101009d 8955e8 mov dword ptr [ebp-18h],edx
    010100a0 90 nop
    010100a1 8b0d30204202 mov ecx,dword ptr ds:[2422030h] ("Polymorphism01 demo")
    *** WARNING: Unable to verify checksum for C:\WINDOWS\assembly\NativeImages_v2.0.50727_32\mscorlib\b14359470744c840c59fbe4e58034fd6\mscorlib.ni.dll
    010100a7 e8b0457878 call mscorlib_ni+0x6d465c (7979465c) (System.Console.WriteLine(System.String), mdToken: 060007c8)
    010100ac 90 nop
    010100ad b99830af00 mov ecx,0AF3098h (MT: BaseClass)
    010100b2 e8651fadff call 00ae201c (JitHelp: CORINFO_HELP_NEWSFAST)
    010100b7 8945e4 mov dword ptr [ebp-1Ch],eax
    010100ba 8b4de4 mov ecx,dword ptr [ebp-1Ch]
    010100bd ff15d830af00 call dword ptr ds:[0AF30D8h] (BaseClass..ctor(), mdToken: 06000005)
    010100c3 8b45e4 mov eax,dword ptr [ebp-1Ch]
    010100c6 8945f8 mov dword ptr [ebp-8],eax
    010100c9 b90c31af00 mov ecx,0AF310Ch (MT: ChlidClass)
    010100ce e8491fadff call 00ae201c (JitHelp: CORINFO_HELP_NEWSFAST)
    010100d3 8945e0 mov dword ptr [ebp-20h],eax
    010100d6 8b4de0 mov ecx,dword ptr [ebp-20h]
    010100d9 ff154c31af00 call dword ptr ds:[0AF314Ch] (ChlidClass..ctor(), mdToken: 06000008)
    010100df 8b45e0 mov eax,dword ptr [ebp-20h]
    010100e2 8945f4 mov dword ptr [ebp-0Ch],eax
    010100e5 b98831af00 mov ecx,0AF3188h (MT: BrotherClass)
    010100ea e82d1fadff call 00ae201c (JitHelp: CORINFO_HELP_NEWSFAST)
    010100ef 8945dc mov dword ptr [ebp-24h],eax
    010100f2 8b4ddc mov ecx,dword ptr [ebp-24h]
    010100f5 ff15cc31af00 call dword ptr ds:[0AF31CCh] (BrotherClass..ctor(), mdToken: 0600000c)
    010100fb 8b45dc mov eax,dword ptr [ebp-24h]
    010100fe 8945f0 mov dword ptr [ebp-10h],eax
    01010101 b90832af00 mov ecx,0AF3208h (MT: DerivedOfBrotherClass)
    01010106 e8111fadff call 00ae201c (JitHelp: CORINFO_HELP_NEWSFAST)
    0101010b 8945d8 mov dword ptr [ebp-28h],eax
    0101010e 8b4dd8 mov ecx,dword ptr [ebp-28h]
    01010111 ff154c32af00 call dword ptr ds:[0AF324Ch] (DerivedOfBrotherClass..ctor(), mdToken: 06000010)
    01010117 8b45d8 mov eax,dword ptr [ebp-28h]
    0101011a 8945ec mov dword ptr [ebp-14h],eax
    0101011d b90832af00 mov ecx,0AF3208h (MT: DerivedOfBrotherClass)
    01010122 e8f51eadff call 00ae201c (JitHelp: CORINFO_HELP_NEWSFAST)
    01010127 8945d4 mov dword ptr [ebp-2Ch],eax
    0101012a 8b4dd4 mov ecx,dword ptr [ebp-2Ch]
    0101012d ff154c32af00 call dword ptr ds:[0AF324Ch] (DerivedOfBrotherClass..ctor(), mdToken: 06000010)
    01010133 8b45d4 mov eax,dword ptr [ebp-2Ch]
    01010136 8945e8 mov dword ptr [ebp-18h],eax
    01010139 8b4df8 mov ecx,dword ptr [ebp-8]
    0101013c 8b01 mov eax,dword ptr [ecx]
    0101013e ff5038 call dword ptr [eax+38h]
    01010141 90 nop
    01010142 8b4df4 mov ecx,dword ptr [ebp-0Ch]
    01010145 8b01 mov eax,dword ptr [ecx]
    01010147 ff5038 call dword ptr [eax+38h]
    0101014a 90 nop
    0101014b 8b4df0 mov ecx,dword ptr [ebp-10h]
    0101014e 8b01 mov eax,dword ptr [ecx]
    01010150 ff5038 call dword ptr [eax+38h]
    01010153 90 nop
    01010154 8b4dec mov ecx,dword ptr [ebp-14h]
    01010157 8b01 mov eax,dword ptr [ecx]
    01010159 ff5038 call dword ptr [eax+38h]
    0101015c 90 nop
    0101015d 8b4dec mov ecx,dword ptr [ebp-14h]
    01010160 8b01 mov eax,dword ptr [ecx]
    01010162 ff503c call dword ptr [eax+3Ch]
    01010165 90 nop
    01010166 8b4de8 mov ecx,dword ptr [ebp-18h]
    01010169 8b01 mov eax,dword ptr [ecx]
    0101016b ff5040 call dword ptr [eax+40h]
    0101016e 90 nop
    0101016f e8fc477878 call mscorlib_ni+0x6d4970 (79794970) (System.Console.ReadLine(), mdToken: 060007ba)
    01010174 90 nop
    01010175 90 nop
    01010176 8be5 mov esp,ebp
    01010178 5d pop ebp
    01010179 c3 ret
  • 这里最重要的几行:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    01010139 8b4df8 mov ecx,dword ptr [ebp-8] // 这里是BaseClass实例对象的地址 放到 ecx寄存器,Jit采用类似fastcall的调用协定,前2个不大于4字节的参数用 ecx edx来传递,而实例方法的调用第一个参数是隐含的this指针(托管对象在托管堆上的地址),如果是静态方法就不需要传this pointer了
    0101013c 8b01 mov eax,dword ptr [ecx] // 托管堆上的对象(值类型装箱后也是一样)第一个4字节(64位8字节)是对象的方法表地址(MethodTable),这里是把方法表(MethodTable)地址赋给eax寄存器
    0101013e ff5038 call dword ptr [eax+38h] // 这里就是实际的方法调用 上面说了 第一个虚方法在方法表的偏移28h位置,前4个是Object里的4个虚方法,所以 VirtualFun1 的入口在方法表地址(MT) + 28h + 4×4字节 也就是偏移38h的位置
    01010141 90 nop
    01010142 8b4df4 mov ecx,dword ptr [ebp-0Ch] // 这里是 ChlidClass的对象地址赋给ecx
    01010145 8b01 mov eax,dword ptr [ecx] // 同样ChlidClass的方法表地址赋给eax
    01010147 ff5038 call dword ptr [eax+38h] // 调用ChlidClass方法表偏移38h的方法,也是VirtualFun1 方法
    0101014a 90 nop
    0101014b 8b4df0 mov ecx,dword ptr [ebp-10h] // BrotherClass的对象地址赋给ecx
    0101014e 8b01 mov eax,dword ptr [ecx] // BrotherClass方法表地址赋给eax
    01010150 ff5038 call dword ptr [eax+38h] // 调用BrotherClass方法表偏移38h的方法,也是VirtualFun1 方法
    01010153 90 nop
    01010154 8b4dec mov ecx,dword ptr [ebp-14h] // DerivedOfBrotherClass的对象地址赋给ecx
    01010157 8b01 mov eax,dword ptr [ecx] // DerivedOfBrotherClass方法表地址赋给eax
    01010159 ff5038 call dword ptr [eax+38h] // 调用DerivedOfBrotherClass方法表偏移38h的方法,也是VirtualFun1 方法
    0101015c 90 nop
    0101015d 8b4dec mov ecx,dword ptr [ebp-14h] // 还是DerivedOfBrotherClass对象地址
    01010160 8b01 mov eax,dword ptr [ecx] // DerivedOfBrotherClass的方法表赋给eax
    01010162 ff503c call dword ptr [eax+3Ch] // 这次偏移不一样了,第6个方法 VirtualFun2 (28h+5×4字节)
    01010165 90 nop
    01010166 8b4de8 mov ecx,dword ptr [ebp-18h] // 还是DerivedOfBrotherClass对象地址
    01010169 8b01 mov eax,dword ptr [ecx] // DerivedOfBrotherClass的方法表赋给eax
    0101016b ff5040 call dword ptr [eax+40h] // 这次偏移又不一样了,第7个方法 VirtualFun3 (28h+6×4字节)
  • 可以看到 继承多态在CLR运行时的实现是通过方法表的偏移 间接调用的,而方法表内继承虚方法的构建顺序是先父类再子类,由于.NET是单一继承,这样就确保了在同一家族的同一虚方法的偏移量是一样的.

  • 接下来用Framework 4.0 编译下源码,4.0 和2.0相比 在实现上多了一层间接寻址,但思路是一样的

1
2
%windir%\Microsoft.NET\Framework\v4.0.30319\csc.exe /debug /target:exe /out:e:\temp\Polymorphism01_4.0.exe e:\temp\Polymorphism01.cs
pause
  • 运行 Polymorphism01_4.0.exe

  • 启动windbg 附加进程 加载SOS (这里要加载对于4.0的sos.dll)

  • 直接查找Main方法:
    !Name2EE Polymorphism01_4.0.exe Program.Main
1
2
3
4
5
6
7
0:004> !Name2EE Polymorphism01_4.0.exe Program.Main
Module: 00b32ea4
Assembly: Polymorphism01_4.0.exe
Token: 06000001
MethodDesc: 00b33838
Name: Program.Main(System.String[])
JITTED Code Address: 033a0070
  • 看Main方法的区别:!u 033a0070
    这里只截取最重要的一段,调用构造器和其他的部分都先忽略

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    ...
    e:\temp\Polymorphism01.cs @ 16:
    033a0139 8b4df8 mov ecx,dword ptr [ebp-8] // 这个还是一样BaseClass对象的地址赋给ecx
    033a013c 8b01 mov eax,dword ptr [ecx] // 还是对象的第一个4字节是方法表地址 赋给eax
    033a013e 8b4028 mov eax,dword ptr [eax+28h] // 这里是和2.0的区别 所有继承的虚方法的起始地址保存在方法表偏移28h的位置,也就是偏移量不是从方法表地址开始算了
    033a0141 ff5010 call dword ptr [eax+10h] // 这里的方式一样的 eax是虚方法的起始位置了,前4个是Object的4个虚方法,偏移10h是第5个方法 VirtualFun1
    033a0144 90 nop
    e:\temp\Polymorphism01.cs @ 17:
    033a0145 8b4df4 mov ecx,dword ptr [ebp-0Ch] // ChlidClass对象地址赋给ecx
    033a0148 8b01 mov eax,dword ptr [ecx] // ChlidClass方法表地址赋给eax
    033a014a 8b4028 mov eax,dword ptr [eax+28h] // 虚表入口地址赋给eax
    033a014d ff5010 call dword ptr [eax+10h] //还是偏移到第5个方法 VirtualFun1
    033a0150 90 nop
    e:\temp\Polymorphism01.cs @ 18:
    033a0151 8b4df0 mov ecx,dword ptr [ebp-10h] // BrotherClass对象地址赋给ecx
    033a0154 8b01 mov eax,dword ptr [ecx] // BrotherClass方法表地址赋给eax
    033a0156 8b4028 mov eax,dword ptr [eax+28h] // 虚表入口地址赋给eax
    033a0159 ff5010 call dword ptr [eax+10h] //还是偏移到第5个方法 VirtualFun1
    033a015c 90 nop
    e:\temp\Polymorphism01.cs @ 19:
    033a015d 8b4dec mov ecx,dword ptr [ebp-14h] // DerivedOfBrotherClass对象地址赋给ecx
    033a0160 8b01 mov eax,dword ptr [ecx] // DerivedOfBrotherClass方法表地址赋给eax
    033a0162 8b4028 mov eax,dword ptr [eax+28h] // 虚表入口地址赋给eax
    033a0165 ff5010 call dword ptr [eax+10h] //还是偏移到第5个方法 VirtualFun1
    033a0168 90 nop
    e:\temp\Polymorphism01.cs @ 20:
    033a0169 8b4dec mov ecx,dword ptr [ebp-14h] // 上面同一个对象
    033a016c 8b01 mov eax,dword ptr [ecx]
    033a016e 8b4028 mov eax,dword ptr [eax+28h]
    033a0171 ff5014 call dword ptr [eax+14h] // 这里比上面的调用多偏移了4个字节 也就是第6个方法 VirtualFun2
    033a0174 90 nop
    e:\temp\Polymorphism01.cs @ 21:
    033a0175 8b4de8 mov ecx,dword ptr [ebp-18h] // 和上面不是同一个对象地址,但是是实例化同样类型的对象
    033a0178 8b01 mov eax,dword ptr [ecx]
    033a017a 8b4028 mov eax,dword ptr [eax+28h]
    033a017d ff5018 call dword ptr [eax+18h] // 这里比上面的调用再多偏移了4个字节 也就是第7个方法 VirtualFun3
    033a0180 90 nop
    ...
  • .NET 4.0 比2.0 多了一次间接寻址,就是先偏移到虚表的入口,再从这个入口开始偏移到相应的方法,这样的好处(个人觉得)虚表的存储位置可以更灵活 如果方法表(MT)包含多个可变长结构也没问题 只要入口地址保存在偏移28h的位置即可

参考文档:

https://www.microsoft.com/china/MSDN/library/netFramework/netframework/JITCompiler.mspx?mfr=true
http://www.codeproject.com/Articles/20481/NET-Type-Internals-From-a-Microsoft-CLR-Perspecti
http://blogs.microsoft.co.il/sasha/2012/03/15/virtual-method-dispatch-and-object-layout-changes-in-clr-40/
http://www.cnblogs.com/BlueTzar/articles/884694.html