lldb 调试 linux下 .net Core 总结及开源扩展 yinuo

相信很多朋友在跟随微软.net core 从windows平台迁移至linux平台的过程中遇到很多别扭的地方,
这里我只聊聊 运行时 调试的那些事儿。

  • 首先从工具上来讲Windows上的windbg肯定是运行时的首选调试工具(因为有对应版本的SOS.dll),在linux平台运行时调试需要切换到lldb (Only lldb is supported by the SOS plugin. gdb can be used to debug the coreclr code but with no SOS support.)
    调试器的原理和功能基本一样,但细节到某个功能的命令自然会有区别,尤其是熟练了其中1个的命令之后(比如之前在看汇编的时候是Intel格式,现在要适用AT&T格式)…

这里先总结一些个人常用的命令在 windbg下和lldb下的对比:

  • 非托管命令:
非托管命令 lldb windbg
列出当前模块 image list lmf
当前线程 thread list ~
当前线程栈回溯 thread backtrace kp
所有线程栈回溯 thread backtrace all ~* kp
切换线程 thread select 2 ~2s kp
查看寄存器 re r r
查看内存(8字节) memory read –size 8 –format x <address> dq <address>

LLDB同GDB的命令对比:https://lldb.llvm.org/lldb-gdb.html

  • 托管命令:

这里先介绍下自己写的开源lldb调试.net Core扩展模块 Yinuo
在使用lldb调试linux .net Core程序的过程中,有很多不适应的地方,比如遍历并查看所有线程的托管栈回溯 在windbg下可以~*e !clrstack 在lldb里虽然有bt allclrstack 但是却只能手动切换单个线程再回溯,没有办法结合到一起,还有一个原因是lldb内命令输出的内容颜色统一,不太好区分重点关注的点,比如线程回溯比较关注方法名,托管对象转储比较关注内部对象地址等等,lldb的好处是支持python或者c++接口,可以通过接口方式写lldb的扩展来辅助我们调试过程,提高调试效率。
下面介绍下调试扩展 Yinuo 的加载过程:

  • 以下的软件环境 CentOS7(x64),lldb-3.6.0,python-2.7.5

  • 首先git下载模块 git clone https://github.com/espider/yinuo
    目录没有要求,但要记得,因为加载模块的时候要知道在哪。

  • 启动lldb 并附加被调试的进程 (lldb) attach -p PID

  • 加载Yinuo调试模块 (lldb) command script import ~/yinuo/ynlldb.py 之前git下来的目录里的python文件

  • 成功加载ynlldb.py后可以 help 看下当前注册进来的命令,都以 yn_ 为前缀,加载模块的时候会判断当前的.net Core版本号,并自动加载对应版本的调试插件libsosplugin.so

接下来介绍下当前注册进lldb的辅助调试命令

  • yn_heap_dump
    查看当前托管堆信息命令,以色块和色块的比例直观的感受 Gen 0,1,2 LOH 在同一个堆内的比例 以及其实际大小(这里的比例按托管堆的地址空间计算,并没有排除Free的和Gen0没有使用的地址空间,由于比例可能相去甚远所以有可能看不到某个堆的色块)

  • yn_object_dump
    转储托管对象,可以根据类型的方法表、类型名、对象地址 进行批量或者单个转储,同时计算对象(按方法表或类型的话针对每1个单独对象)所属托管堆的位置Gen 0,1,2,LOH,并统计4类堆内的数量。
    支持选项和参数
    • –methodtable/-m 转储此方法表的所有对象,后跟方法表地址;
    • –type/-t 转储此类型名的所有对象,后跟类型名(同方法表选项互斥 2选1);
    • –offset/-o 转储对象的同时是否深入转储其内部偏移对象,后跟该对象的偏移量(目前只支持1级内部偏移)
    • –address/-a 转储单个对象,后跟对象地址
    • –dumpobj/-d 是否转储对象,默认为True,如果不转储则只返回对象地址;
    • –gen/-g 是否显示对象所在的堆Gen0,1,2,LOH,默认True;

例如想要查看 类型Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.Frame`1[[Microsoft.AspNetCore.Hosting.Internal.HostingApplication+Context, Microsoft.AspNetCore.Hosting]] 方法表地址 00007ff8711df620 可以这样写转储命令:

这里会对输出内容做下颜色处理,比如我们比较关心其内部成员的Value这列,如果是地址的话会显示成黄色,方便调试时候的快速定位。

如果想要转储某个对象可以这样写,根据对象地址:

发现对象内的某个成员比较感兴趣,例如 刚刚的对象内偏移 0xe0 位置是 RawTarget

想对其进行偏移转储 可以这样写:

  • yn_thread_clrstack
    显示某个线程或者所有线程的托管栈回溯,不指定选项 –thread/-t 的话默认显示当前线程的托管栈回溯, -t 跟线程index可以回溯指定线程,或者 跟 all,来批量显示所有线程的托管栈回溯,参数 -a 可选(此为SOS命令clrstack可选参数)

这里也对输出的内容做了颜色处理,比如IP指令指针列和CallSite是我们比较关注的,这里分别用黄色和绿色标注。

  • yn_thread_pe
    显示某个线程或者所有线程托管异常,选项同 yn_thread_clrstack 一样

  • yn_transfer
    此命令只用于转移执行其他lldb命令,因为yinuo项目调试的时候会在当前目录生成一个log文件(ynlldb.log)会把所有执行的yinuo命令及输出写入日志便于以后的查询,使用例如:yn_transfer dumpheap -stat 会执行 dumpheap -stat 并把结果输出到终端和日志文件里。
  • Yinuo 项目 License 采用 BSD,大家有兴趣可以自己调整或者联系我共同维护,实现自己的调试命令比较简单,git项目内的 commandlist 目录,所有自动注册的调试命令都在这里以,自命名.py文件即可,内容例子及说明如下:
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
#!/usr/bin/python
# coding:utf-8
import lldb
import commandlist.ynbase as yn
from util.colorstyle import *
from util.exportcontent import *
"""这里把自定义类型注册进来等待加载的时候自动注册"""
def register_lldb_commands():
return [
YNTransfer()
]
"""自定义类型继承自yn.YNCommand即可"""
class YNTransfer(yn.YNCommand):
""" transfer lldb command and exe it for log """
def __init__(self):
pass
"""这个是注册到lldb里的命令名字"""
def name(self):
# register function name in lldb
return 'yn_transfer'
"""如果有选项的话在这里定义,基于python argparse模块实现的 """
def options(self):
return [
]
"""描述信息"""
def description(self):
return 'Transfer lldb command for log,e.g. arguments: dumpheap -stat'
"""这里是实际命令的具体实现了"""
def run(self, options, arguments):
target = lldb.debugger.GetSelectedTarget()
if target:
if arguments:
YNTransfer.handle_command(arguments)
else:
export_content(' no arguments in yn_transfer.')
else:
export_content(' no target in current debugger.')
@staticmethod
def handle_command(args):
(ci, result) = yn.run_log_command(
" " + (" ".join(args) if len(args) > 0 else ''))
success = result.Succeeded()
if success:
output = result.GetOutput()
contents = output.strip()
lines = contents.splitlines(False)
for i in range(len(lines)):
export_content(' %s' % lines[i])
else:
export_content(
' error="%s"' %
use_style_level(
important_level['high2'],
result.GetError()))
export_content(
' %s ' %
use_style_level(
important_level['low2'],
'-------------'))

参考文档:

https://github.com/dotnet/coreclr/blob/master/Documentation/building/debugging-instructions.md
https://lldb.llvm.org/python-reference.html
https://github.com/llvm-mirror/lldb
https://github.com/facebook/chisel

快速搭建本地 .NET Core 运行时调试环境

需要的软件环境:

  • Oracle VM VirtualBox
  • CentOS 7
  • llvm lldb 3.6.0 (3.5.0我试过 dumpobj时候一直报无效参数 Invalid parameter T_T)

先在VirtualBox创建新虚机:
一路 Next ,文件位置可以自定义下(默认是在Users/当前用户/.. 目录下)
创建完选在
设置 – 存储 里 选下 CentOS7的镜像文件
设置 – 网络里选 桥接网卡
然后启动虚机 开始安装CentOS7

安装过程基本都是默认选项,键盘、时区选下、软件选择 选 最小安装(Minimal Install)
开始安装,设置下root账号的密码

  • 下面所有命令都是在root权限下完成的

安装后重启
root登录后先改网卡配置:
/etc/sysconfig/network-scripts/ 目录下会有个 ifcfg-e开头的文件,修改其内容:onboot=no改成onboot=yes
然后用 ifup 命令激活网口

安装net-tools
yum -y install net-tools
ifconfig 查看下IP地址

有了IP后就可以用自己习惯的ssh工具连接啦,比如:PuTTY

默认防火墙 systemctl disable firewalld.service
重启 reboot
验证状态 firewall-cmd --state

安装 dotnet SDK
mkdir /home/tool && cd /home/tool

下SDK
curl -sSL -o dotnet-1.1.tar.gz https://go.microsoft.com/fwlink/?LinkID=835019

创建目录 解压
mkdir -p /opt/dotnet && tar zxf dotnet-1.1.tar.gz -C /opt/dotnet

创建链接
ln -s /opt/dotnet/dotnet /usr/local/bin

验证 dotnet --info

如果验证出现如下错误

1
2
Failed to load /opt/dotnet/shared/Microsoft.NETCore.App/1.1.0/libcoreclr.so, error: libunwind.so.8: cannot open shared object file: No such file or directory
Failed to bind to CoreCLR at '/opt/dotnet/shared/Microsoft.NETCore.App/1.1.0/libcoreclr.so'

则安装:yum install libunwind

如果验证出现如下错误

1
Failed to initialize CoreCLR, HRESULT: 0x80131500

则安装:yum install icu

成功则会有版本信息:

接下来安装llvm lldb

相关依赖安装:

  • yum -y install wget
  • yum install gcc
  • yum install gcc-c++
  • yum install swig python-devel libedit-devel

下载llvm的源代码:

  • wget http://releases.llvm.org/3.6.0/llvm-3.6.0.src.tar.xz
  • tar -xf llvm-3.6.0.src.tar.xz
  • mv llvm-3.6.0.src llvm

下载clang的源代码:

  • cd llvm/tools
  • wget http://releases.llvm.org/3.6.0/cfe-3.6.0.src.tar.xz
  • tar -xf cfe-3.6.0.src.tar.xz
  • mv cfe-3.6.0.src clang

下载lldb的源代码:

  • wget http://releases.llvm.org/3.6.0/lldb-3.6.0.src.tar.xz
  • tar -xf lldb-3.6.0.src.tar.xz
  • mv lldb-3.6.0.src lldb

下载compiler-rt的源代码:

  • cd ../projects
  • wget http://releases.llvm.org/3.6.0/compiler-rt-3.6.0.src.tar.xz
  • tar -xf compiler-rt-3.6.0.src.tar.xz
  • mv compiler-rt-3.6.0.src compiler-rt

下载libcxxabi的源代码:

  • wget http://releases.llvm.org/3.6.0/libcxxabi-3.6.0.src.tar.xz
  • tar -xf libcxxabi-3.6.0.src.tar.xz
  • mv libcxxabi-3.6.0.src libcxxabi

下载libcxx的源代码:

  • wget http://releases.llvm.org/3.6.0/libcxx-3.6.0.src.tar.xz
  • tar -xf libcxx-3.6.0.src.tar.xz
  • mv libcxx-3.6.0.src libcxx

配置编译选项:

  • cd ..
  • ./configure --enable-optimized CC=gcc CXX=g++

编译llvm:

  • make

漫长的等待… … …

如果编译过程这样的错误c++: internal compiler error: Killed (program cc1plus 则增加swap分区文件大小后再试:
dd if=/dev/zero of=/swapfile bs=1k count=2048000
mkswap /swapfile
swapon /swapfile

编译成功后 只安装lldb,进入llvm/tools/lldb中运行 make install

创建一个.net core web站点
mkdir -p /home/www/core01 && cd /home/www/core01

dotnet new -t web
dotnet restore
ASPNETCORE_URLS="http://*:5000" dotnet run

启动lldb 附加进程 加载SOS调试扩展

执行SOS命令:soshelp

看到熟悉的托管调试命令 甚是亲切 赶紧试试

此文只是简单快速的搭建 .NET Core 在CentOS7下运行时的调试环境,后续再总结 Windbg 和 LLDB 之间的命令习惯差异。

参考文档:

https://github.com/dotnet/coreclr/blob/master/Documentation/building/debugging-instructions.md
http://www.cnblogs.com/dudu/p/build-coreclr-on-centos.html
http://www.cnblogs.com/dudu/p/4294374.html

CLR运行时细节 - 接口多态的实现

关于接口多态(Framework 2.0之后的版本),CLR运行时的实现比继承多态要复杂得多,而网上大部分的文章和资料描述的还是Framework 1.1的实现方式,2.0之后变化非常大,这也是写此文的目的,说明下2.0版本开始的实现细节。
先看个有趣的Demo:

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
using System;
using System.Diagnostics;
public class Program
{
public static void Main(string[] args)
{
Console.WriteLine("Polymorphism02_01 demo");
int loopCount = 100000000;
IFoo ifo = null;
Stopwatch sw = Stopwatch.StartNew();
#region Case 1
for (int i = 0; i < loopCount; ++i)
{
if (i % 2 == 0)
{
ifo = new TestA();
}
else
{
ifo = new TestB();
}
ifo.Foo();
}
Console.WriteLine("Case 1 take time: {0} ms", sw.ElapsedMilliseconds);
#endregion
#region Case 2
for (int i = 0; i < loopCount; ++i)
{
if (i % 2 == 0)
{
ifo = new TestA();
ifo.Foo();
}
else
{
ifo = new TestB();
ifo.Foo();
}
}
Console.WriteLine("Case 2 take time: {0} ms", sw.ElapsedMilliseconds);
#endregion
sw.Reset();
Console.ReadLine();
}
}
public interface IFoo
{
void Foo();
}
public class TestA : IFoo
{
public void Foo()
{
try
{
int i = 1 + 1;
}
catch (Exception ex)
{ }
}
}
public class TestB : IFoo
{
public void Foo()
{
try
{
int i = 1 + 1;
}
catch (Exception ex)
{ }
}
}

源码内有2种方案:

  • Case 1:根据条件实例化后 一次调用 实现的 Foo() 方法
  • Case 2:根据条件每次实例化后都立即调用实现的 Foo() 方法

2种方案的功能上没有区别,只是写法上,第一种是我们日常多态的写法,第二种没有体现多态的优势.

请用Framework 2.0(或者2.0以后的版本)分别编译 Case1功能和Case2功能,看2个方案的执行时间:

截图是我本地电脑的截图,不具有普遍性,但具有代表性:也就是 Case 2 的速度要比 Case 1 的速度快,这个性能的差异就与CLR运行时的实现有关系了.

  • 我们先来简单看下 Framework 1.1 在接口多态的运行时实现方式,下面截取Main方法中关于接口调用部分:
    1
    2
    3
    4
    5
    6
    7
    8
    ...
    012200b3 8bf7 mov esi,edi
    012200b5 8bce mov ecx,esi
    012200b7 8b01 mov eax,dword ptr [ecx]
    012200b9 8b400c mov eax,dword ptr [eax+0Ch]
    012200bc 8b80d0000000 mov eax,dword ptr [eax+0D0h]
    012200c2 ff10 call dword ptr [eax]
    ...

可以看到Framework1.1对于接口多态的运行时实现和4.0的继承多态实现类似,先把对象地址赋给ecx寄存器,在把对象第1个4字节(64位8字节)的方法表(MethodTable)赋给eax寄存器,再通过方法表地址偏移0Ch的位置 取出接口偏移表地址 IOT (Interface Offset Table) 赋给eax寄存器,再通过IOT偏移0D0h的位置 取出对应实现接口的方法入口地址 赋给eax寄存器,最后调用.

也就是每个类型的方法表内包含接口偏移表的入口地址(偏移0Ch的位置),再通过接口偏移表内相同的方法实现有着相同的偏移量来完成多态,但接口是可以多实现的,和继承不同,继承是单一继承(先父类再子类的排列顺序可以保证同一个虚方法的实现有着相同的偏移量),而接口的这种做法明显是以空间换时间的方案, Framework2.0 开始对于接口的调用实现有了非常大的调整.

  • 通过下面的Demo看下2.0之后的实现
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
using System;
public class Program
{
public static void Main(string[] args)
{
Console.WriteLine("Polymorphism02_02 demo");
Console.ReadLine();
IFoo2 ifo2 = new TestA();
ifo2.Foo2();
IFoo1 ifo = null;
for (int i = 0; i <= 102; i++)
{
if (i <= 1)
{ ifo = new TestA(); }
else
{ ifo = new TestB(); }
ifo.Foo11();
}
Console.ReadLine();
}
}
public interface IFoo1
{
void Foo1();
void Foo11();
}
public interface IFoo2
{
void Foo2();
}
public class TestA : IFoo1,IFoo2
{
public void Foo1()
{
Console.WriteLine("TestA IFoo1");
}
public void Foo11()
{
Console.WriteLine("TestA Foo11");
}
public void Foo2()
{
Console.WriteLine("TestA Foo2");
}
}
public class TestB : IFoo1,IFoo2
{
public void Foo1()
{
Console.WriteLine("TestB IFoo1");
}
public void Foo11()
{
Console.WriteLine("TestB Foo11");
}
public void Foo2()
{
Console.WriteLine("TestB Foo2");
}
}
  • 用2.0方式编译:

    1
    2
    %windir%\Microsoft.NET\Framework\v2.0.50727\csc.exe /debug /target:exe /out:e:\temp\Polymorphism02_02_2.0.exe e:\temp\Polymorphism02_02.cs
    pause
  • 运行程序并通过windbg附加到程序进程,查找Main方法的编译结果:

    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
    0:004> !u 01010070
    Normal JIT generated code
    Program.Main(System.String[])
    Begin 01010070, size f2
    >>> 01010070 55 push ebp
    01010071 8bec mov ebp,esp
    01010073 83ec20 sub esp,20h
    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 c745f400000000 mov dword ptr [ebp-0Ch],0
    01010093 33d2 xor edx,edx
    01010095 8955f8 mov dword ptr [ebp-8],edx
    01010098 33d2 xor edx,edx
    0101009a 8955f0 mov dword ptr [ebp-10h],edx
    0101009d 90 nop
    0101009e 8b0d30204202 mov ecx,dword ptr ds:[2422030h] ("Polymorphism02_02 demo")
    *** WARNING: Unable to verify checksum for C:\WINDOWS\assembly\NativeImages_v2.0.50727_32\mscorlib\b14359470744c840c59fbe4e58034fd6\mscorlib.ni.dll
    010100a4 e8b3457878 call mscorlib_ni+0x6d465c (7979465c) (System.Console.WriteLine(System.String), mdToken: 060007c8)
    010100a9 90 nop
    010100aa e8c1487878 call mscorlib_ni+0x6d4970 (79794970) (System.Console.ReadLine(), mdToken: 060007ba)
    010100af 90 nop
    010100b0 b99431af00 mov ecx,0AF3194h (MT: TestA)
    010100b5 e8621fadff call 00ae201c (JitHelp: CORINFO_HELP_NEWSFAST)
    010100ba 8945e8 mov dword ptr [ebp-18h],eax
    010100bd 8b4de8 mov ecx,dword ptr [ebp-18h]
    010100c0 ff15d831af00 call dword ptr ds:[0AF31D8h] (TestA..ctor(), mdToken: 06000009)
    010100c6 8b45e8 mov eax,dword ptr [ebp-18h]
    010100c9 8945f0 mov dword ptr [ebp-10h],eax
    010100cc 8b4df0 mov ecx,dword ptr [ebp-10h]
    010100cf ff151000b000 call dword ptr ds:[0B00010h]
    010100d5 90 nop
    010100d6 33d2 xor edx,edx
    010100d8 8955ec mov dword ptr [ebp-14h],edx
    010100db 33d2 xor edx,edx
    010100dd 8955f8 mov dword ptr [ebp-8],edx
    010100e0 90 nop
    010100e1 eb61 jmp 01010144
    010100e3 90 nop
    010100e4 837df801 cmp dword ptr [ebp-8],1
    010100e8 0f9fc0 setg al
    010100eb 0fb6c0 movzx eax,al
    010100ee 8945f4 mov dword ptr [ebp-0Ch],eax
    010100f1 837df400 cmp dword ptr [ebp-0Ch],0
    010100f5 7521 jne 01010118
    010100f7 90 nop
    010100f8 b99431af00 mov ecx,0AF3194h (MT: TestA)
    010100fd e81a1fadff call 00ae201c (JitHelp: CORINFO_HELP_NEWSFAST)
    01010102 8945e0 mov dword ptr [ebp-20h],eax
    01010105 8b4de0 mov ecx,dword ptr [ebp-20h]
    01010108 ff15d831af00 call dword ptr ds:[0AF31D8h] (TestA..ctor(), mdToken: 06000009)
    0101010e 8b45e0 mov eax,dword ptr [ebp-20h]
    01010111 8945ec mov dword ptr [ebp-14h],eax
    01010114 90 nop
    01010115 90 nop
    01010116 eb1e jmp 01010136
    01010118 90 nop
    01010119 b92832af00 mov ecx,0AF3228h (MT: TestB)
    0101011e e8f91eadff call 00ae201c (JitHelp: CORINFO_HELP_NEWSFAST)
    01010123 8945e4 mov dword ptr [ebp-1Ch],eax
    01010126 8b4de4 mov ecx,dword ptr [ebp-1Ch]
    01010129 ff156c32af00 call dword ptr ds:[0AF326Ch] (TestB..ctor(), mdToken: 0600000d)
    0101012f 8b45e4 mov eax,dword ptr [ebp-1Ch]
    01010132 8945ec mov dword ptr [ebp-14h],eax
    01010135 90 nop
    01010136 8b4dec mov ecx,dword ptr [ebp-14h]
    01010139 ff159000b000 call dword ptr ds:[0B00090h]
    0101013f 90 nop
    01010140 90 nop
    01010141 ff45f8 inc dword ptr [ebp-8]
    01010144 837df866 cmp dword ptr [ebp-8],66h
    01010148 0f9ec0 setle al
    0101014b 0fb6c0 movzx eax,al
    0101014e 8945f4 mov dword ptr [ebp-0Ch],eax
    01010151 837df400 cmp dword ptr [ebp-0Ch],0
    01010155 758c jne 010100e3
    01010157 e814487878 call mscorlib_ni+0x6d4970 (79794970) (System.Console.ReadLine(), mdToken: 060007ba)
    0101015c 90 nop
    0101015d 90 nop
    0101015e 8be5 mov esp,ebp
    01010160 5d pop ebp
    01010161 c3 ret
  • 这里最重要的几行:
    010100cf ff151000b000 call dword ptr ds:[0B00010h]
    对应源码:ifo2.Foo2();

01010139 ff159000b000 call dword ptr ds:[0B00090h]
对应源码:ifo.Foo11();

也就是说运行时不管调用这个接口方法的实例是什么类型的都通过这个内存地址的内容去到相应的实现
前来看看目前这2块内存的内容:

分别指向 00b0601200b06022 这4个内容地址所属的位置:

也就是说运行时为每个调用点(call site)分配一个 Indirect cell (在应用程序域的IndcellHeap堆内)默认情况下其内容指针指向 Lookup Cell(在应用程序域的LookupHeap堆内),再来看看这2块lookupHeap的内容:

这里2个 DispatchToken 30000h,50001h分别代表2个接口方法的调用,DispatchToken的构成比较有意思,分为2段各16位(目前是32位进程),高16位是TypeID,应用程序域在运行时为每个接口类型动态分配的ID,也就是接口类型在运行时唯一的标识,TypeID的起始值是3步长是2,应用程序域内持有2个HashMap分别用于对于TypeID和方法表的对应(MethodTable),以及方法表对应TypeID

  • 这2个HashMap的Hash算法是一样的:每个桶存储4对 key/value 算法是Hash key 右移2位为种子(seed),种子模桶的长度找到对应的桶,然后循环桶内的4个slot匹配key,如果桶已满,则种子增加incr 继续查找。
  • 图里方法表00af312c是IFoo2的方法表,00af30c0是IFoo1的方法表,可以看出在运行时先编译到IFoo2的方法调用,所以先为IFoo2分配TypeID=3,再编译到IFoo1的方法调用 TypeID=5
  • 在第一个HashMap(TypeID->MT)内,IFoo2的key:3右移2位为0,所以在0号桶内,value:MT 右移1位存如桶内对应的位置。IFoo1的key:5 右移2位为1,所以在1号桶内,value:MT 右移1位存入桶内对应的位置。
  • 在第二个HashMap(MT->TypeID)内,IFoo2的key:00af312c右移2位为2BCC4B 再模11为7,所以IFoo2的MT和TypeID在7号桶,同样IFoo1的key:00af30c0右移2位为2BCC30 再模11为2,所以IFoo1的MT和TypeID在2号桶里。

DispatchToken的低16位是由方法在接口内的方法索引(slot index前面的文章介绍过如果查看方法描述及方法槽索引),也就是在运行时DispatchToken可以唯一代表某个接口的某个方法调用,这里和实现类还没有关系。

在循环内i=2的时候再中断观察接口调用(call site)的内存变化(这里前2次都用TestA来实例化是因为IndcellHeap的内容并不是立刻被改的,官方的说法:appropriate do it)

目前的调用点IndcellHeap的内容指向了DispatchHeap,内容也有了变化:

可以看到DispatchHeap的内容就3行,第一行比较传进来的对象是不是TestA类型的,不是的话跳到 00b0a011,是TestA类型的话跳到 010101e8,这个010101e8就是TestA类型实现接口方法 Foo11 的入口,也就是运行时先假设我们传进来的对象都是TestA,这样经过短短的几条指令就能到达TestA的接口实现,那如果不是TestA呢?TestB也实现了,并且在运行时调用了。看下条件失败的调整地址情况:

第1句是对某个内存地址减1的操作,看下这块内存目前内容是64h(十进制的100),也就是在多态接口调用的时候不是TestA对象的话会进入另外一个流程,并在其内部做1个失败计数器,从100开始递减,递减的过程:

最终减完会发生什么,等程序循环执行完再观察调用点的内存。

接口调用点(call site)IndcellHeap的内容指向了ResolveHeap,ResolveHeap的内容:

现在调用点的IndcellHeap内容就不再变化了,不管再用那个对象在循环内再调用Foo11方法,都是通过ResolveHeap内存的方法进行中转的。

总结下:.NET 2.0 开始对接口的多态实现比1.1的时候要复杂的多,也更灵活,它假设我们在接口调用的时候是单态的 或者说多态的次数不多的情况,用某个实现类的方法表(MT)加以判断,如果是则直接跳到这个实现类的接口实现入口,如果不是 则对失败进行计数,并在失败达到一定数量后(100次)认为确实是多态调用,并将call site的入口指向ResolveHeap,此时不再单独判断某个类型的方法表(MT),这也是为何在文章开头的Demo里为何2种写法的性能是有差别的原因了。

参考文档:

https://github.com/dotnet/coreclr/blob/master/Documentation/botr/virtual-stub-dispatch.md
http://blogs.microsoft.co.il/sasha/2012/03/15/virtual-method-dispatch-and-object-layout-changes-in-clr-40/
https://github.com/dotnet/coreclr/blob/master/src/vm/methodtable.h
https://www.microsoft.com/china/MSDN/library/netFramework/netframework/JITCompiler.mspx?mfr=true

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

CLR运行时细节 - Method Descriptor

方法描述符:MethodDesc

  • 运行时用来描述类型的托管方法,它保存在方法描述桶(MethodDescChunk)内;
  • 方法描述符保存了方法在运行时的一些重要信息:
    • 是否JIT编译;
    • 是否有方法表槽(决定了方法入口是跟在方法描述符(MethodDesc)后还是在方法表(MethodTable)后面);
    • 距离MethodDescChunk的索引(chunkIndex);
    • Token的末位(这个在编译期确定了);
    • 方法的一些标识比如是否静态 非内联等;
    • 方法表槽(slot number);
    • 以及最重要的方法入口(entrypoint);

官方的描述:
MethodDesc (method descriptor) is the internal representation of a managed method. It serves several purposes:

  • Provides a unique method handle, usable throughout the runtime. For normal methods, the MethodDesc is a unique handle for a triplet.
  • Caches frequently used information that is expensive to compute from metadata (e.g. whether the method is static).
  • Captures the runtime state of the method (e.g. whether the code has been generated for the method already).
  • Owns the entry point of the method.

先看下Demo C# MethodDesc.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
using System;
using System.Runtime.CompilerServices;
public class Program
{
public static void Main(string[] args)
{
Console.WriteLine("MethodDesc demo");
Console.ReadLine();
MDChlidClass cc = new MDChlidClass();
cc.VirtualFun1();
cc.VirtualFun2();
cc.IFun1();
cc.IFun2();
cc.InstanceFun1();
cc.InstanceFun2();
MDChlidClass.StaticFun1();
Console.ReadLine();
}
}
public class MDBaseClass
{
[MethodImpl(MethodImplOptions.NoInlining)]
public virtual void VirtualFun1()
{
Console.WriteLine("MDBaseClass VirtualFun1");
}
[MethodImpl(MethodImplOptions.NoInlining)]
public virtual void VirtualFun2()
{
Console.WriteLine("MDBaseClass VirtualFun2");
}
}
public class MDChlidClass : MDBaseClass, IFoo
{
public static int TempInt = 0;
[MethodImpl(MethodImplOptions.NoInlining)]
public override void VirtualFun1()
{
Console.WriteLine("MDChlidClass VirtualFun1");
}
[MethodImpl(MethodImplOptions.NoInlining)]
public override void VirtualFun2()
{
Console.WriteLine("MDChlidClass VirtualFun2");
}
[MethodImpl(MethodImplOptions.NoInlining)]
public void IFun1()
{
Console.WriteLine("MDChlidClass IFun1");
}
[MethodImpl(MethodImplOptions.NoInlining)]
public void IFun2()
{
Console.WriteLine("MDChlidClass IFun2");
}
[MethodImpl(MethodImplOptions.NoInlining)]
public void InstanceFun1()
{
Console.WriteLine("MDChlidClass InstanceFun1");
}
[MethodImpl(MethodImplOptions.NoInlining)]
public void InstanceFun2()
{
Console.WriteLine("MDChlidClass InstanceFun2");
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static void StaticFun1()
{
Console.WriteLine("MDChlidClass StaticFun1");
}
}
public interface IFoo
{
void IFun1();
void IFun2();
}
  • 编译代码:
1
2
%windir%\Microsoft.NET\Framework\v2.0.50727\csc.exe /debug /target:exe /out:e:\temp\MethodDesc_2.0.exe e:\temp\MethodDesc.cs
pause
  • 运行 MethodDesc_2.0.exe

  • 启动windbg 加载SOS

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

  • 根据模块查找方法表:
    !DumpModule -mt 00af2c5c

  • 通过MDChlidClass方法表地址查看其EEClass 查找方法描述桶在EEClass偏移40h的位置(64位的话偏移60h 因为标记位使用不变 所有地址类型由4字节变成8字节):

!DumpMT -md 00af31e4

  • 通过方法桶的地址00af3180观察其下方法描述符:

  • 可以看到方法描述桶的第一个4字节(64位8字节)是方法表(MethodTable)的地址
    可以看到MDChlidClass的方法描述符(MD)
  • VirtualFun1 方法描述符地址:00af3190 其内容:00000008 20000004 第一个00代表方法入口在方法表(MT)后面以及还没jit编译,第二个00代表距方法描述桶(MethodDescChunk)的索引(便于找到桶的起始位置),后面的0008是方法的token末位 在编译成IL时确定,可以通过ildasm查看 MethodDesc_2.0.exe 文件,这个token是在编译期程序集内自增的,也就是在运行时并不是唯一的接下来的2000代表方法非内联,0004代表方法表槽slot number 也就是方法入口(entrypoint)在方法表(MT)后的索引(索引从0开始 一般来说前4个方法都是从Object继承下来的4个虚方法 除了接口类型),方法入口:00afc075

  • VirtualFun2 方法描述符地址:00af3198 其内容:00020009 20000005 依旧是没jit编译,方法入口在方法表后,token:0009,非内联,slot number:0005,方法入口:00afc079
  • IFun1 方法描述符地址:00af31a0 其内容:0004000a 20000006 依旧是没jit编译,方法入口在方法表后,token:000a,非内联,slot number:0006,方法入口:00afc07d
  • IFun2 方法描述符地址:00af31a8 其内容:0006000b 20000007 依旧是没jit编译,方法入口在方法表后,token:000b,非内联,slot number:0007,方法入口:00afc081
  • InstanceFun1 方法描述符地址:00af31b0 其内容:4008000c 2000000a 00afc085 ‘40’这位(bit)代表方法入口(slot)是跟在方法描述符(MD)后面的并非在方法表(MT)后面,依旧是没jit编译,方法入口在方法表后,token:000c,非内联,slot number:000a(这里的slot number依然有值,但值是大于等方法表的slot长度的),方法入口:00afc085
  • InstanceFun2 方法描述符地址:00af31bc 其内容:400b000d 2000000b 00afc089 依旧是没jit编译,方法入口在方法描述符(MD)后,token:000d,非内联,slot number:000b,方法入口:00afc089
  • StaticFun1 方法描述符地址:00af31c8 其内容:400b000e 2020000c 00afc08d 依旧是没jit编译,方法入口在方法描述符(MD)后,token:000e,2020非内联 并且静态,slot number:000c,方法入口:00afc08d
  • .ctor 实例构造方法 方法描述符地址:00af31d4 其内容:0011000f 00000008 依旧是没jit编译,方法入口在方法表后,token:000f,slot number:0008,方法入口:00afc091
  • .cctor 静态构造方法 方法描述符地址:00af31dc 其内容:00130010 00200009 依旧是没jit编译,方法入口在方法表后,token:0010,静态的:0020,slot number:0009,方法入口:00afc095

可以看到所有的虚方法(继承或者实现接口)以及构造器方法(实例或者静态)的方法入口(slot)都是在方法表后面的,而其他实例方法和静态方法的方法入口(slot)是跟在方法描述符(MD)后面的

这里引用下CLR文档的一段:
Each MethodDesc has a slot, which contains the entry point of the method. The slot and entry point must exist for all methods, even the ones that never run like abstract methods. There are multiple places in the runtime that depend on the 1:1 mapping between entry points and MethodDescs, making this relationship an invariant.
The slot is either in MethodTable or in MethodDesc itself. The location of the slot is determined by mdcHasNonVtableSlot bit on MethodDesc.
The slot is stored in MethodTable for methods that require efficient lookup via slot index, e.g. virtual methods or methods on generic types. The MethodDesc contains the slot index to allow fast lookup of the entry point in this case.

接下来让 MethodDesc_2.0.exe 继续执行,并回车跳过第一个ReadLine(),再中断到调试器,观察MDChlidClass的方法表00af31e4(MT)和其方法描述桶00af3180(MDC)

可以看到所有Jit编译过的方法,其方法描述符的 00h或者40h 会 逻辑 31h,都是按位的,其中30h是安全描述符先忽略,01h代表是否Jit编译过,同时所有Jit编译过的方法其方法入口(entrypoint)会更新

更新安全描述符:

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
0:000> uf mscorwks!MethodDesc::SetCriticalTransparentInfo
mscorwks!MethodDesc::SetCriticalTransparentInfo:
79e90595 55 push ebp
79e90596 8bec mov ebp,esp
79e90598 837d0c00 cmp dword ptr [ebp+0Ch],0
79e9059c 0f84a4e01100 je mscorwks!MethodDesc::SetCriticalTransparentInfo+0xe (79fae646)
mscorwks!MethodDesc::SetCriticalTransparentInfo+0x9:
79e905a2 6a30 push 30h
79e905a4 58 pop eax
mscorwks!MethodDesc::SetCriticalTransparentInfo+0x1b:
79e905a5 6a01 push 1
79e905a7 50 push eax
79e905a8 e8caffffff call mscorwks!MethodDesc::InterlockedUpdateFlags2 (79e90577)
79e905ad 5d pop ebp
79e905ae c20800 ret 8
mscorwks!MethodDesc::SetCriticalTransparentInfo+0xe:
79fae646 8b4508 mov eax,dword ptr [ebp+8]
79fae649 f7d8 neg eax
79fae64b 1bc0 sbb eax,eax
79fae64d 83e010 and eax,10h
79fae650 83c010 add eax,10h
79fae653 e94d1feeff jmp mscorwks!MethodDesc::SetCriticalTransparentInfo+0x1b (79e905a5)

先更新方法入口,再更新是否Jit编译标记位:
https://github.com/dotnet/coreclr/blob/master/src/vm/method.cpp#L5099

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
BOOL MethodDesc::SetStableEntryPointInterlocked(PCODE addr)
{
CONTRACTL {
THROWS;
GC_NOTRIGGER;
} CONTRACTL_END;
_ASSERTE(!HasPrecode());
PCODE pExpected = GetTemporaryEntryPoint();
PTR_PCODE pSlot = GetAddrOfSlot();
EnsureWritablePages(pSlot);
BOOL fResult = FastInterlockCompareExchangePointer(pSlot, addr, pExpected) == pExpected;
InterlockedUpdateFlags2(enum_flag2_HasStableEntryPoint, TRUE);
return fResult;
}

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:000> uf mscorwks!MethodDesc::SetStableEntryPointInterlocked
mscorwks!MethodDesc::SetStableEntryPointInterlocked:
79ed9a27 55 push ebp
79ed9a28 8bec mov ebp,esp
79ed9a2a 53 push ebx
79ed9a2b 56 push esi
79ed9a2c 57 push edi
79ed9a2d 8bf9 mov edi,ecx
79ed9a2f e8b96afbff call mscorwks!MethodDesc::GetTemporaryEntryPoint (79e904ed)
79ed9a34 8bcf mov ecx,edi
79ed9a36 8bd8 mov ebx,eax
79ed9a38 e8156bfbff call mscorwks!MethodDesc::GetAddrOfSlot (79e90552)
79ed9a3d 8b5508 mov edx,dword ptr [ebp+8]
79ed9a40 53 push ebx
79ed9a41 8bc8 mov ecx,eax
79ed9a43 ff15d0c33c7a call dword ptr [mscorwks!FastInterlockCompareExchange (7a3cc3d0)]
79ed9a49 8bf0 mov esi,eax
79ed9a4b 2bf3 sub esi,ebx
79ed9a4d f7de neg esi
79ed9a4f 1bf6 sbb esi,esi
79ed9a51 46 inc esi
79ed9a52 ba00000001 mov edx,1000000h
79ed9a57 8bcf mov ecx,edi
79ed9a59 ff1574c23c7a call dword ptr [mscorwks!FastInterlockOr (7a3cc274)]
79ed9a5f 5f pop edi
79ed9a60 8bc6 mov eax,esi
79ed9a62 5e pop esi
79ed9a63 5b pop ebx
79ed9a64 5d pop ebp
79ed9a65 c20400 ret 4
  • 可以通过 DumpMD SOS扩展命令观察方法描述符:

为毛要研究方法描述符这个东西?
方法描述符在CLR运行时作为方法的最基础服务,继承多态在运行时的实现依赖方法描述符,接口多态的运行时DispatchToken以及实现也依赖.

参考文档:

https://github.com/dotnet/coreclr/blob/master/Documentation/botr/method-descriptor.md
https://github.com/dotnet/coreclr/blob/master/src/vm/methodtablebuilder.cpp
https://github.com/dotnet/coreclr/blob/master/src/vm/method.hpp
http://blogs.microsoft.co.il/sasha/2009/09/27/how-are-methods-compiled-just-in-time-and-only-then/