在使用 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:[img]/images/biggrin.gif[/img]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
原文地址:http://os.chinaunix.net/a2004/0823/1042/000001042623.shtml