用WinDbg探索CLR世界 [8] InternalCall 的使用与实现

类别:.NET开发 点击:0 评论:0 推荐:

原文:http://www.blogcn.com/User8/flier_lu/index.html?id=3270482


    在使用 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:windowsmicrosoft.net rameworkv1.1.4322mscorlib.dll)
Flags : 11
IL RVA : 00460008


    通过 DumpMD 命令查看 FindImplForMethod 函数以 ECX 寄存器传入的 MethodDesc* pMDofCall 参数,可以知道目前是哪个函数被调用。在 FindImplForMethod 函数运行结束后,再来看刚刚被处理的方法描述,就会发现 IL RVA : 00460008 已经变成 Method VA : 7921b264,而这个入口地址,正是刚刚 EmitECallMethodStub 方法从全局 ECall 表中查询到的 Marshal.Copy 对应的实现代码 CopyToNative。
以下为引用:

0:000> !dumpmd 79ba9e68
Method Name : [DEFAULT] Void System.Runtime.InteropServices.Marshal.Copy(SZArray Char,I4,I4,I4)
MethodTable 79ba916c
Module: 79b66000
mdToken: 060020d3 (e:windowsmicrosoft.net rameworkv1.1.4322mscorlib.dll)
Flags : 11
Method VA : 7921b264

0:000> u 7921b264
mscorwks!CopyToNative:
7921b264 55               push    ebp
...


    毕竟 Rotor 只是一个非商业用途的参考实现,删去优化代码来降低程序复杂度也是无可厚非的。

    在了解了 InternalCall 方法在 Rotor 中是如何被动态调用,以及在 .NET Framework 1.1 中如何被动态解析后,我们来看看这些调用和解析的背后,是如何定位方法的实现函数入口的。
    Rotor 中的 FindECFuncForMethod 函数(vm/ecall.cpp:1886)是最能说明问题的:
以下为引用:

static ECFunc* FindECFuncForMethod(MethodDesc* pMD)
{
    // check the cache
    ECFunc**cacheEntry = getCacheEntry(pMD);
    ECFunc* cur = *cacheEntry;
    if (cur != 0 && cur->m_pMD == pMD)
        return(cur);

    cur = FindImplsForClass(pMD->GetMethodTable());
    if (cur == 0)
        return(0);

    cur = GetECForIndex(FindECIndexForMethod(pMD, cur), cur);
    if (cur == 0)
        return(0);

    *cacheEntry = cur;                          // put in the cache
    return cur;
}

static ECFunc *GetECForIndex(USHORT index, ECFunc *impls)
{
    if (index == (USHORT) -1)
        return NULL;
    else
        return impls + index;
}

ECFunc* FindImplsForClass(MethodTable* pMT)
{
    return GetImplsForIndex(FindImplsIndexForClass(pMT));
}

ECFunc* GetImplsForIndex(USHORT index)
{
    if (index == (USHORT) -1)
        return NULL;
    else
        return gECClasses[index].m_pECFunc;
}


    FindECFuncForMethod 函数首先试图从缓存中获取 pMD 参数指定的方法,如果不存在缓存则调用 FindImplsIndexForClass 函数和FindECIndexForMethod 函数,分别从类和方法两个层面寻找合适的 ECall 实现。要理解这两个函数的实现,要首先看看全局 ECall 映射表是如何定义的:
以下为引用:

struct ECFunc {
    BOOL IsFCall()                      { return TRUE; }
    CorInfoIntrinsics   IntrinsicID()   { return CorInfoIntrinsics(m_intrinsicID); }

    LPCUTF8            m_wszMethodName;
    LPHARDCODEDMETASIG m_wszMethodSig;
    LPVOID             m_pImplementation;
    MethodDesc*        m_pMD;               // for reverse mapping

    int                m_intrinsicID : 8;

    ECFunc*            m_pNext;             // linked list for hash table
};

static ECFunc gStringFuncs[] =
{
  ...
  {FCFuncElement("GetHashCode",  NULL, (LPVOID)COMString::GetHashCode)},
  {FCIntrinsic("InternalLength", NULL, (LPVOID)COMString::Length, CORINFO_INTRINSIC_StringLength)},
  ...
};


    首先,对于每个类的 InternalCall 方法,需要在 ECall.cpp 中定义一个 ECFunc 类型的全局数组,数组每行对应于一个函数。内容包括函数的名字、Signature、实现函数入口等等。
以下为引用:

struct ECClass
{
    LPCUTF8      m_wszClassName;
    LPCUTF8      m_wszNameSpace;
    ECFunc      *m_pECFunc;
};

static ECClass gECClasses[] =
{
  ...
  {ECClassesElement("String", "System", gStringFuncs)},
  ...
};


    然后,对于每个拥有 InternalCall 方法的类型,需要在一个全局 ECall 注册表中增加一个表现。每项包括类型的名称、名字空间和函数映射表。这些映射表都是在编译时就预定义死了,然后编译到程序中去。虽然在发行版中没有导出这个符号,但我们可以通过 GetImplsForIndex 函数的实现代码在运行时环境中找到这个映射表:
以下为引用:

0:003> u mscorwks!GetImplsForIndex
mscorwks!GetImplsForIndex:
791d711f 55               push    ebp
791d7120 8bec             mov     ebp,esp
791d7122 66837d08ff       cmp     word ptr [ebp+0x8],0xffff
791d7127 0f840eb30200     je      mscorwks!GetImplsForIndex+0xb (7920243b)
791d712d 0fb74508         movzx   eax,word ptr [ebp+0x8]
791d7131 8d0440           lea     eax,[eax+eax*2] // index *= 3
791d7134 8b048548d13d79   mov eax,[mscorwks!gStringBufferFuncs+0x5cd8 (793dd148)+eax*4]
791d713b 5d               pop     ebp
791d713c c20400           ret     0x4
791d713f 33c0             xor     eax,eax
791d7141 40               inc     eax
791d7142 5e               pop     esi
791d7143 c3               ret

0:008> dd 793dd140
793dd140  791d7c40 791d71c4 793e0420 791d7c30
793dd150  791d71c4 793e03f0 791d86f8 791d71c4
...

0:008> db 791d7c40
791d7c40  41 70 70 44 6f 6d 61 69-6e 00 90 90 4c 6f 67 00  AppDomain...Log.
...

0:008> db 791d71c4
791d71c4  53 79 73 74 65 6d 00 90-5f 5f 4b 65 79 48 61 6e  System..__KeyHan


    GetImplsForIndex 函数中的语句 gECClasses[index].m_pECFunc 将被编译成 ADDRESS_OF(gECClasses) + index * sizeof(ECClass) + OFFSET_OF(ECClass, m_pECFunc),刚好对应于 mscorwks!GetImplsForIndex 函数中的那两行,反向解析后 793dd140 地址就是 gECClasses。而对此地址内容的查看证明了这一点。
以下为引用:

0:008> dd 793e0420
793e0420  7923db2c 00000000 7933957f 00000000
793e0430  000000ff 00000000 7923db18 00000000

...

0:008> db 7923db2c
7923db2c  43 72 65 61 74 65 42 61-73 69 63 44 6f 6d 61 69  CreateBasicDomai
7923db3c  6e 00 8d 41 14 c3 55 8b-ec 51 51 53 56 57 8b 7d  n..A..U..QQSVW.}

...

0:008> u 7933957f
mscorwks!AppDomainNative::CreateBasicDomain:
7933957f 55               push    ebp


    进一步查看 System.AppDomain 的方法映射表也是跟我们预期相同。

    知道了 ECall 的全局映射表之后,再来理解 FindImplsIndexForClass 函数和FindECIndexForMethod 函数就非常容易了。前者遍历 gECClasses 表,找出与 MethodTable 指向类型名称和名字空间相同的函数映射表入口;后者遍历函数映射表,找出函数名称相同的实现函数入口。

    至此,InternalCall 类型函数的使用和实现就大概介绍完了。通过了解这个内部原理,我们可以很方便的实现一些无法在 IL 层面实现的底层功能。

本文地址:http://com.8s8s.com/it/it44435.htm