前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >.NET9 GC标记原理(超核技术)

.NET9 GC标记原理(超核技术)

作者头像
江湖评谈
发布2024-04-25 18:19:09
620
发布2024-04-25 18:19:09
举报
文章被收录于专栏:天下风云天下风云

前言

GC是CLR里面一个重要的模块,跟上一篇:NET9异常(CLR)原理(顶阶技术)里面介绍的异常模块一样,属于CLR里的顶阶技术。它管控托管堆的分配,销毁。.NET9 GC同样有所改进,本篇看下GC标记的原理。

例子

简单的例子:

代码语言:javascript
复制
 public class Program
 {
     static void Main(string[] args)
     {
         try
         {
             throw new Exception();
         }
         catch(Exception ex) 
         {
             Console.WriteLine(ex.Message);
         }
         GC.Collect();
         Console.ReadLine();
     }
 }

try里面new了一个Exception,这种分配的堆必然会产生一个对象地址值,此值会被放到rbp或者rsp所在的偏移地址。后面有一个GC.Collect()进行垃圾回收,标记阶段会标记此处栈空间的实例对象,用以观察此处new的实例。ASM如下:

代码语言:javascript
复制
00007FFF1335F740 55                   push        rbp  
//此处省略,便于观看
00007FFF1335F771 48 B9 38 88 2B 13 FF 7F 00 00 mov         rcx,7FFF132B8838h  
00007FFF1335F77B E8 30 B4 D5 5E       call        JIT_TrialAllocSFastMP_InlineGetThread (07FFF720BABB0h)  
00007FFF1335F780 48 89 45 F0          mov         qword ptr [rbp-10h],rax  
00007FFF1335F784 48 8B 4D F0          mov         rcx,qword ptr [rbp-10h]  
00007FFF1335F788 FF 15 82 80 F3 FF    call        qword ptr [7FFF13297810h]  
00007FFF1335F78E 48 8B 4D F0          mov         rcx,qword ptr [rbp-10h]  
00007FFF1335F792 E8 F9 A3 8E 5E       call        IL_Throw (07FFF71C49B90h)  
00007FFF1335F797 CC                   int         3  
00007FFF1335F798 FF 15 72 68 51 00    call        qword ptr [7FFF13876010h]  
00007FFF1335F79E 90                   nop  
00007FFF1335F79F FF 15 63 76 93 00    call        qword ptr [7FFF13C96E08h]  
00007FFF1335F7A5 48 89 45 D8          mov         qword ptr [rbp-28h],rax  
00007FFF1335F7A9 90                   nop  
00007FFF1335F7AA 90                   nop  
00007FFF1335F7AB 48 83 C4 50          add         rsp,50h  
00007FFF1335F7AF 5D                   pop         rbp  
00007FFF1335F7B0 C3                   ret

地址00007FFF1335F77B处对new Exception进行了快速路径实例分配,结果存储到[rbp-10h]的内存处。而这里的-10h等于16进制的0xFFFFFFF0。GC标记阶段,会从GCInfo里面取到[rbp-10h]这个地址处的实例对象进行标记。而在此之前,首先要做的就是对0xFFFFFFF0进行加密形式的内存存储。

JIT Compile

JIT在对C#代码进行编译的时候,会进行空间的分配。比如rbp-10h这个-10h空间。这个空间怎么来的呢?

JIT有一个编译类Compiler,此类里面包含了lvaTable变量,它是一个LclVarDsc结构体。LclVarDsc里面包含了lvStkOffs私有字段,这个字段即是分配这个空间的。

代码语言:javascript
复制
class LclVarDsc 
{
  private int:
      lvStkOffs;
}
class Compiler
{
   LclVarDsc varDesc;
}

可以通过以下函数定位lvStkOffs,看它何时被赋值,如何赋值

代码语言:javascript
复制
void SetStackOffset(int offset)
{
    lvStkOffs = offset;
}
//小技巧提示,一般来说CLR里面的给某某字段赋值都是setXXX这种形式
//如果不是这种形式,可以通过值更改的断点方式查找某个字段何时被赋值,如何赋值

最终lvStkOffs被赋值到了spOffset字段,GetStackSlotId函数参数spOffset即是通过lvStkOffs得来的

代码语言:javascript
复制
GcSlotId GcInfoEncoder::GetStackSlotId( INT32 spOffset, GcSlotFlags flags, GcStackSlotBase spBase )
{
#ifdef _DEBUG
    _ASSERTE( !m_IsSlotTableFrozen );
#endif
    if( m_NumSlots == m_SlotTableSize )
    {
        GrowSlotTable();
    }
    _ASSERTE( m_NumSlots < m_SlotTableSize );
    _ASSERTE(GC_SP_REL != spBase || spOffset >= 0);
    _ASSERTE( (flags & (GC_SLOT_IS_REGISTER | GC_SLOT_IS_DELETED)) == 0 );
    if (!(TargetOS::IsApplePlatform && TargetArchitecture::IsArm64))
    {
        _ASSERTE((spOffset % TARGET_POINTER_SIZE) == 0);
    }
    m_SlotTable[ m_NumSlots ].Slot.Stack.SpOffset = spOffset;
    m_SlotTable[ m_NumSlots ].Slot.Stack.Base = spBase;
    m_SlotTable[ m_NumSlots ].Flags = flags;
    GcSlotId newSlotId;
    newSlotId = m_NumSlots++;
    return newSlotId;
}

下一步就是把这个空间的偏移量(m_SlotTable[ m_NumSlots ].Slot.Stack.SpOffset)即是-10h也即是:0xFFFFFFF0进行加密。

GC加密

上面的偏移0xFFFFFFF0是如何加密到GCInfo的呢?先来看一段代码:

代码语言:javascript
复制
//gcinfoencoder.cpp:1700
currentNormStackSlot = NORMALIZE_STACK_SLOT(pSlotDesc->Slot.Stack.SpOffset)
//小技巧提示:以上字段SpOffset因为在gcinfoencoder.cpp里面有多个,可以把所有包含SpOffset都打上条件断点(注意是条件断点)进行观察。
//而不是只打某一个或者几个(需要全部),这样可能观察不到。

pSlotDesc->Slot.Stack.SpOffset即是0xFFFFFFF0,NORMALIZE_STACK_SLOT是把SpOffset右移3位结果是:0x1ffffffe,但CLR里面的这个结果是:0xfffffffe,注意看最高位是1和f的区别,什么原因,目前没有发现,此处记录下。但不影响GCInfo的结果。

此后会通过以下代码把0xfffffffe写入到GCInfo

代码语言:javascript
复制
//gcinfoencoder.cpp:1707
GCINFO_WRITE_VARL_S(m_Info1, currentNormStackSlot, STACK_SLOT_ENCBASE, UntrackedSlotSize);

注意参数里面的宏STACK_SLOT_ENCBASE值为6,后面需要用到这个为6的值。GCINFO_WRITE_VARL_S宏调用了函数EncodeVarLengthSigned如下:

代码语言:javascript
复制
int BitStreamWriter::EncodeVarLengthSigned( SSIZE_T n, UINT32 base )
{
    _ASSERTE((base > 0) && (base < BITS_PER_SIZE_T));
    size_t numEncodings = size_t{ 1 } << base;
    for(int bitsUsed = base+1; ; bitsUsed += base+1)
    {
        size_t currentChunk = ((size_t) n) & (numEncodings-1);
        size_t topmostBit = currentChunk & (numEncodings >> 1);
        n >>= base; // signed arithmetic shift
        if((topmostBit && (n == (SSIZE_T)-1)) || (!topmostBit && (n == 0)))
        {
            // The topmost bit correctly represents the sign
            Write( currentChunk, base+1 ); // This sets the extension bit to zero
            return bitsUsed;
        }
        else
        {
            Write( currentChunk | numEncodings, base+1 );
        }
    }
}

经过上面的各种位计算,写入的结果是Write( currentChunk, base+1 )这一行代码,写入的值即是currentChunk==0x000000000000003e,写入的bit位数base(6)+1==7。也就是说,0xfffffffe会加密成了0x000000000000003e,占据7个bit位,写入到GCInfo。

但此处加密还没完,Write函数又进行了位计算

代码语言:javascript
复制
void BitStreamWriter::Write( size_t data, UINT32 count )
{
     //为了便于观察,此处省略部分代码
     WriteInCurrentSlot( data, count );
     m_FreeBitsInCurrentSlot -= count;
}

Write函数里面调用了WriteInCurrentSlot,它这里面又进行了位计算。

代码语言:javascript
复制
inline void WriteInCurrentSlot( size_t data, UINT32 count )
{
    data &= ((size_t)-1 >> (BITS_PER_SIZE_T - count));
    data <<= (BITS_PER_SIZE_T - m_FreeBitsInCurrentSlot);
    *m_pCurrentSlot |= data;
}

m_FreeBitsInCurrentSlot代表的是当前的64个bit位里面还剩余多少个bit位。传入的参数data值是0x000000000000003e,经过位计算,最终的代码: *m_pCurrentSlot |= data;此时的data为0x00f8000000000000。也就是说最终会把0x00f8000000000000这个数值写入到m_pCurrentSlot地址所指向的值里面相应的bit位上。

那么GC标记的时候,就会通过解码的形式把0x00f8000000000000这个数值给它解码成0xFFFFFFF0,以获取到new Exception实例的地址。

GC解码

上面JIT对new Exception对象地址的地址进行了GC加密,在调用例子里面的GC.Collect的时候,会对GC进行解密。比如本例通过解码的形式把0x00f8000000000000这个数值给它解码成0xFFFFFFF0,以获取到new Exception实例的地址,看下此过程。

看下解码的代码,这里的slotIndex==6。也就是上面所说的宏STACK_SLOT_ENCBASE值为6。实际上是通过STACK_SLOT_ENCBASE占据的bit位数取到值。从GCInfo bit位获取。ReportStackSlotToGC函数:

代码语言:javascript
复制
const GcSlotDesc* pSlot = slotDecoder.GetSlotDesc(slotIndex);

GetSlotDesc函数如下:

代码语言:javascript
复制
const GcSlotDesc* GcSlotDecoder::GetSlotDesc(UINT32 slotIndex)
{
   //为了便于观察,这个函数里面省略了很多。它其实是一个循环。
   INT32 normSpOffsetDelta = (INT32) m_SlotReader.DecodeVarLengthUnsigned(STACK_SLOT_DELTA_ENCBASE);
   INT32 normSpOffset = normSpOffsetDelta + NORMALIZE_STACK_SLOT(m_pLastSlot->Slot.Stack.SpOffset);
   m_pLastSlot->Slot.Stack.SpOffset = DENORMALIZE_STACK_SLOT(normSpOffset);
   return m_pLastSlot;
}

pSlot->Slot.Stack.SpOffset里面包含的就是-10h也即是0xFFFFFFF0,为寄存器rbp的偏移量。

Register获取

因为new Exception对象的地址在[rbp-10h]处,上面的GC解码获取到了-10h偏移但是没有获取到rbp寄存器的值,所以这里依然需要获取它。

代码:

代码语言:javascript
复制
ULONGLONG **ppRax = &pRD->pCurrentContextPointers->Rax;
return (OBJECTREF*)*(ppRax + regNum);

pRD在GC的时候进行栈帧的遍历获取的

代码语言:javascript
复制
StackWalkAction Thread::StackWalkFramesEx(
                    PREGDISPLAY pRD,        // virtual register set at crawl start
                    PSTACKWALKFRAMESCALLBACK pCallback,
                    VOID *pData,
                    unsigned flags,
                    PTR_Frame pStartFrame
                )
{
        StackFrameIterator iter;
        if (iter.Init(this, pStartFrame, pRD, flags) == TRUE)
        {
            while (iter.IsValid())
            {
                retVal = MakeStackwalkerCallback(&iter.m_crawl, pCallback, pData DEBUG_ARG(iter.m_uFramesProcessed));
                if (retVal == SWA_ABORT)
                {
                    break;
                }

                retVal = iter.Next();
                if (retVal == SWA_FAILED)
                {
                    break;
                }
            }
        }

        SET_THREAD_TYPE_STACKWALKER(pStackWalkThreadOrig);
    }
    return retVal;
}

函数MakeStackwalkerCallback的参数&iter.m_crawl是CrawlFrame类型,CrawlFrame里面包含了pRD。pRD->pCurrentContextPointers->Rax意即当前的栈帧的排列Rax寄存器相对于pCurrentContextPointers的偏移地址。+5取值(*(ppRax + regNum),regNum==5)转换成OBJECTREF*。对这个结果进行取值+spOffset,最终的结果就是new Exception对象的地址

代码语言:javascript
复制
pObjRef = (OBJECTREF*)(*pFrameReg + spOffset)

也即是说rbp寄存器的值,根据当前栈帧的Rax偏移地址进行取值。这个值加上上面解码的偏移值0xFFFFFFF0,最终就找到了new Exception对象的地址。然后对它进行一个标记。

GC标记

取到了最终的new Exception对象地址之后,就可以获取它的MethodTable,

代码语言:javascript
复制
MethodTable *RawGetMethodTable() const
{
  return m_pMethTab;
}

然后把m_pMethTab的最后一位赋值为1,即为标记

代码语言:javascript
复制
#define GC_MARKED (size_t)0x1
void SetMarked()
{
    _ASSERTE(RawGetMethodTable());
    RawSetMethodTable((MethodTable *) (((size_t) RawGetMethodTable()) | GC_MARKED));
}

结尾

上面的东西较多,这里依然需要梳理下。JIT进行编译的时候,会计算出实例化对象地址的偏移量,比如-10h。然后把这个偏移量进行加密存入到GCInfo。在GC垃圾回收的时候,从GCInfo里面取出偏移量的加密位进行解码。然后找到rbp寄存器所在的地址,两者进行相加,结果就是new Excepiton对象的地址。此后对对象进行一个标记。一般的观察通过ReportStackSlotToGC函数进行倒推即可。

如下流程:

  1. JIT对对象的偏移进行加密存入到GCInfo
  2. GC垃圾回收对GCInfo进行解码取出对象偏移地址
  3. GC垃圾回收通过当前栈帧的rax获取到rbp所在的地址
  4. 通过rbp地址+偏移量计算出new Exception对象地址
  5. 对这个对象的MethodTable最后一位进行标记(即赋值为1)
本文参与?腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2024-04-24,如有侵权请联系?cloudcommunity@tencent.com 删除

本文分享自 江湖评谈 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体分享计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 例子
  • JIT Compile
  • 最终lvStkOffs被赋值到了spOffset字段,GetStackSlotId函数参数spOffset即是通过lvStkOffs得来的
  • 下一步就是把这个空间的偏移量(m_SlotTable[ m_NumSlots ].Slot.Stack.SpOffset)即是-10h也即是:0xFFFFFFF0进行加密。
  • GC加密
  • GC解码
  • Register获取
  • GC标记
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
http://www.vxiaotou.com