在使用 Reflector.NET 或者 Rotor 源码查看 BCL 库的实现时,经常会碰到一些被标记为 InternalCall 的方法。如 System.String 中用于获取字符串长度的 Length 属性,实现上就是调用被标记为 InternalCall 的 String.InternalLength 方法:
以下内容为程序代码:
namespace System
{
[Serializable]
public sealed class String : ...
{
[MethodImpl(MethodImplOptions.InternalCall)]
private int InternalLength();
public int Length
{
get
{
return this.InternalLength();
}
}
}
}
这些方法因为执行效率、安全性或者为了实现简单等不同原因,通过 IL 代码以外的 Native Code 形式提供实现代码。但与通过 DllImport 定义的 Interoper 方法不同的是,他们无需被定义为 static extern 方法,也无需通过单独的 DLL 导出函数被实现。它们作为 CLR 的诸多内部调用方式之一,被封装在一个看似密不透风的盒子里面,通过一个 InternalCall 的函数定义,将函数最终使用者与函数功能提供者隔离开来。
但实际使用中为了分析 CLR 运行机制和调试,我们经常性需要了解和分析这类函数。下面将从 CLR 内部使用与实现 InternalCall 函数的不同角度,对其做一个粗略的分析。
作为一个 BCL 函数,被定义成 InternalCall 的函数使用上与普通 IL 函数没有任何区别。如同我前面《用WinDbg探索CLR世界 [3] 跟踪方法的 JIT 过程》一文中所述,它们在 MethodTable 中,最初的入口地址也被指向 mscorwks!PreStubWorker,可以通过 sos 查看之:
以下为引用:
0:003 !Name2EE mscorlib.dll System.String
--------------------------------------
MethodTable: 79b7daf8
EEClass: 79b7de44
Name: System.String
0:003 !DumpMT -MD 79b7daf8
EEClass : 79b7de44
Module : 79b66000
Name: System.String
mdToken: 0200000f
(e:windowsmicrosoft.net rameworkv1.1.4322mscorlib.dll)
MethodTable Flags : 2000000
Number of elements in array: 2
Number of IFaces in IFaceMap : 4
Interface Map : 79b7de24
Slots in VTable : 190
--------------------------------------
MethodDesc Table
Entry
MethodDesc
JIT
Name
799917c0 79b7ebc8
PreJIT [DEFAULT] [hasThis] String System.String.ToString()
...
79b7e253 79b7e258
None
[DEFAULT] [hasThis] I4 System.String.InternalLength()
...
0:003 !DumpMD 79b7e258
Method Name : [DEFAULT] [hasThis] I4 System.String.InternalLength()
MethodTable 79b7daf8
Module: 79b66000
mdToken: 060000b1 (e:windowsmicrosoft.net rameworkv1.1.4322mscorlib.dll)
Flags : 1
IL RVA : 0073000b
通过上述命令我们可以看到,String.InternalLength 方法缺省没有经过 JIT 编译,其入口地址为 79b7e253。反汇编此地址的指令,并一路追述下去可以发现,实际上最终也是调用 mscorwks!PreStubWorker 方法:
以下为引用:
0:003 u 79b7e253
mscorlib_79980000+0x1fe253:
79b7e253 e8287ffeff
call
mscorlib_79980000+0x1e6180 (79b66180)
79b7e258 4d
dec
ebp
...
mscorlib_79980000+0x1e6180:
79b66180 e9eb805e86
jmp
0014e270
79b66185 0000
add
[eax],al
...
0:003 u 0014e270
0014e270 52
push
edx
...
0014e290 56
push
esi
0014e291 e8b4870879
call
mscorwks!PreStubWorker (791d6a4a)
0014e296 897b08
mov
[ebx+0x8],edi
...
这个 PreStubWorker 函数(vm/prestub.cpp:574)可以说是所有 IL 函数进行 JIT 的入口,负责编译 IL 代码以生成 Native 代码,并将 JIT 生成的代码入口安装到相应 MD (MethodDesc) 上:
以下内容为程序代码:
extern "C" const BYTE * __stdcall PreStubWorker(PrestubMethodFrame *pPFrame)
{
MethodDesc *pMD = pPFrame-GetFunction();
MethodTable *pDispatchingMT = NULL;
if (pMD-IsVtableMethod() && !pMD-GetClass()-IsValueClass())
{
OBJECTREF curobj = GetActiveObject(pPFrame);
if (curobj != 0)
pDispatchingMT = curobj-GetMethodTable();
}
return pMD-DoPrestub(pDispatchingMT);
}
PreStubWorker 函数的参数是一个方法帧,从中可以获取当前函数的 MD,进而调用此方法的 DoPresub 函数完成实际工作。而 MethodDesc:oPrestub 方法(vm/prestub.cpp:590)中,在进行实际代码生成时,会根据方法的类型进行各种特殊情况的处理:
以下内容为程序代码:
const BYTE * MethodDesc:
oPrestub(MethodTable *pDispatchingMT){
Stub *pStub = NULL;
//...
if (IsSpecialStub())
{
//...
}
else if (IsIL())
{
//...
}
else
//!IsSpecialStub() && !IsIL() case
{
if (IsECall())
{
// See if it is an FCALL and already "jitted", which for fcall
// means that its m_CodeOrIL is not already set. We explicitly
// check for the mcECall bit since IsECall is really
// IsRuntimeGenerated and so includes array also
if (IsJitted() && (mcECall == GetClassification()))
pStub = (Stub*) GetAddrofJittedCode();
else
pStub = (Stub*) FindImplForMethod(this);
}
if (pStub != 0)
{
_ASSERTE(IsECall() || !(GetClass()-IsAnyDelegateClass()));
if (!fRemotingIntercepted && !(GetClass()-IsAnyDelegateClass()))
{
// backpatch the main slot.
pMT-GetVtable()[GetSlot()] = (SLOT) pStub;
}
bBashCall = bIsCode = TRUE;
}
else
{
//...
}
}
}
}
inline DWORD MethodDesc::IsECall()
{
return mcECall == GetClassification() || mcArray == GetClassification();
}
这儿 IsSpecialStub(), IsIL(), IsECall()等等方法,实际上都是通过 GetClassification() 获取方法类型来进行判断的。而此方法类型则是编译器根据 MethodImplAttribute 等标记,在编译时写入到 Metadata 中。对 MethodImplOptions.InternalCall 来说,实际对应于 mcECall 这种类型。其他的 CLR 内部调用类型,以后有机会再详细介绍。
对于 GetClassification() 返回 mcECall 这种情况,实际上时通过 FindImplForMethod() 函数完成的。此函数在 RVA 为 0 的情况下,会调用 FindECFuncForMethod 从一个全局 ECall 注册表中查找 InternalCall 的实现代码所在。
以下内容为程序代码:
void* FindImplForMethod(MethodDesc* pMDofCall)
{
DWORD_PTR rva = pMDofCall-GetRVA();
// ...
if (rva == 0)
{
ret = FindECFuncForMethod(pBaseMD);
}
// ...
}
不过与 Rotor 的实现不太一样的是,.NET Framework 1.1 为效率做了很多额外的优化工作。如前面的 DumpMD 命令结果所示,CLR v1.1 中 InternalCall 的方法也是有 RVA 的,只是他们指向的是一个直接返回的 ret 的 IL 指令。而 FindImplForMethod 对 ECall 类型的处理方法,也因 rva 不为 0,而从每次调用时以 FindECFuncForMethod 函数在全局 ECall 注册表中通过字符串匹配查找,改为通过 mscorwks!ECall::EmitECallMethodStub() 方法,生成一个对 ECall 实现代码的调用 Stub 代码。这样一来,只需要在第一次调用 ECall 代码时,完成字符串匹配性质的 ECall 实现代码定位,就可以一劳永逸的以等同于 JIT 代码的方式调用了。
可以通过在 FindImplForMethod 方法上下断点的方式,跟踪每次 InternalCall 类型函数的调用初始化工作,如:
以下为引用:
0:000 bp mscorwks!FindImplForMethod
0:000 g
Breakpoint 0 hit
eax=00000001 ebx=00000001 ecx=79ba9e68 edx=c0000000 esi=79ba9e68 edi=00000000
eip=791d8d5b esp=0012f084 ebp=0012f158 iopl=0
nv up ei pl nz na pe nc
cs=001b
ss=0023
ds=0023
es=0023
fs=0038
gs=0000
efl=00000202
mscorwks!FindImplForMethod:
791d8d5b 55
push
ebp
0:000 !dumpmd ecx
Method Name : [DEFAULT] Void System.Runtime.InteropServices.Marshal.Copy(SZArray Char,I4,I4,I4)
MethodTable 79ba916c
Module: 79b66000
mdToken: 060020d3 (e:windowsmicrosof