前言
前一陣子遇到了
vs2022
卡死的問題,在
中重點分析了崩潰的原因 —— 當
vs2022
嘗試分配
923MB
的記憶體時,實體記憶體+頁檔大小不足以滿足這次分配請求,於是丟擲異常。
本篇文章將重點挖掘一下
vs2022
在崩潰之前已經分配的內容。
說明: 本文很早就寫了草稿,一直沒時間整理釋出,Finally~
還是先從呼叫棧入手,找到關鍵參數,然後檢視參數內容。
尋找 vector 物件地址
棧幀
0b
對應的函式是
std::vector<T>::_Emplace_reallocate()
,棧幀
0c
會呼叫這個函式。根據呼叫約定可知,呼叫類成員函式時,
rcx
會指向類物件,在這裏
rcx
會指向
std::vector<std::shared_ptr<std::stringstream>>
型別的例項。可以透過檢視棧幀
0c
的反組譯程式碼確定
rcx
的來源。
從圖中可知,
rcx
的值來自
rbp-0x70
。那
rbp
的值是多少呢?使用
uf
檢視
vcpkg!code_store::a_store::a_thread_impl::append_code_item_name()
函式的反組譯程式碼。
由上圖可知,先把
rsp-0x920
賦值給
rbp
,然後
rsp
會減小
0xa20
。所以可以透過
rsp+0xa20-0x920
計算出對應的
rbp
的值,再減去
0x70
即可得到
rcx
的值。由此可知
vector
物件的地址是
0x000000b1 6547e5d0
。
檢視 vector 內容
查閱
vs
提供的
STL
源碼可知,
vector
物件起始偏移
0
的位置儲存了第一個元素的地址,起始偏移
8
(
64
位程式)的位置儲存了最後一個元素後面的地址。可以檢視
vector
中前
20
個元素。
由呼叫棧可知,
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
。
可見,當時
vs
在呼叫類似
push_back()
之類的方法向容器中增加元素,但是容器正好滿了,觸發了擴容操作。由此也可以驗證之前的分析是正確的。
驗證參照計數物件數據
拿第一個元素進行驗證,實際物件的地址是
000001be 580056f0
,參照計數物件的地址是
000001be 580056e0
。先驗證參照計數物件是否正確。
_Ref_count_base
結構如下圖所示:
說明:
devenv
載入的模組所對應的偵錯符號已經去除了
Type
資訊,沒辦法透過
dt
顯示型別資訊。上圖是我用
windbg
偵錯新建的測試工程時的截圖。
從下圖可知,參照計數相關數據是完美匹配的。
一般
shared_ptr<T>
的參照計數和實際的數據是沒有關系的,比如下面的程式碼:
int* p = newint();
std::shared_ptr<int> sp(p);
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
字節。
大概的記憶體結構圖如下:
務必註意
_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