.NET8顶级调试lldb观察FOH堆字符串分配

前言

好久没有动用LLDB了,本篇通过它来看下FOH也就是.NET8里面优化字符串,为了提高其性能增加的FOH堆分配过程。关于FOH可以参考:.NET8极致性能优化Non-GC Heap

详细

来看一个简单的例子:

public static string GetPrefix() => "https://"; static void Main(string[] args) {    GetPrefix();    GC.Collect();    Console.ReadLine(); } 

函数GetPrefix里面的字符串“https://”就是被分配到FOH堆里面的,如何验证呢?

首先通过LLDB把CLR运行到托管Main入口

(lldb) b RunMainInternal Breakpoint 7: where = libcoreclr.so`RunMainInternal(Param*) at assembly.cpp:1257, address = 0x00007ffff6d43930 (lldb) r Process 2697 launched: '/home/tang/opt/dotnet/debug_clr/clrrun' (x86_64) Process 2697 stopped * thread #1, name = 'clrrun', stop reason = breakpoint 6.1 7.1     frame #0: 0x00007ffff6d43930 libcoreclr.so`RunMainInternal(pParam=0x00007ffff7faaab6) at assembly.cpp:1257    1254  } param;    1255      1256  static void RunMainInternal(Param* pParam) -> 1257  {    1258      MethodDescCallSite  threadStart(pParam->pFD);    1259      1260      PTRARRAYREF StrArgArray = NULL; (lldb)  

然后把其运行到JIT前置入口

(lldb) b PreStubWorker Breakpoint 8: where = libcoreclr.so`::PreStubWorker(TransitionBlock *, MethodDesc *) at prestub.cpp:1865, address = 0x00007ffff6ee6c10 (lldb) c Process 2697 resuming Process 2697 stopped * thread #1, name = 'clrrun', stop reason = breakpoint 8.1     frame #0: 0x00007ffff6ee6c10 libcoreclr.so`::PreStubWorker(pTransitionBlock=0x00000000ffffcb38, pMD=0x0000000155608c70) at prestub.cpp:1865    1862  // returns a pointer to the new code for the prestub's convenience.    1863  //=============================================================================    1864  extern "C" PCODE STDCALL PreStubWorker(TransitionBlock* pTransitionBlock, MethodDesc* pMD) -> 1865  {    1866      PCODE pbRetVal = NULL;    1867      1868      BEGIN_PRESERVE_LAST_ERROR; 

此时可以看下当前JIT编译的函数是谁,这里需要先n命令单步一下

(lldb) n Process 2697 stopped * thread #1, name = 'clrrun', stop reason = step over     frame #0: 0x00007ffff6ee6c36 libcoreclr.so`::PreStubWorker(pTransitionBlock=0x00007fffffffc648, pMD=0x00007fff78f56b70) at prestub.cpp:1866:11    1863  //=============================================================================    1864  extern "C" PCODE STDCALL PreStubWorker(TransitionBlock* pTransitionBlock, MethodDesc* pMD)    1865  { -> 1866      PCODE pbRetVal = NULL;    1867      1868      BEGIN_PRESERVE_LAST_ERROR; 

然后通过微软提供的sos.dll Dump下当前的函数描述结构体MethodDesc,pMD是传过来的函数参数,也即是MethodDesc的变量

(lldb) sos dumpmd pMD Method Name:          ConsoleApp1.Test+Program.Main(System.String[]) Class:                00007fff78f97530 MethodTable:          00007fff78f56c08 mdToken:              0000000006000008 Module:               00007fff78f542d0 IsJitted:             no Current CodeAddr:     ffffffffffffffff Version History:   ILCodeVersion:      0000000000000000   ReJIT ID:           0   IL Addr:            00007ffff7faa2aa      CodeAddr:           0000000000000000  (MinOptJitted)      NativeCodeVersion:  0000000000000000 

可以清晰的看到Method Name就是ConsoleApp1.Test+Program.Main,OK这一步确定了,我们下面继续寻找字符串分配到FOH,首先删掉前面所有的断点

(lldb) br del About to delete all breakpoints, do you want to do that?: [Y/n] y All breakpoints removed. (3 breakpoints) 

在TryAllocateObject 函数上下断,它是分配托管内存的函数

(lldb) b TryAllocateObject Breakpoint 9: 2 locations. 

运行到此处

(lldb) c Process 2697 resuming Process 2697 stopped * thread #1, name = 'clrrun', stop reason = breakpoint 9.1     frame #0: 0x00007ffff70300c0 libcoreclr.so`FrozenObjectHeapManager::TryAllocateObject(this=0x00007fffffff80b8, type=0x00000008017fa948, objectSize=140737488322759, publish=false) at frozenobjectheap.cpp:22    19    // May return nullptr if object is too large (larger than FOH_COMMIT_SIZE)    20    // in such cases caller is responsible to find a more appropriate heap to allocate it    21    Object* FrozenObjectHeapManager::TryAllocateObject(PTR_MethodTable type, size_t objectSize, bool publish) -> 22    {    23        CONTRACTL    24        {    25            THROWS; 

这个函数就是字符串分配到FOH堆的地方,通过它的函数所在的类名即可看出FrozenObjectHeapManager,但是我们依然还是需要验证下。继续n单步这个函数的返回的地方,也就是如下代码:

 202 -> 203       return object; 

此时的这个object变量就是示例里面字符串的“https://”的对象地址,看下它的地址值

(lldb) p/x object (Object *) $14 = 0x00007fffe6bff8c0 

记住这个值:0x00007fffe6bff8c0,后面会把它和GC堆的范围进行一个比较。如果它不在GC堆范围,说明.NET8的字符串确实分配在了FOH堆里面。

我们继续单步向下,运行到这个对象被赋值字符串的地方

STRINGREF AllocateStringObject(EEStringData *pStringData, bool preferFrozenObjHeap, bool* pIsFrozen) {     //此处省略    memcpyNoGCRefs(strDest, pStringData->GetStringBuffer(), cCount*sizeof(WCHAR));      //此处省略 } 

然后看下它的内存:

(lldb) memory re 0x00007fffe6bff8c0 0x7fffe6bff8c0: 68 00 74 00 74 00 70 00 73 00 3a 00 2f 00 2f 00  h.t.t.p.s.:././. 0x7fffe6bff8dc: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 

它确实是https字符串的对象地址。没有问题。
避免干扰,此时我们再次删除所有断点

(lldb) br del About to delete all breakpoints, do you want to do that?: [Y/n] y All breakpoints removed. (1 breakpoint) 

然后在函数is_in_find_object_range处下断,它是在GC回收的时候,判断当前的对象地址是否在GC堆里面,如果是则进行对象标记,如果不是直接返回。可以通过这个获取GC堆的范围,运行到此处

(lldb) b is_in_find_object_range (lldb) c Process 2697 resuming Process 2697 stopped * thread #1, name = 'clrrun', stop reason = breakpoint 13.8     frame #0: 0x00007ffff72d881d libcoreclr.so`WKS::GCHeap::Promote(Object**, ScanContext*, unsigned int) [inlined] WKS::gc_heap::is_in_find_object_range(o=0x0000000000000000) at gc.cpp:7906:11    7903  inline    7904  bool gc_heap::is_in_find_object_range (uint8_t* o)    7905  { -> 7906      if (o == nullptr)    7907      {    7908          return false;    7909      } 

单步n

(lldb) n Process 2697 stopped * thread #1, name = 'clrrun', stop reason = step over     frame #0: 0x00007ffff72d8831 libcoreclr.so`WKS::GCHeap::Promote(Object**, ScanContext*, unsigned int) [inlined] WKS::gc_heap::is_in_find_object_range(o="@x9bxd6xxffU0000007f") at gc.cpp:7911:14    7908          return false;    7909      }    7910  #if defined(USE_REGIONS) && defined(FEATURE_CONSERVATIVE_GC) -> 7911      return ((o >= g_gc_lowest_address) && (o < bookkeeping_covered_committed));    7912  #else //USE_REGIONS && FEATURE_CONSERVATIVE_GC    7913      if ((o >= g_gc_lowest_address) && (o < g_gc_highest_address))    7914      { 

注意,此时我们看到了GC堆的一个范围,也就是变量g_gc_lowest_address和变量g_gc_highest_address,看下它们的地址范围

(lldb) p/x g_gc_lowest_address  (uint8_t *) $17 = 0x00007fbf68000000 "" (lldb) p/x g_gc_highest_address (uint8_t *) $18 = 0x00007fff68000000 "0" 

上面很明显了,GC堆的范围起始地址:0x00007fbf68000000 ,结束地址:0x00007fff68000000 。而字符串“https://”的对象地址是0x00007fffe6bff8c0,很明显它不在GC堆的范围内。

以上通过分配一个字符串到FOH堆,后调用一个GC.Collect()查看GC堆的范围,对FOH对象地址和GC堆范围进行一个判断,为一个非常简单的FOH字符串分配验证。

欢迎加入C#12/.NET8最新技术交流群

结尾

作者:jianghupt
原文:.NET8顶级调试lldb观察FOH堆字符串分配
公众号:jianghupt,文章首发,欢迎关注

发表评论

评论已关闭。

相关文章