.Net CLR GC動態獲取函數頭地址,C++的騷操作(慎入)

前言:

太懶了,從沒有在這裏正兒八經的寫過文章。看到一些人的高產,真是慚愧。決定稍微變得不那麼懶。如有疏漏,請指正。
.net的GC都談的很多了,本篇主要是劍走偏鋒,聊聊一些個人認爲較爲核心的細節方面的問題。至於,標記,計劃,壓縮,清掃這些不在討論之列。

動態函數頭地址的一些概念:

一段內存有內存的起始地址(暫叫base),內存的結束地址,以及內存指針當前指向的地址大致的三個概念。而在這段內存裏面分配了函數之後,一個函數在內存裏面必定有一個函數的起始地址也就是指令(第一個push)所在的地址,稱之爲函數頭地址,函數的結束地址也就是指令(ret)所在的地址。在函數裏面做了一些事情,那麼這些可以稱之爲函數中間的某個地址。
通過函數中間的某個地址(不固定的)獲取到函數頭地址(固定的)。稱之爲動態獲取函數頭地址
硬編碼動態獲取到函數頭地址之後,你就可以得到GC信息,方法描述符信息,調試信息,異常信息,回滾信息,幀棧信息等等。

C#代碼:

    static void Main(string[] args)
        {
            GC.Collect();
            Console.ReadLine();
        }

把這段代碼反彙編一下:

7:         static void Main(string[] args)
     8:         {
00007FFB098C5EC0 55                   push        rbp  
00007FFB098C5EC1 57                   push        rdi  
00007FFB098C5EC2 56                   push        rsi  
00007FFB098C5EC3 48 83 EC 30          sub         rsp,30h  
00007FFB098C5EC7 48 8B EC             mov         rbp,rsp  
00007FFB098C5ECA 33 C0                xor         eax,eax  
00007FFB098C5ECC 48 89 45 28          mov         qword ptr [rbp+28h],rax  
00007FFB098C5ED0 48 89 4D 50          mov         qword ptr [rbp+50h],rcx  
00007FFB098C5ED4 83 3D 95 CB 09 00 00 cmp         dword ptr [7FFB09962A70h],0  
00007FFB098C5EDB 74 05                je          ConsoleApp10.Program.Main(System.String[])+022h (07FFB098C5EE2h)  
00007FFB098C5EDD E8 0E 27 CB 5F       call        00007FFB695785F0  
00007FFB098C5EE2 90                   nop  
     9:             GC.Collect();
00007FFB098C5EE3 E8 70 ED FF FF       call        CLRStub[MethodDescPrestub]@7ffb098c4c58 (07FFB098C4C58h)  
00007FFB098C5EE8 90                   nop  
    10:             Console.ReadLine();
00007FFB098C5EE9 E8 42 FF FF FF       call        CLRStub[MethodDescPrestub]@7ffb098c5e30 (07FFB098C5E30h)  
00007FFB098C5EEE 48 89 45 28          mov         qword ptr [rbp+28h],rax  
00007FFB098C5EF2 90                   nop  
    11:         }
00007FFB098C5EF3 90                   nop  
00007FFB098C5EF4 48 8D 65 30          lea         rsp,[rbp+30h]  
00007FFB098C5EF8 5E                   pop         rsi  
00007FFB098C5EF9 5F                   pop         rdi  
00007FFB098C5EFA 5D                   pop         rbp  
00007FFB098C5EFB C3                   ret  

我們看到地址:00007FFB098C5EC0就是函數頭的地址。00007FFB098C5EFB則是函數結束地址。中間的比如調用GC.Collection的地址00007FFB098C5EE3和調用Console.ReadLine的地址00007FFB098C5EE9,則可以稱之爲中間地址。

如何通過中間的某個地址(可能是00007FFB098C5EE3,也可能是00007FFB098C5EE9,還有可能是中間其它地址)動態的找到函數頭的固定地址呢?

計算公式一:奇偶數的偏移(value-1)

我們先來看下函數頭地址:00007FFB098C5EC0,在內存裏面的存儲數值。

CLR的操作是:
value-1 =(00007FFB098C5EC0 - base) & 31 >>2+1
base:是函數所在內存的其實地址
value-1:是計算的結果

這個value-1的結果要麼是1,要麼是5,爲啥?仔細分析下。一般的來說,base也就是函數所在的內存的其實地址末尾兩字節一般都是 00 00。也就是說base - 00007FFB098C5EC0的結果一定四0xnnnnnnnnnnnn5EC0。n表示未知數。因爲上面的公式&31,所以只需要關注最後兩個字節就可以了。

回到上面爲啥value-1等於1或者5呢?不能等於其它。5EC0中C0的二進制是:
1100 0000。把它&31,結果是0。0>>2還是0。然後加上1,結果也就是value-1等於1.

那麼5是怎麼來的呢?我們注意看,0xC是能被2整除的偶數。如果是不能被2整除的奇數,比如0xD的話,低位的向左第五位必定位1,其它第四位無論是什麼,右移2之後一定是4,然後 4+1 等於5。

所以低位向左第五位如果是偶數,則value-1爲1,如果是奇數則value-1爲5。不能有其它,此處大家可以自行驗證。

關於計算公式參考:https://github.com/dotnet/runtime/blob/main/src/coreclr/vm/codeman.cpp

計算公式二:0的個數的32位索引

標題頭的意思是:以0的個數表示有幾個32

還是按照上面來,此處函數頭的其實地址是:00007FFB098C5EC0。這裏的計算公式略有不同:

value-2 = 28 - (00007FFB098C5EC0 - base) >> 5 & 7 << 2

同樣:
base:表示函數所在內存的起始地址
value-2 則是此公式計算的結果

因爲此公式右移的是5,而且base最後兩位一般爲0。所以只需要看最後一字節也就是C0即可。

1100 0000 右移5位,結果爲0110,也就是6。6&7等於6,6左移2,結果爲0x18。十進制的24。然後28-24 ==4。value-2的結果爲4。

公式一計算得出的value-1的值爲1。因爲C0的C是偶數。所以爲1。
公式二計算得出的value-2的值爲4。

value = value-1 << value-2
value就是最終函數頭地址:00007FFB098C5EC0在內存裏面存儲的形式,二進制表示就是:0001 0000。十進制的:16 。十六進制的:0x10 。

關於計算公式參考:https://github.com/dotnet/runtime/blob/main/src/coreclr/vm/codeman.cpp

中間地址計算動態找出函數頭:

此處中間地址取GC.Collection的地址:00007FFB098C5EE3。

startPos = (00007FFB098C5EE3 - base) >> 5,此處取GC.Collection地址的最後兩位5EE3 >> 5。結果爲:startPos = 0x2F7。

首先從內存裏面取出公式二里面計算的value值:0x10。然後套用公式二的value-2的計算:

Result = 28 -(00007FFB098C5EE3 - base) >> 5 & 7 << 2
很明顯Result的結果爲 0
把tmp = value >> Result 。
結果tmp == 0x10。

 if (tmp)
    {
        startPos--;
        while (!(tmp & NIBBLE_MASK))
        {
            tmp = tmp >> NIBBLE_SIZE;
            startPos--;
        }
        return base + POSOFF2ADDR(startPos, tmp & NIBBLE_MASK);
    }

NIBBLE_MASK:0xf
POSOFF2ADDR: startPos << 5 + (tmp -1 ) << 2

因爲tmp爲0x10,所以startPos--。 2f7-1 == 2f6 。然後因爲 !(tmp & NIBBLE_MASK) 所以 tmp = tmp >> NIBBLE_SIZE; 也就是 tmp == 1。

那麼結果就是 base + 2f6 << 5 + (1 -1) << 2
用n表示未知數 0xnnnnnnnnnnnn5EC0。剛好是函數頭的地址。

此方法適用於任何一箇中間地址動態獲取函數頭地址。

過程

我們在C#源代碼中調用GC.Collection會運行以下幾個步驟:
1.GC.Collection()
2.GCScanRoot()
3.EECodeInfo.Init(寄存器Rip)
4.FindMethodCode(寄存器Rip)
5.通過FindMethodCode找到函數頭地址,然後通過函數頭的地址-8。得到的就是EHinfo,DebugInfo,GCinfo,MethodDesc,UwndInfo信息
6.通過GCinfo找到根對象
7.通過根對象遍歷所有對象
8.在這些對象中找到非存活對象,然後進行回收

這個過程過於複雜,省略了很多與本節主題無關的東西。我們看到FindMethodCode就是獲取到函數頭的地址的函數。

公式一和二的參考如下:

公式一:

void EEJitManager::NibbleMapSetUnlocked(HeapList * pHp, TADDR pCode, BOOL bSet)
{
    CONTRACTL {
        NOTHROW;
        GC_NOTRIGGER;
    } CONTRACTL_END;

    // Currently all callers to this method ensure EEJitManager::m_CodeHeapCritSec
    // is held.
    _ASSERTE(m_CodeHeapCritSec.OwnedByCurrentThread());

    _ASSERTE(pCode >= pHp->mapBase);

    size_t delta = pCode - pHp->mapBase;

    size_t pos  = ADDR2POS(delta);
    DWORD value = bSet?ADDR2OFFS(delta):0;

    DWORD index = (DWORD) (pos >> LOG2_NIBBLES_PER_DWORD);
    DWORD mask  = ~((DWORD) HIGHEST_NIBBLE_MASK >> ((pos & NIBBLES_PER_DWORD_MASK) << LOG2_NIBBLE_SIZE));

    value = value << POS2SHIFTCOUNT(pos);

    PTR_DWORD pMap = pHp->pHdrMap;

    // assert that we don't overwrite an existing offset
    // (it's a reset or it is empty)
    _ASSERTE(!value || !((*(pMap+index))& ~mask));

    // It is important for this update to be atomic. Synchronization would be required with FindMethodCode otherwise.
    *(pMap+index) = ((*(pMap+index))&mask)|value;
}

公式二:

TADDR EEJitManager::FindMethodCode(RangeSection * pRangeSection, PCODE currentPC)
{
    LIMITED_METHOD_DAC_CONTRACT;

    _ASSERTE(pRangeSection != NULL);

    HeapList *pHp = dac_cast<PTR_HeapList>(pRangeSection->pHeapListOrZapModule);

    if ((currentPC < pHp->startAddress) ||
        (currentPC > pHp->endAddress))
    {
        return NULL;
    }

    TADDR base = pHp->mapBase;
    TADDR delta = currentPC - base;
    PTR_DWORD pMap = pHp->pHdrMap;
    PTR_DWORD pMapStart = pMap;

    DWORD tmp;

    size_t startPos = ADDR2POS(delta);  // align to 32byte buckets
                                        // ( == index into the array of nibbles)
    DWORD  offset   = ADDR2OFFS(delta); // this is the offset inside the bucket + 1

    _ASSERTE(offset == (offset & NIBBLE_MASK));

    pMap += (startPos >> LOG2_NIBBLES_PER_DWORD); // points to the proper DWORD of the map

    // get DWORD and shift down our nibble

    PREFIX_ASSUME(pMap != NULL);
    tmp = VolatileLoadWithoutBarrier<DWORD>(pMap) >> POS2SHIFTCOUNT(startPos);

    if ((tmp & NIBBLE_MASK) && ((tmp & NIBBLE_MASK) <= offset) )
    {
        return base + POSOFF2ADDR(startPos, tmp & NIBBLE_MASK);
    }

    // Is there a header in the remainder of the DWORD ?
    tmp = tmp >> NIBBLE_SIZE;

    if (tmp)
    {
        startPos--;
        while (!(tmp & NIBBLE_MASK))
        {
            tmp = tmp >> NIBBLE_SIZE;
            startPos--;
        }
        return base + POSOFF2ADDR(startPos, tmp & NIBBLE_MASK);
    }
}

你也可以直接參考:
https://github.com/dotnet/runtime/blob/main/src/coreclr/vm/codeman.cpp


微信公衆號:jianghupt. QQ羣:676817308
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章