运行这段代码并分析 IL 和生成的 ASM,我们可以看到发生了什么:
internal class Program
{
[MethodImpl(MethodImplOptions.NoInlining)]
private static void Test()
{
var b = GetA();
b.GenericVirtual<string>();
b.GenericVirtual<int>();
b.GenericVirtual<StringBuilder>();
b.GenericVirtual<int>();
b.GenericVirtual<StringBuilder>();
b.GenericVirtual<string>();
b.NormalVirtual();
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static A GetA()
{
return new B();
}
private class A
{
public virtual void GenericVirtual<T>()
{
}
public virtual void NormalVirtual()
{
}
}
private class B : A
{
public override void GenericVirtual<T>()
{
base.GenericVirtual<T>();
Console.WriteLine("Generic virtual: {0}", typeof(T).Name);
}
public override void NormalVirtual()
{
base.NormalVirtual();
Console.WriteLine("Normal virtual");
}
}
public static void Main(string[] args)
{
Test();
Console.ReadLine();
Test();
}
}
我用 WinDbg 对 Program.Test 进行了断点:
.loadby sos clr; !bpmd CSharpNewTest CSharpNewTest.Program.Test
然后,我使用Sosex.dll的出色!muf
命令向我展示交错的源代码、IL 和 ASM:
0:000> !muf
CSharpNewTest.Program.Test(): void
b:A
002e0080 55 push ebp
002e0081 8bec mov ebp,esp
002e0083 56 push esi
var b = GetA();
IL_0000: call CSharpNewTest.Program::GetA()
IL_0005: stloc.0 (b)
>>>>>>>>002e0084 ff15c0371800 call dword ptr ds:[1837C0h]
002e008a 8bf0 mov esi,eax
b.GenericVirtual<string>();
IL_0006: ldloc.0 (b)
IL_0007: callvirt A::GenericVirtuallong
002e008c 6800391800 push 183900h
002e0091 8bce mov ecx,esi
002e0093 ba50381800 mov edx,183850h
002e0098 e877e49b71 call clr!JIT_VirtualFunctionPointer (71c9e514)
002e009d 8bce mov ecx,esi
002e009f ffd0 call eax
b.GenericVirtual<int>();
IL_000c: ldloc.0 (b)
IL_000d: callvirt A::GenericVirtuallong
002e00a1 6830391800 push 183930h
002e00a6 8bce mov ecx,esi
002e00a8 ba50381800 mov edx,183850h
002e00ad e862e49b71 call clr!JIT_VirtualFunctionPointer (71c9e514)
002e00b2 8bce mov ecx,esi
002e00b4 ffd0 call eax
b.GenericVirtual<StringBuilder>();
IL_0012: ldloc.0 (b)
IL_0013: callvirt A::GenericVirtuallong
002e00b6 6870391800 push 183970h
002e00bb 8bce mov ecx,esi
002e00bd ba50381800 mov edx,183850h
002e00c2 e84de49b71 call clr!JIT_VirtualFunctionPointer (71c9e514)
002e00c7 8bce mov ecx,esi
002e00c9 ffd0 call eax
b.GenericVirtual<int>();
IL_0018: ldloc.0 (b)
IL_0019: callvirt A::GenericVirtuallong
002e00cb 6830391800 push 183930h
002e00d0 8bce mov ecx,esi
002e00d2 ba50381800 mov edx,183850h
002e00d7 e838e49b71 call clr!JIT_VirtualFunctionPointer (71c9e514)
002e00dc 8bce mov ecx,esi
002e00de ffd0 call eax
b.GenericVirtual<StringBuilder>();
IL_001e: ldloc.0 (b)
IL_001f: callvirt A::GenericVirtuallong
002e00e0 6870391800 push 183970h
002e00e5 8bce mov ecx,esi
002e00e7 ba50381800 mov edx,183850h
002e00ec e823e49b71 call clr!JIT_VirtualFunctionPointer (71c9e514)
002e00f1 8bce mov ecx,esi
002e00f3 ffd0 call eax
b.GenericVirtual<string>();
IL_0024: ldloc.0 (b)
IL_0025: callvirt A::GenericVirtuallong
002e00f5 6800391800 push 183900h
002e00fa 8bce mov ecx,esi
002e00fc ba50381800 mov edx,183850h
002e0101 e80ee49b71 call clr!JIT_VirtualFunctionPointer (71c9e514)
002e0106 8bce mov ecx,esi
002e0108 ffd0 call eax
b.NormalVirtual();
IL_002a: ldloc.0 (b)
002e010a 8bce mov ecx,esi
002e010c 8b01 mov eax,dword ptr [ecx]
002e010e 8b4028 mov eax,dword ptr [eax+28h]
IL_002b: callvirt A::NormalVirtual()
002e0111 ff5014 call dword ptr [eax+14h]
}
IL_0030: ret
有趣的是普通的虚拟调用,可以将其与通用的虚拟调用进行比较:
b.NormalVirtual();
IL_002a: ldloc.0 (b)
002e010a 8bce mov ecx,esi
002e010c 8b01 mov eax,dword ptr [ecx]
002e010e 8b4028 mov eax,dword ptr [eax+28h]
IL_002b: callvirt A::NormalVirtual()
002e0111 ff5014 call dword ptr [eax+14h]
看起来很标准。让我们看一下泛型调用:
b.GenericVirtual<string>();
IL_0024: ldloc.0 (b)
IL_0025: callvirt A::GenericVirtuallong
002e00f5 6800391800 push 183900h
002e00fa 8bce mov ecx,esi
002e00fc ba50381800 mov edx,183850h
002e0101 e80ee49b71 call clr!JIT_VirtualFunctionPointer (71c9e514)
002e0106 8bce mov ecx,esi
002e0108 ffd0 call eax
好的,所以通用虚拟调用是通过加载我们的对象b
(在 中esi
,被移动到中ecx
),然后调用到clr!JIT_VirtualFunctionPointer
. 还推送了两个常量:183850
in edx
。我们可以得出结论,这可能是函数的句柄A.GenericVirtual<T>
,因为它对于 6 个调用站点中的任何一个都没有改变。另一个常量183900
看起来是泛型参数的类型句柄。事实上,SSCLI证实了这些怀疑:
HCIMPL3(CORINFO_MethodPtr, JIT_VirtualFunctionPointer, Object * objectUNSAFE,
CORINFO_CLASS_HANDLE classHnd,
CORINFO_METHOD_HANDLE methodHnd)
因此,查找基本上委托给JIT_VirtualFunctionPointer
,它必须准备一个可以调用的地址。假设它将 JIT 它并返回一个指向 JIT 代码的指针,或者制作一个蹦床,当第一次调用时,它将 JIT 函数。
0:000> uf clr!JIT_VirtualFunctionPointer
clr!JIT_VirtualFunctionPointer:
71c9e514 55 push ebp
71c9e515 8bec mov ebp,esp
71c9e517 83e4f8 and esp,0FFFFFFF8h
71c9e51a 83ec0c sub esp,0Ch
71c9e51d 53 push ebx
71c9e51e 56 push esi
71c9e51f 8bf2 mov esi,edx
71c9e521 8bd1 mov edx,ecx
71c9e523 57 push edi
71c9e524 89542414 mov dword ptr [esp+14h],edx
71c9e528 8b7d08 mov edi,dword ptr [ebp+8]
71c9e52b 85d2 test edx,edx
71c9e52d 745c je clr!JIT_VirtualFunctionPointer+0x70 (71c9e58b)
clr!JIT_VirtualFunctionPointer+0x1b:
71c9e52f 8b12 mov edx,dword ptr [edx]
71c9e531 89542410 mov dword ptr [esp+10h],edx
71c9e535 8bce mov ecx,esi
71c9e537 c1c105 rol ecx,5
71c9e53a 8bdf mov ebx,edi
71c9e53c 03ca add ecx,edx
71c9e53e c1cb05 ror ebx,5
71c9e541 03d9 add ebx,ecx
71c9e543 a180832872 mov eax,dword ptr [clr!g_pJitGenericHandleCache (72288380)]
71c9e548 8b4810 mov ecx,dword ptr [eax+10h]
71c9e54b 33d2 xor edx,edx
71c9e54d 8bc3 mov eax,ebx
71c9e54f f77104 div eax,dword ptr [ecx+4]
71c9e552 8b01 mov eax,dword ptr [ecx]
71c9e554 8b0490 mov eax,dword ptr [eax+edx*4]
71c9e557 85c0 test eax,eax
71c9e559 7430 je clr!JIT_VirtualFunctionPointer+0x70 (71c9e58b)
clr!JIT_VirtualFunctionPointer+0x47:
71c9e55b 8b4c2410 mov ecx,dword ptr [esp+10h]
clr!JIT_VirtualFunctionPointer+0x50:
71c9e55f 395804 cmp dword ptr [eax+4],ebx
71c9e562 7521 jne clr!JIT_VirtualFunctionPointer+0x6a (71c9e585)
clr!JIT_VirtualFunctionPointer+0x55:
71c9e564 39480c cmp dword ptr [eax+0Ch],ecx
71c9e567 751c jne clr!JIT_VirtualFunctionPointer+0x6a (71c9e585)
clr!JIT_VirtualFunctionPointer+0x5a:
71c9e569 397010 cmp dword ptr [eax+10h],esi
71c9e56c 7517 jne clr!JIT_VirtualFunctionPointer+0x6a (71c9e585)
clr!JIT_VirtualFunctionPointer+0x5f:
71c9e56e 397814 cmp dword ptr [eax+14h],edi
71c9e571 7512 jne clr!JIT_VirtualFunctionPointer+0x6a (71c9e585)
clr!JIT_VirtualFunctionPointer+0x64:
71c9e573 f6401801 test byte ptr [eax+18h],1
71c9e577 740c je clr!JIT_VirtualFunctionPointer+0x6a (71c9e585)
clr!JIT_VirtualFunctionPointer+0x85:
71c9e579 8b4008 mov eax,dword ptr [eax+8]
71c9e57c 5f pop edi
71c9e57d 5e pop esi
71c9e57e 5b pop ebx
71c9e57f 8be5 mov esp,ebp
71c9e581 5d pop ebp
71c9e582 c20400 ret 4
clr!JIT_VirtualFunctionPointer+0x6a:
71c9e585 8b00 mov eax,dword ptr [eax]
71c9e587 85c0 test eax,eax
71c9e589 75d4 jne clr!JIT_VirtualFunctionPointer+0x50 (71c9e55f)
clr!JIT_VirtualFunctionPointer+0x70:
71c9e58b 8b4c2414 mov ecx,dword ptr [esp+14h]
71c9e58f 57 push edi
71c9e590 8bd6 mov edx,esi
71c9e592 e8c4800400 call clr!JIT_VirtualFunctionPointer_Framed (71ce665b)
71c9e597 5f pop edi
71c9e598 5e pop esi
71c9e599 5b pop ebx
71c9e59a 8be5 mov esp,ebp
71c9e59c 5d pop ebp
71c9e59d c20400 ret 4
可以在 SSCLI 中查看实现,看起来它仍然适用:
HCIMPL3(CORINFO_MethodPtr, JIT_VirtualFunctionPointer, Object * objectUNSAFE,
CORINFO_CLASS_HANDLE classHnd,
CORINFO_METHOD_HANDLE methodHnd)
{
CONTRACTL {
SO_TOLERANT;
THROWS;
DISABLED(GC_TRIGGERS); // currently disabled because of FORBIDGC in HCIMPL
} CONTRACTL_END;
OBJECTREF objRef = ObjectToOBJECTREF(objectUNSAFE);
if (objRef != NULL && g_pJitGenericHandleCache)
{
JitGenericHandleCacheKey key(objRef->GetMethodTable(), classHnd, methodHnd);
HashDatum res;
if (g_pJitGenericHandleCache->GetValueSpeculative(&key,&res))
return (CORINFO_GENERIC_HANDLE)res;
}
// Tailcall to the slow helper
ENDFORBIDGC();
return HCCALL3(JIT_VirtualFunctionPointer_Framed, OBJECTREFToObject(objRef), classHnd, methodHnd);
}
HCIMPLEND
所以基本上它会检查缓存以查看我们之前是否见过这种类型/类组合,否则将其发送到JIT_VirtualFunctionPointer_Framed
哪个调用MethodDesc::GetMultiCallableAddrOfVirtualizedCode
以获取它的地址。该MethodDesc
调用传递了对象引用和泛型类型句柄,因此它可以查找要分派到哪个虚函数,以及虚函数的版本(即带有什么泛型参数)。
如果您想更深入地了解所有这些,可以在 SSCLI 中查看 - 似乎这在 4.0 版本的 CLR 中没有改变。
简而言之,CLR 可以满足您的期望。生成不同的调用站点,这些调用站点携带调用虚拟通用函数的类型的信息。然后将其传递给 CLR 以进行调度。复杂性在于 CLR 必须同时跟踪通用虚函数及其 JIT 处理的版本。