1. 問題表現
經常出現程式崩潰,崩潰堆疊較為底層
原因基本上都是 read write memory 時觸發了異常,盤查後初步懷疑是記憶體寫壞了。
2. 排查期
UE 支援各種記憶體分配器:
- TBB
- Ansi
- Jemalloc
- Stomp
還有自帶的記憶體分配器: - Binned
- Binned2
- Binned3
可以參考文章 UE 中的記憶體分配器。
其中 Stomp 是引擎提供的排查記憶體寫壞的工具之一,透過增加引數-stompmalloc
可以讓 UE 預設採用該記憶體分配器,啟用了之後崩潰的第一現場就是記憶體寫壞的程式碼地址。
透過排查發現崩潰原因是遍歷迭代器時刪除元素後沒有及時 continue,大致示例如下:
for(TArray<Actor*>::TIterator Iter(Actors); Iter; ++ Iter)
{
if (xxx)
{
Iter.RemoveCurrent(); //沒有 continue
}
if (xxx)
{
Iter.RemoveCurrent();//沒有 continue
}
}
當陣列元素只剩一個時,如果觸發了兩次 RemoveCurrent,就會導致寫到陣列之外的記憶體空間,RemoveCurrent 的機制會把後面的陣列元素遷移到刪除的位置上,保證資料連貫。同時 RemoveCurrent 完畢後會自動把迭代器的下標前移一位。
3. Stomp 原理
3.1 記憶體覆蓋
Stomp 其主要的功能是在寫壞記憶體時可以馬上捕獲到第一現場。記憶體寫壞了通常指程式在操作記憶體時寫入了非法的資料或超出了記憶體分配的範圍,導致程式出現錯誤或崩潰。這種情況通常被稱為越界訪問或非法訪問記憶體。
大部分情況下有記憶體池的技術,且作業系統分配記憶體往往會向上按頁對其分配,所以一時的記憶體越界讀寫有可能不會馬上出現問題。而 Stomp 是在記憶體越界時就對其丟擲異常。
3.2 實現原理
開啟 Stomp 之後,記憶體分配基本上由 FMallocStomp::Malloc
和 FMallocStomp::Free
接管。
void* FMallocStomp::Malloc(SIZE_T Size, uint32 Alignment)
void FMallocStomp::Free(void* InPtr)
要做到寫壞記憶體後能直接觸發異常,需要在記憶體分配上做手腳,這裡主要用到了兩點:
- 作業系統支援的 Pagefault 和 Page 許可權控制
- 哨兵機制
Stomp 在給使用者分配記憶體的時候會額外分配 2 個 Page 出來,分別在返回給使用者的指標地址空間前後。當使用者超出分配給他的記憶體上讀寫時,就會觸發異常。其分配記憶體的流程大致如下:
這裡有個問題是 FAllocationData 只有 32 個位元組,但是其分配了一個 Page 給其使用,這裡主要是由於分配記憶體都需要對齊 Page。到此記憶體分配完畢,接下來有 2 種情況:
- 從 Page2 寫資料一直寫到 Page3,由於 Page3 被標記為不可讀不可寫,因此一旦出現越界,就會直接丟擲異常
- 從 Page1 寫資料一直寫到 Page0,由於 Page0 末端分配了一個 FAllocationData,因此一旦越界,哨兵值會被覆蓋,當釋放記憶體時
FMallocStomp::Free
就會對記憶體塊的 FAllocationData 進行檢查,一旦哨兵比對異常就丟擲異常