经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » ASP.net » 查看文章
.NET8顶级调试lldb观察FOH堆字符串分配
来源:cnblogs  作者:江湖评谈  时间:2023/12/8 11:47:28  对本文有异议

前言

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

详细

来看一个简单的例子:

  1. public static string GetPrefix() => "https://";
  2. static void Main(string[] args)
  3. {
  4. GetPrefix();
  5. GC.Collect();
  6. Console.ReadLine();
  7. }

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

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

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

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

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

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

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

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

  1. (lldb) sos dumpmd pMD
  2. Method Name: ConsoleApp1.Test+Program.Main(System.String[])
  3. Class: 00007fff78f97530
  4. MethodTable: 00007fff78f56c08
  5. mdToken: 0000000006000008
  6. Module: 00007fff78f542d0
  7. IsJitted: no
  8. Current CodeAddr: ffffffffffffffff
  9. Version History:
  10. ILCodeVersion: 0000000000000000
  11. ReJIT ID: 0
  12. IL Addr: 00007ffff7faa2aa
  13. CodeAddr: 0000000000000000 (MinOptJitted)
  14. NativeCodeVersion: 0000000000000000

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

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

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

  1. (lldb) b TryAllocateObject
  2. Breakpoint 9: 2 locations.

运行到此处

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

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

  1. 202
  2. -> 203 return object;

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

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

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

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

  1. STRINGREF AllocateStringObject(EEStringData *pStringData, bool preferFrozenObjHeap, bool* pIsFrozen)
  2. {
  3. //此处省略
  4. memcpyNoGCRefs(strDest, pStringData->GetStringBuffer(), cCount*sizeof(WCHAR));
  5. //此处省略
  6. }

然后看下它的内存:

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

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

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

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

  1. (lldb) b is_in_find_object_range
  2. (lldb) c
  3. Process 2697 resuming
  4. Process 2697 stopped
  5. * thread #1, name = 'clrrun', stop reason = breakpoint 13.8
  6. 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
  7. 7903 inline
  8. 7904 bool gc_heap::is_in_find_object_range (uint8_t* o)
  9. 7905 {
  10. -> 7906 if (o == nullptr)
  11. 7907 {
  12. 7908 return false;
  13. 7909 }

单步n

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

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

  1. (lldb) p/x g_gc_lowest_address
  2. (uint8_t *) $17 = 0x00007fbf68000000 ""
  3. (lldb) p/x g_gc_highest_address
  4. (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,文章首发,欢迎关注

原文链接:https://www.cnblogs.com/tangyanzhi1111/p/17879198.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号