For investors
股价:
5.36 美元 %For investors
股价:
5.36 美元 %认真做教育 专心促就业
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 代码:
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();
}
编译代码:
%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)会更新
更新安全描述符:
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
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;
}
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以及实现也依赖.