编译调试 .NET Core 5.0 Preview 并分析 Span 的实现原理(二)


风晓
风晓 2023-12-31 10:19:45 54302 赞同 0 反对 0
分类: 资源
接编译调试 .NET Core 5.0 Preview 并分析 Span 的实现原理(一)

查看托管函数对应 GC 信息中的各个 Slot

GC 信息是 .NET 运行时查找各个线程中托管函数的本地变量 (根对象) 时使用的信息,因为 GC 信息的编码非常复杂,这里不会介绍如何解码 GC 信息,
而是下断点来看各个 Slot 的内容,从扫描到标记的调用链跟踪 (backtrace) 如下:

  * frame #0: 0x00007ffff5cb0fcf libcoreclr.so`WKS::gc_heap::mark_object_simple(po=0x00007fffffffa460) at gc.cpp:19675
    frame #1: 0x00007ffff5cb6fe8 libcoreclr.so`WKS::GCHeap::Promote(ppObject=0x00007fffffffd230, sc=0x00007fffffffc9c0, flags=1) at gc.cpp:36730
    frame #2: 0x00007ffff5808fe8 libcoreclr.so`PromoteCarefully(fn=(libcoreclr.so`WKS::GCHeap::Promote(Object**, ScanContext*, unsigned int) at gc.cpp:36666), ppObj=0x00007fffffffd230, sc=0x00007fffffffc9c0, flags=1)(Object**, ScanContext*, unsigned int), Object**, ScanContext*, unsigned int) at siginfo.cpp:4874
    frame #3: 0x00007ffff5918c4a libcoreclr.so`GcEnumObject(pData=0x00007fffffffc710, pObj=0x00007fffffffd230, flags=1) at gcenv.ee.common.cpp:167
    frame #4: 0x00007ffff5a87abc libcoreclr.so`GcInfoDecoder::ReportStackSlotToGC(this=0x00007fffffffab38, spOffset=-80, spBase=GC_FRAMEREG_REL, gcFlags=1, pRD=0x00007fffffffb5c0, flags=0, pCallBack=(libcoreclr.so`GcEnumObject(void*, OBJECTREF*, unsigned int) at gcenv.ee.common.cpp:148), hCallBack=0x00007fffffffc710)(void*, OBJECTREF*, unsigned int), void*) at gcinfodecoder.cpp:1848
    frame #5: 0x00007ffff5a88381 libcoreclr.so`GcInfoDecoder::ReportSlotToGC(this=0x00007fffffffab38, slotDecoder=0x00007fffffffa8d0, slotIndex=0, pRD=0x00007fffffffb5c0, reportScratchSlots=true, inputFlags=0, pCallBack=(libcoreclr.so`GcEnumObject(void*, OBJECTREF*, unsigned int) at gcenv.ee.common.cpp:148), hCallBack=0x00007fffffffc710)(void*, OBJECTREF*, unsigned int), void*) at gcinfodecoder.h:679
    frame #6: 0x00007ffff5a8666d libcoreclr.so`GcInfoDecoder::ReportUntrackedSlots(this=0x00007fffffffab38, slotDecoder=0x00007fffffffa8d0, pRD=0x00007fffffffb5c0, inputFlags=0, pCallBack=(libcoreclr.so`GcEnumObject(void*, OBJECTREF*, unsigned int) at gcenv.ee.common.cpp:148), hCallBack=0x00007fffffffc710)(void*, OBJECTREF*, unsigned int), void*) at gcinfodecoder.cpp:1034
    frame #7: 0x00007ffff5a85d28 libcoreclr.so`GcInfoDecoder::EnumerateLiveSlots(this=0x00007fffffffab38, pRD=0x00007fffffffb5c0, reportScratchSlots=false, inputFlags=0, pCallBack=(libcoreclr.so`GcEnumObject(void*, OBJECTREF*, unsigned int) at gcenv.ee.common.cpp:148), hCallBack=0x00007fffffffc710)(void*, OBJECTREF*, unsigned int), void*) at gcinfodecoder.cpp:983
    frame #8: 0x00007ffff570225a libcoreclr.so`EECodeManager::EnumGcRefs(this=0x0000555555822680, pRD=0x00007fffffffb5c0, pCodeInfo=0x00007fffffffb3f0, flags=0, pCallBack=(libcoreclr.so`GcEnumObject(void*, OBJECTREF*, unsigned int) at gcenv.ee.common.cpp:148), hCallBack=0x00007fffffffc710, relOffsetOverride=4294967295)(void*, OBJECTREF*, unsigned int), void*, unsigned int) at eetwain.cpp:5150
    frame #9: 0x00007ffff5919462 libcoreclr.so`GcStackCrawlCallBack(pCF=0x00007fffffffb1c0, pData=0x00007fffffffc710) at gcenv.ee.common.cpp:283
    frame #10: 0x00007ffff580e52f libcoreclr.so`Thread::MakeStackwalkerCallback(this=0x0000555555838aa0, pCF=0x00007fffffffb1c0, pCallback=(libcoreclr.so`GcStackCrawlCallBack(CrawlFrame*, void*) at gcenv.ee.common.cpp:201), pData=0x00007fffffffc710, uFramesProcessed=5)(CrawlFrame*, void*), void*, unsigned int) at stackwalk.cpp:886
    frame #11: 0x00007ffff580e77b libcoreclr.so`Thread::StackWalkFramesEx(this=0x0000555555838aa0, pRD=0x00007fffffffb5c0, pCallback=(libcoreclr.so`GcStackCrawlCallBack(CrawlFrame*, void*) at gcenv.ee.common.cpp:201), pData=0x00007fffffffc710, flags=34048, pStartFrame=0x0000000000000000)(CrawlFrame*, void*), void*, unsigned int, Frame*) at stackwalk.cpp:966
    frame #12: 0x00007ffff580f337 libcoreclr.so`Thread::StackWalkFrames(this=0x0000555555838aa0, pCallback=(libcoreclr.so`GcStackCrawlCallBack(CrawlFrame*, void*) at gcenv.ee.common.cpp:201), pData=0x00007fffffffc710, flags=34048, pStartFrame=0x0000000000000000)(CrawlFrame*, void*), void*, unsigned int, Frame*) at stackwalk.cpp:1049
    frame #13: 0x00007ffff5ceeadb libcoreclr.so`ScanStackRoots(pThread=0x0000555555838aa0, fn=(libcoreclr.so`WKS::GCHeap::Promote(Object**, ScanContext*, unsigned int) at gc.cpp:36666), sc=0x00007fffffffc9c0)(Object**, ScanContext*, unsigned int), ScanContext*) at gcenv.ee.cpp:146
    frame #14: 0x00007ffff5cee7ab libcoreclr.so`GCToEEInterface::GcScanRoots(fn=(libcoreclr.so`WKS::GCHeap::Promote(Object**, ScanContext*, unsigned int) at gc.cpp:36666), condemned=2, max_gen=2, sc=0x00007fffffffc9c0)(Object**, ScanContext*, unsigned int), int, int, ScanContext*) at gcenv.ee.cpp:182
    frame #15: 0x00007ffff5cfa3d9 libcoreclr.so`GCScan::GcScanRoots(fn=(libcoreclr.so`WKS::GCHeap::Promote(Object**, ScanContext*, unsigned int) at gc.cpp:36666), condemned=2, max_gen=2, sc=0x00007fffffffc9c0)(Object**, ScanContext*, unsigned int), int, int, ScanContext*) at gcscan.cpp:155
    frame #16: 0x00007ffff5c9f701 libcoreclr.so`WKS::gc_heap::mark_phase(condemned_gen_number=2, mark_only_p=NO) at gc.cpp:21062
    frame #17: 0x00007ffff5c9b479 libcoreclr.so`WKS::gc_heap::gc1() at gc.cpp:16713
    frame #18: 0x00007ffff5cab832 libcoreclr.so`WKS::gc_heap::garbage_collect(n=2) at gc.cpp:18345
    frame #19: 0x00007ffff5c90dea libcoreclr.so`WKS::GCHeap::GarbageCollectGeneration(this=0x0000555555793aa0, gen=2, reason=reason_induced) at gc.cpp:38188
    frame #20: 0x00007ffff5cdd3bb libcoreclr.so`WKS::GCHeap::GarbageCollectTry(this=0x0000555555793aa0, generation=2, low_memory_p=NO, mode=2) at gc.cpp:37524
    frame #21: 0x00007ffff5cde614 libcoreclr.so`WKS::GCHeap::GarbageCollect(this=0x0000555555793aa0, generation=2, low_memory_p=false, mode=2) at gc.cpp:37458
    frame #22: 0x00007ffff58be151 libcoreclr.so`GCInterface::Collect(generation=-1, mode=2) at comutilnative.cpp:986
    frame #23: 0x00007fff7bb55853
    frame #24: 0x00007fff7bb55788
    frame #25: 0x00007fff7bb553c3
    frame #26: 0x00007ffff5a965f3 libcoreclr.so`CallDescrWorkerInternal at unixasmmacrosamd64.inc:862
    frame #27: 0x00007ffff589cc9c libcoreclr.so`CallDescrWorkerWithHandler(pCallDescrData=0x00007fffffffd5a8, fCriticalCall=NO) at callhelpers.cpp:70
    frame #28: 0x00007ffff589da1c libcoreclr.so`MethodDescCallSite::CallTargetWorker(this=0x00007fffffffd6e0, pArguments=0x00007fffffffd680, pReturnValue=0x0000000000000000, cbReturnValue=0) at callhelpers.cpp:546
    frame #29: 0x00007ffff56ee983 libcoreclr.so`MethodDescCallSite::Call(this=0x00007fffffffd6e0, pArguments=0x00007fffffffd680) at callhelpers.h:459
    frame #30: 0x00007ffff5ac1c64 libcoreclr.so`RunMainInternal(pParam=0x00007fffffffd950) at assembly.cpp:1487
    frame #31: 0x00007ffff5ac1989 libcoreclr.so`RunMain(this=0x00007fffffffd858, pParam=0x00007fffffffd950)::$_1::operator()(Param*) const::'lambda'(Param*)::operator()(Param*) const at assembly.cpp:1559
    frame #32: 0x00007ffff5abf1f9 libcoreclr.so`RunMain(this=0x00007fffffffd940, __EXparam=0x00007fffffffd950)::$_1::operator()(Param*) const at assembly.cpp:1561
    frame #33: 0x00007ffff5abf019 libcoreclr.so`RunMain(pFD=0x00007fff7bd5c368, numSkipArgs=1, piRetVal=0x00007fffffffda4c, stringArgs=0x00007fffffffdf20) at assembly.cpp:1561
    frame #34: 0x00007ffff5abf4a2 libcoreclr.so`Assembly::ExecuteMainMethod(this=0x00005555557d4d70, stringArgs=0x00007fffffffdf20, waitForOtherThreads=YES) at assembly.cpp:1671
    frame #35: 0x00007ffff56e8a6b libcoreclr.so`CorHost2::ExecuteAssembly(this=0x000055555578eb40, dwAppDomainId=1, pwzAssemblyPath=u"/console/bin/Release/netcoreapp3.1/console.dll", argc=0, argv=0x0000000000000000, pReturnValue=0x00007fffffffe100) at corhost.cpp:460
    frame #36: 0x00007ffff568822a libcoreclr.so`::coreclr_execute_assembly(hostHandle=0x000055555578eb40, domainId=1, argc=0, argv=0x0000000000000000, managedAssemblyPath="/console/bin/Release/netcoreapp3.1/console.dll", exitCode=0x00007fffffffe100) at unixinterface.cpp:407
    frame #37: 0x00007ffff67dfd8a libhostpolicy.so`___lldb_unnamed_symbol100$$libhostpolicy.so + 810
    frame #38: 0x00007ffff67e022d libhostpolicy.so`___lldb_unnamed_symbol101$$libhostpolicy.so + 45
    frame #39: 0x00007ffff67e095b libhostpolicy.so`corehost_main + 203
    frame #40: 0x00007ffff6a4b73c libhostfxr.so`___lldb_unnamed_symbol204$$libhostfxr.so + 1740
    frame #41: 0x00007ffff6a49ea1 libhostfxr.so`___lldb_unnamed_symbol202$$libhostfxr.so + 641
    frame #42: 0x00007ffff6a444f3 libhostfxr.so`hostfxr_main_startupinfo + 147
    frame #43: 0x00005555555623b7 dotnet`___lldb_unnamed_symbol114$$dotnet + 791
    frame #44: 0x0000555555562b90 dotnet`___lldb_unnamed_symbol115$$dotnet + 128
    frame #45: 0x00007ffff6ca3b97 libc.so.6`__libc_start_main + 231
    frame #46: 0x0000555555557810 dotnet`___lldb_unnamed_symbol9$$dotnet + 41

GcInfoDecoder::EnumerateLiveSlots 是枚举 Slot 的函数,GcInfoDecoder::ReportSlotToGC 是处理各个 Slot 的函数 (包括寄存器与栈),GcInfoDecoder::ReportStackSlotToGC 是处理栈上 (引用类型或 ref 类型) 本地变量的函数。

我们可以在 这个位置 下断点,然后查看解析出的各个 Slot 的信息:

(lldb) b gcinfodecoder.h:679
Breakpoint 8: where = libcoreclr.so`GcInfoDecoder::ReportSlotToGC(GcSlotDecoder&, unsigned int, REGDISPLAY*, bool, unsigned int, void (*)(void*, OBJECTREF*, unsigned int), void*) + 396 at gcinfodecoder.h:679, address = 0x00007ffff5a8836c
(lldb) c
Process 6460 resuming
Process 6460 stopped
* thread #1, name = 'dotnet', stop reason = breakpoint 8.1
    frame #0: 0x00007ffff5a8836c libcoreclr.so`GcInfoDecoder::ReportSlotToGC(this=0x00007fffffffab28, slotDecoder=0x00007fffffffa8c0, slotIndex=0, pRD=0x00007fffffffb5b0, reportScratchSlots=true, inputFlags=0, pCallBack=(libcoreclr.so`GcEnumObject(void*, OBJECTREF*, unsigned int) at gcenv.ee.common.cpp:148), hCallBack=0x00007fffffffc700)(void*, OBJECTREF*, unsigned int), void*) at gcinfodecoder.h:679
   676 	            GcStackSlotBase spBase = pSlot->Slot.Stack.Base;
   677 	            if( reportScratchSlots || !IsScratchStackSlot(spOffset, spBase, pRD) )
   678 	            {
-> 679 	                ReportStackSlotToGC(
   680 	                            spOffset,
   681 	                            spBase,
   682 	                            pSlot->Flags,
(lldb) p *pSlot
(const GcSlotDesc) $12 = {
  Slot = {
    RegisterNumber = 4294967216
    Stack = (SpOffset = -80, Base = GC_FRAMEREG_REL)
  }
  Flags = GC_SLOT_INTERIOR
}

这个 Slot 代表 $rbp-80 ($rbp-0x50) 处有引用类型或 ref 类型的本地变量,在前面的内容中我们已经知道 $rbp-0x50 储存了第二个 span 对象,此外标志 GC_SLOT_INTERIOR 代表本地变量是对象中间的内存地址,而不是对象开头(对象头之后类型信息之前)的内存地址,这个标志会对 GC 标记与重定位对象产生很大的影响,微软官方称这样的变量为 Interior Pointer

继续执行 c 与 p *pSlot 可以看到其他 Slot 的内容:

# $rbp-0x40, 即第一个 span 对象
(const GcSlotDesc) $13 = {
  Slot = {
    RegisterNumber = 4294967232
    Stack = (SpOffset = -64, Base = GC_FRAMEREG_REL)
  }
  Flags = GC_SLOT_INTERIOR
}
# $rbp-0x20, 即本地变量 span
(const GcSlotDesc) $14 = {
  Slot = {
    RegisterNumber = 4294967264
    Stack = (SpOffset = -32, Base = GC_FRAMEREG_REL)
  }
  Flags = GC_SLOT_INTERIOR
}
# $rbp-0x30, 用于初始化数组的句柄
(const GcSlotDesc) $15 = {
  Slot = {
    RegisterNumber = 4294967248
    Stack = (SpOffset = -48, Base = GC_FRAMEREG_REL)
  }
  Flags = GC_SLOT_BASE
}
# $rbp-0x28, 原始数组对象
(const GcSlotDesc) $16 = {
  Slot = {
    RegisterNumber = 4294967256
    Stack = (SpOffset = -40, Base = GC_FRAMEREG_REL)
  }
  Flags = GC_SLOT_BASE
}
# $rbp-0x10, args 参数
(const GcSlotDesc) $17 = {
  Slot = {
    RegisterNumber = 4294967280
    Stack = (SpOffset = -16, Base = GC_FRAMEREG_REL)
  }
  Flags = GC_SLOT_BASE
}

标志 GC_SLOT_BASE 代表是普通的引用类型变量,指向对象的开始地址。

GC 扫描 Span 对象时的处理

接下来我们看看 GC 扫描 Span 对象时会做什么处理,尽管在上述例子中栈上保留了原始数组的地址,使用 Release 模式编译时可能会出现不保留的情况,因此 .NET Core 的运行时支持根据对象中间的地址找到对象的开始地址 (在前几年已经实现了),重新运行程序并使用以下命令可以给标记对象存活的函数下断点:

(lldb) b GCHeap::Promote
Breakpoint 10: 2 locations.

继续执行到达断点以后我们可以从 ppObject 得到标记对象地址的地址,这里的对象地址是第二个 span 对象中保存的开始地址,同时 flags 为 1 即 GC_CALL_INTERIOR 代表地址为对象中间的地址:

(lldb) b GCHeap::Promote
Breakpoint 2: 2 locations.
(lldb) c
Process 6636 resuming
Process 6636 stopped
* thread #1, name = 'dotnet', stop reason = breakpoint 2.1
    frame #0: 0x00007ffff5cb6dc3 libcoreclr.so`WKS::GCHeap::Promote(ppObject=0x00007fffffffd220, sc=0x00007fffffffc9b0, flags=1) at gc.cpp:36669
   36666	{
   36667	    THREAD_NUMBER_FROM_CONTEXT;
   36668	#ifndef MULTIPLE_HEAPS
-> 36669	    const int thread = 0;
   36670	#endif //!MULTIPLE_HEAPS
   36671
   36672	    uint8_t* o = (uint8_t*)*ppObject;
(lldb) p/x *((long*)0x00007fffffffd220)
(long) $0 = 0x00007fff5400ed85

因为地址在对象中间,.NET Core 运行时需要先找到对象的开始地址才能标记对象存活 (标记存活的位是类型信息的最低位),处理的代码如下 (文件):

#ifdef INTERIOR_POINTERS
if (flags & GC_CALL_INTERIOR)
{
    if ((o < hp->gc_low) || (o >= hp->gc_high))
    {
        return;
    }
    if ( (o = hp->find_object (o, hp->gc_low)) == 0)
    {
        return;
    }

}
#endif //INTERIOR_POINTERS

这里会先判断地址是否在托管堆中 (如果是 stackalloc 生成的就不在),然后使用 gc_heap::find_object 来找到对象的开始地址,find_object 会先找到中间地址在 Brick 表对应的 Brick,然后找到该 Brick 对应范围中的第一个托管对象,然后一个个扫描托管对象判断地址属于哪个托管对象,如果找到属于的托管对象则使用该对象的开始地址,这是一个比较昂贵的操作。关于 Brick 表可以参考我之前写的文章

GC 重定位 Span 对象时的处理

接下来我们看看 GC 是怎么重定位 Span 对象的,先退出 LLDB 然后执行以下命令设置环境变量,这个环境变量可以强制每次 GC 的时候都启用压缩:

export COMPlus_gcForceCompact=1

然后再执行 LLDB,给 GCHeap::Relocate 下断点并执行到断点:

(lldb) b GCHeap::Relocate
Breakpoint 2: 2 locations.
(lldb) c
Process 6676 resuming
Process 6676 stopped
* thread #1, name = 'dotnet', stop reason = breakpoint 2.2
    frame #0: 0x00007ffff5cb4633 libcoreclr.so`WKS::GCHeap::Relocate(ppObject=0x00007fffffffd220, sc=0x00007fffffffb810, flags=1) at gc.cpp:36741
   36738	{
   36739	    UNREFERENCED_PARAMETER(sc);
   36740
-> 36741	    uint8_t* object = (uint8_t*)(Object*)(*ppObject);
   36742
   36743	    THREAD_NUMBER_FROM_CONTEXT;
   36744
(lldb) p/x *((long*)0x00007fffffffd220)
(long) $0 = 0x00007fff5400ed85

同样的,ppObject 是标记对象地址的地址,flags 为 1 即 GC_CALL_INTERIOR。具体处理代码如下:

if ((flags & GC_CALL_INTERIOR) && gc_heap::settings.loh_compaction)
{
    if (!((object >= hp->gc_low) && (object < hp->gc_high)))
    {
        return;
    }

    if (gc_heap::loh_object_p (object))
    {
        pheader = hp->find_object (object, 0);
        if (pheader == 0)
        {
            return;
        }

        ptrdiff_t ref_offset = object - pheader;
        hp->relocate_address(&pheader THREAD_NUMBER_ARG);
        *ppObject = (Object*)(pheader + ref_offset);
        return;
    }
}

{
    pheader = object;
    hp->relocate_address(&pheader THREAD_NUMBER_ARG);
    *ppObject = (Object*)pheader;
}

因为压缩阶段已经把对象内容移动了,重定位阶段只需要修改地址到移动后的地址,不管地址是在对象开头还是在对象中间,
对于小对象并不需要检查标记是否带有 GC_CALL_INTERIOR,直接找到对应的 Plug (relocate_address 会再次判断地址是否在托管堆中),
获取 Plug 中保存的偏移值,然后让地址减去该偏移值即可。而大对象则需要使用 find_object 来先定位对象的开始地址,以提升处理效率。

至此我们可以发现,因为 .NET 可以只根据 Span 找到原始对象并实现标记与重定位,所以 Span 原理上是可以保存在堆上的,但这需要牺牲一定性能支持线程安全与放弃 stackalloc (或者分离到另一个类型),所以微软没有选择这么做。

如果您发现该资源为电子书等存在侵权的资源或对该资源描述不正确等,可点击“私信”按钮向作者进行反馈;如作者无回复可进行平台仲裁,我们会在第一时间进行处理!

评价 0 条
风晓L1
粉丝 1 资源 2038 + 关注 私信
最近热门资源
桌面通用(全架构)【在双系统环境下隐藏Windows启动菜单】操作指南  2125
银河麒麟桌面操作系统V10(SP1)2203-如何进行远程桌面互访?  2028
银河麒麟桌面操作系统【保留数据盘重装系统】  1839
麒麟系统各种原因开不了机解决(合集)  1657
桌面通用(全架构)【rpm包转成deb包】操作方法  936
银河麒麟桌面操作系统 V10-SP1 双系统安装 efi 分区问题  921
统信系统安装(合集)  870
统信桌面专业版【手动分区安装UOS系统】介绍  852
统启动异常几种类型(initramfs 模式)  693
Linux系统软件包的导出  27
最近下载排行榜
桌面通用(全架构)【在双系统环境下隐藏Windows启动菜单】操作指南 0
银河麒麟桌面操作系统V10(SP1)2203-如何进行远程桌面互访? 0
银河麒麟桌面操作系统【保留数据盘重装系统】 0
麒麟系统各种原因开不了机解决(合集) 0
桌面通用(全架构)【rpm包转成deb包】操作方法 0
银河麒麟桌面操作系统 V10-SP1 双系统安装 efi 分区问题 0
统信系统安装(合集) 0
统信桌面专业版【手动分区安装UOS系统】介绍 0
统启动异常几种类型(initramfs 模式) 0
Linux系统软件包的导出 0
作者收入月榜
1

prtyaa 收益393.72元

2

zlj141319 收益221.42元

3

1843880570 收益214.2元

4

IT-feng 收益213.03元

5

风晓 收益208.24元

6

777 收益172.82元

7

Fhawking 收益106.6元

8

信创来了 收益105.89元

9

克里斯蒂亚诺诺 收益91.08元

10

技术-小陈 收益79.65元

请使用微信扫码

加入交流群

请使用微信扫一扫!