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 对象时会做什么处理,尽管在上述例子中栈上保留了原始数组的地址,使用 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 对象的,先退出 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 (或者分离到另一个类型),所以微软没有选择这么做。
如果您发现该资源为电子书等存在侵权的资源或对该资源描述不正确等,可点击“私信”按钮向作者进行反馈;如作者无回复可进行平台仲裁,我们会在第一时间进行处理!
加入交流群
请使用微信扫一扫!