- A+
楔子
还是做点事情,不要那么散漫。
本文以简单的Substring(int startindex,int Length)
函数为例,来递进下它在托管和非托管的一些行为。
以下均为个人理解,如有疏漏请指正。
定义和实现
它的定义是在System.Runtime.dll里面
public string Substring(int startIndex, int length) { throw null; }
它的实现在System.Private.CoreLib.dll里面
public string Substring(int startIndex, int length) { //此处省略一万字 return InternalSubString(startIndex, length); }
继续来看下InternalSubString
private string InternalSubString(int startIndex, int length) { string text = string.FastAllocateString(length); UIntPtr elementCount = (UIntPtr)text.Length; Buffer.Memmove<char>(ref text._firstChar, Unsafe.Add<char>(ref this._firstChar, (IntPtr)((UIntPtr)startIndex)), elementCount); return text; }
FastAllocateString是个FCall函数(也就是微软提供的在托管里面调用非托管的一种方式,它的实际实现是在JIT里面)
[MethodImpl(MethodImplOptions.InternalCall)] internal static extern string FastAllocateString(int length);
Buffer.Memmove是个托管函数,它的作用主要是把FastAllocateString返回的string对象赋值为startIndex和elementCount中间的字符串。过程是利用了Unsafe.Add(它的定义在System.Runtime.CompilerServices,实现实在CLR里面)指针偏移来实现,过程比较简单,不赘述。
FastAllocateString
重点在于这个函数,这个函数进入到了非托管。它进入的方式是通过RyuJit加载这个方法的IL代码。然后对这个IL代码进行解析,重构成汇编代码。
它的非托管原型如下:
#define _DYNAMICALLY_ASSIGNED_FCALLS_BASE() DYNAMICALLY_ASSIGNED_FCALL_IMPL(FastAllocateString, FramedAllocateString)
FramedAllocateString原型如下:
HCIMPL1(StringObject*, FramedAllocateString, DWORD stringLength) { FCALL_CONTRACT; STRINGREF result = NULL; HELPER_METHOD_FRAME_BEGIN_RET_0(); // Set up a frame result = AllocateString(stringLength); HELPER_METHOD_FRAME_END(); return((StringObject*) OBJECTREFToObject(result)); } HCIMPLEND
注意了,FastAllocateString实际上调用的不是FramedAllocateString。因为在CLR启动加载的时候,FastAllocateString被替换成了FCall函数形式的调用
ECall::DynamicallyAssignFCallImpl(GetEEFuncEntryPoint(AllocateStringFastMP_InlineGetThread), ECall::FastAllocateString);
DynamicallyAssignFCallImpl原型:
void ECall::DynamicallyAssignFCallImpl(PCODE impl, DWORD index) { CONTRACTL { NOTHROW; GC_NOTRIGGER; MODE_ANY; } CONTRACTL_END; _ASSERTE(index < NUM_DYNAMICALLY_ASSIGNED_FCALL_IMPLEMENTATIONS); g_FCDynamicallyAssignedImplementations[index] = impl; }
可以看到FastAllocateString作为了索引Index,而他的实现是AllocateStringFastMP_InlineGetThread。
再来看下它的堆栈
> coreclr.dll!ECall::DynamicallyAssignFCallImpl(unsigned __int64 0x00007ffdeed5df50, unsigned long 0x061b1d50) C++ coreclr.dll!InitJITHelpers1() C++ coreclr.dll!EEStartupHelper() C++ coreclr.dll!`EEStartup'::`9'::__Body::Run(void * 0x0000000000000000) C++ coreclr.dll!EEStartup() C++ coreclr.dll!EnsureEEStarted() C++ coreclr.dll!CorHost2::Start() C++ coreclr.dll!coreclr_initialize(const char *
很明显它是在CLR初始化的时候被替代的
何时被调用
最后一个问题,既然FastAllocateString被替代了,那它何时被调用的呢?
在代码:
private string InternalSubString(int startIndex, int length) { string text = string.FastAllocateString(length); UIntPtr elementCount = (UIntPtr)text.Length; Buffer.Memmove<char>(ref text._firstChar, Unsafe.Add<char>(ref this._firstChar, (IntPtr)((UIntPtr)startIndex)), elementCount); return text; }
这里面调用了string.FastAllocateString函数,通过上面推断,实际上它已经被被替换了。注意了,但是替换之前,还得按照CLR内存模型进行运作调用。当我们调用InternalSubString的时候,里面调用了FastAllocateString,后者通过PrecodeFixupThunk来进行替换。
这点可以通过汇编验证:
System_Private_CoreLib!System.String.InternalSubString+0xc: 00007ffd`9a86132c 418bc8 mov ecx,r8d 0:000> t System_Private_CoreLib!System.String.InternalSubString+0xf: 00007ffd`9a86132f ff15b39f7e00 call qword ptr [System_Private_CoreLib+0x9cb2e8 (00007ffd`9b04b2e8)] ds:00007ffd`9b04b2e8={coreclr!AllocateStringFastMP_InlineGetThread (00007ffd`9b20b3a0)} 0:000> t coreclr!AllocateStringFastMP_InlineGetThread: 00007ffd`9b20b3a0 4c8b0d090d3400 mov r9,qword ptr [coreclr!g_pStringClass (00007ffd`9b54c0b0)] ds:00007ffd`9b54c0b0=00007ffd3b6ed698
call qword ptr [System_Private_CoreLib+0x9cb2e8 (00007ffd`9b04b2e8)] ds:00007ffd`9b04b2e8={coreclr!AllocateStringFastMP_InlineGetThread (00007ffd`9b20b3a0)} 就是直接调用了AllocateStringFastMP_InlineGetThread,然后跳转到后者的地址
AllocateStringFastMP_InlineGetThread
这个函数的作用实际上是申请内存,比如你 new 一个对象的时候,又或者本例,你需要一个新的字符串对象来存储截取的字符串。
作者:江湖评谈
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。