當前位置: 妍妍網 > 碼農

偵錯實戰 | 記一次有教益的 vs2022 記憶體分配失敗崩潰分析(續)

2024-06-22碼農

前言

前一陣子遇到了 vs2022 卡死的問題,在 中重點分析了崩潰的原因 —— 當 vs2022 嘗試分配 923MB 的記憶體時,實體記憶體+頁檔大小不足以滿足這次分配請求,於是丟擲異常。

本篇文章將重點挖掘一下 vs2022 在崩潰之前已經分配的內容。

說明: 本文很早就寫了草稿,一直沒時間整理釋出,Finally~

還是先從呼叫棧入手,找到關鍵參數,然後檢視參數內容。

尋找 vector 物件地址

棧幀 0b 對應的函式是 std::vector<T>::_Emplace_reallocate() ,棧幀 0c 會呼叫這個函式。根據呼叫約定可知,呼叫類成員函式時, rcx 會指向類物件,在這裏 rcx 會指向 std::vector<std::shared_ptr<std::stringstream>> 型別的例項。可以透過檢視棧幀 0c 的反組譯程式碼確定 rcx 的來源。

view-vector-instance-from-stackframe-0c

從圖中可知, rcx 的值來自 rbp-0x70 。那 rbp 的值是多少呢?使用 uf 檢視 vcpkg!code_store::a_store::a_thread_impl::append_code_item_name() 函式的反組譯程式碼。

view-rbp-in-frame-0c

由上圖可知,先把 rsp-0x920 賦值給 rbp ,然後 rsp 會減小 0xa20 。所以可以透過 rsp+0xa20-0x920 計算出對應的 rbp 的值,再減去 0x70 即可得到 rcx 的值。由此可知 vector 物件的地址是 0x000000b1 6547e5d0

view-rbp-rcx-in-frame-0c

檢視 vector 內容

查閱 vs 提供的 STL 源碼可知, vector 物件起始偏移 0 的位置儲存了第一個元素的地址,起始偏移 8 64 位程式)的位置儲存了最後一個元素後面的地址。可以檢視 vector 中前 20 個元素。

view-vector-front-20-data

由呼叫棧可知, vector 中的元素型別是 shared_ptr<stringstream> 。根據源碼可知, shared_ptr<T> 型別的大小是 16 字節,偏移 0 的位置儲存了物件的地址,偏移 8 的位置儲存了參照計數物件的地址。

template < class _Ty>
classshared_ptr :
public _Ptr_base<_Ty> { // class for reference counted resource management
...
};
template < class _Ty>
class _Ptr_base {
// base class for shared_ptr and weak_ptr
private:
element_type* _Ptr;
_Ref_count_base* _Rep;
...
};

vector 中有多少個元素

大家應該都知道, vector 中的元素是順序儲存的,知道了起始地址及結束地址,也知道每個元素的大小,可以很容易計算出 vector 中的元質數量。

windbg 中輸入 ? (000001c2434b7170-000001c21ccdd060) / 0n16 可以得到元素個數 40360465

根據上次分析的結果可知,分配的元質數量是 60540697 。透過檢視 vs 提供的源碼可知,容器擴容時會按 1.5 倍進行擴容。

來驗證以一下是否符合這個規律。在 windbg 中輸入 ? 0n40360465 + 0n40360465 / 2 可以得到結果 60540697

view-vector-size

可見,當時 vs 在呼叫類似 push_back() 之類的方法向容器中增加元素,但是容器正好滿了,觸發了擴容操作。由此也可以驗證之前的分析是正確的。

驗證參照計數物件數據

拿第一個元素進行驗證,實際物件的地址是 000001be 580056f0 ,參照計數物件的地址是 000001be 580056e0 。先驗證參照計數物件是否正確。

_Ref_count_base 結構如下圖所示:

view-_Ref_count_base_detail

說明: devenv 載入的模組所對應的偵錯符號已經去除了 Type 資訊,沒辦法透過 dt 顯示型別資訊。上圖是我用 windbg 偵錯新建的測試工程時的截圖。

從下圖可知,參照計數相關數據是完美匹配的。

verify-reference-count-object

一般 shared_ptr<T> 的參照計數和實際的數據是沒有關系的,比如下面的程式碼:

int* p = newint();
std::shared_ptr<intsp(p);

view_normal_shared_ptr

sp._Ptr 的值是 0x017b9450 sp._Rep 的地址是 0x017b9640 ,兩者之間沒有明顯關系。

但是,如果你觀察的比較仔細,可以發現一個非常有趣的現象 —— vector 中的每個元素(智慧指標)的參照計數物件的地址 +0x10 正好等於實際物件的地址。

以第一個元素為例,參照計數物件的地址是 000001be 580056e0 ,實際物件的地址是 000001be 580056f0 ,兩者正好相差了 0x10

這是怎麽回事呢?如果你對 stl 比較熟悉,可能已經想到了 std::make_shared() vector 中儲存的物件都是透過 std::make_shared() 建立出來的。

make_shared

我摘錄了 vs 中提供的源碼

template < class _Ty class... _Types>
shared_ptr<_Ty> make_shared(_Types&&... _Args) {
// make a shared_ptr to non-array object
constauto _Rx = new _Ref_count_obj2<_Ty>(_STD forward<_Types>(_Args)...);
shared_ptr<_Ty> _Ret;
_Ret._Set_ptr_rep_and_enable_shared(_STD addressof(_Rx->_Storage._Value), _Rx);
return _Ret;
}

註意程式碼中 _Ret._Set_ptr_rep_and_enable_shared() 第一個參數的值是 _Rx->_Storage._Value 的地址。

_Rx 的型別是 _Ref_count_obj2<_Ty>* _Ref_count_obj2 繼承自 _Ref_count_base 。而 _Ref_count_base 的大小是 16 字節:虛表指標 8 字節,兩個參照計數各占 4 字節,一共 16 字節。

大概的記憶體結構圖如下:

make_shared_relation

務必註意 _Ref_count_obj2 中的 _Storage 儲存了整個目標物件,而不是指標。

總結

  • procdump 真是事後偵錯的好幫手。以管理員許可權執行 procdump -i -ma d:\dumps\ 即可安裝。 -i 表示安裝(如果要解除安裝,可以使用 -u 參數)。 -ma 表示執行完整轉儲, d:\dumps\ 表示 .dmp 檔保存的位置。

  • 相較於 32 位行程的 4GB 2 32 次方)虛擬記憶體空間而言, 64 位行程的虛擬記憶體空間超級大,目前是 256TB (總共 64 位,目前只用了 48 位),內核態和使用者態平均分,使用者態可以使用一半,也就是 128TB

  • 如果使用 malloc() 或者 new() (內部會呼叫 malloc() )分配的記憶體大小超出堆閾值,那麽內部會使用 NtAllocateVirtualMemory() 分配記憶體,而且 AllocationType 的值是 MEM_COMMIT 。分配 MEM_COMMIT 型別的記憶體是受 實體記憶體+分頁檔大小 限制的。

  • 參考資料

  • vs 源碼

  • NTSTATUS Values