在使用 ReflectorNET 或者 Rotor 源码查看 BCL 库的实现时经常会碰到一些被标记为 InternalCall 的方法如 SystemString 中用于获取字符串长度的 Length 属性实现上就是调用被标记为 InternalCall 的 StringInternalLength 方法
以下内容为程序代码:
namespace System
{
[Serializable]
public sealed class String :
{
[MethodImpl(MethodImplOptionsInternalCall)]
private int InternalLength();
public int Length
{
get
{
return thisInternalLength();
}
}
}
}
这些方法因为执行效率安全性或者为了实现简单等不同原因通过 IL 代码以外的 Native Code 形式提供实现代码但与通过 DllImport 定义的 Interoper 方法不同的是他们无需被定义为 static extern 方法也无需通过单独的 DLL 导出函数被实现它们作为 CLR 的诸多内部调用方式之一被封装在一个看似密不透风的盒子里面通过一个 InternalCall 的函数定义将函数最终使用者与函数功能提供者隔离开来
但实际使用中为了分析 CLR 运行机制和调试我们经常性需要了解和分析这类函数下面将从 CLR 内部使用与实现 InternalCall 函数的不同角度对其做一个粗略的分析
作为一个 BCL 函数被定义成 InternalCall 的函数使用上与普通 IL 函数没有任何区别如同我前面《用WinDbg探索CLR世界 [] 跟蹤方法的 JIT 过程》一文中所述它们在 MethodTable 中最初的入口地址也被指向 mscorwks!PreStubWorker可以通过 sos 查看之
以下为引用
:> !NameEE mscorlibdll SystemString
MethodTable: bdaf
EEClass: bde
Name: SystemString
:> !DumpMT MD bdaf
EEClass : bde
Module : b
Name: SystemString
mdToken: f(e: rameworkvmscorlibdll)
MethodTable Flags :
Number of elements in array:
Number of IFaces in IFaceMap :
Interface Map : bde
Slots in VTable :
MethodDesc Table
EntryMethodDesc JIT Name
c bebcPreJIT [DEFAULT] [hasThis] String SystemStringToString()
be beNone [DEFAULT] [hasThis] I SystemStringInternalLength()
:> !DumpMD be
Method Name : [DEFAULT] [hasThis] I SystemStringInternalLength()
MethodTable bdaf
Module: b
mdToken: b (e: rameworkvmscorlibdll)
Flags :
IL RVA : b
通过上述命令我们可以看到StringInternalLength 方法缺省没有经过 JIT 编译其入口地址为 be反汇编此地址的指令并一路追述下去可以发现实际上最终也是调用 mscorwks!PreStubWorker 方法
以下为引用
:> u be
mscorlib_+xfe:
be effeff callmscorlib_+xe (b)
be d dec ebp
mscorlib_+xe:
b eebe jmp e
b add [eax]al
:> u e
e pushedx
e pushesi
e eb callmscorwks!PreStubWorker (daa)
e b mov [ebx+x]edi
这个 PreStubWorker 函数(vm/prestubcpp:)可以说是所有 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 != )
pDispatchingMT = curobj>GetMethodTable();
}
return pMD>DoPrestub(pDispatchingMT);
}
PreStubWorker 函数的参数是一个方法帧从中可以获取当前函数的 MD进而调用此方法的 DoPresub 函数完成实际工作而 MethodDesc:oPrestub 方法(vm/prestubcpp:)中在进行实际代码生成时会根据方法的类型进行各种特殊情况的处理
以下内容为程序代码:
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 != )
{
_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 中对 MethodImplOptionsInternalCall 来说实际对应于 mcECall 这种类型其他的 CLR 内部调用类型以后有机会再详细介绍
对于 GetClassification() 返回 mcECall 这种情况实际上时通过 FindImplForMethod() 函数完成的此函数在 RVA 为 的情况下会调用 FindECFuncForMethod 从一个全局 ECall 注册表中查找 InternalCall 的实现代码所在
以下内容为程序代码:
void* FindImplForMethod(MethodDesc* pMDofCall)
{
DWORD_PTR rva = pMDofCall>GetRVA();
//
if (rva == )
{
ret = FindECFuncForMethod(pBaseMD);
}
//
}
不过与 Rotor 的实现不太一样的是NET Framework 为效率做了很多额外的优化工作如前面的 DumpMD 命令结果所示CLR v 中 InternalCall 的方法也是有 RVA 的只是他们指向的是一个直接返回的 ret 的 IL 指令而 FindImplForMethod 对 ECall 类型的处理方法也因 rva 不为 而从每次调用时以 FindECFuncForMethod 函数在全局 ECall 注册表中通过字符串匹配查找改为通过 mscorwks!ECall::EmitECallMethodStub() 方法生成一个对 ECall 实现代码的调用 Stub 代码这样一来只需要在第一次调用 ECall 代码时完成字符串匹配性质的 ECall 实现代码定位就可以一劳永逸的以等同于 JIT 代码的方式调用了
可以通过在 FindImplForMethod 方法上下断点的方式跟蹤每次 InternalCall 类型函数的调用初始化工作如
以下为引用
:> bp mscorwks!FindImplForMethod
:> g
Breakpoint hit
eax= ebx= ecx=bae edx=c esi=bae edi=
eip=ddb esp=f ebp=f iopl= nv up ei pl nz na pe nc
cs=bss=ds=es=fs=gs= efl=
mscorwks!FindImplForMethod:
ddb pushebp
:> !dumpmd ecx
Method Name : [DEFAULT] Void SystemRuntimeInteropServicesMarshalCopy(SZArray CharIII)
MethodTable bac
Module: b
mdToken: d (e:windowsmicrosof