coredump原理探究 Windows 筆記
原文鏈接:https://blog.csdn.net/xuzhina/article/details/8247701
感謝原作者,侵刪
一、環境搭建
1、Win7捕獲程序dump
注冊表HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Windows/Windows Error Reporting/LocalDumps
中新建幾個key

2、Windbg符號表設置(Symbols Search Path)
自動下載
D:\Debug\Symbols;SRV\*D:\Debug\Symbols\*http://msdl.microsoft.com/download/symbols;D:\Debug\Dump
手動下載
https://developer.microsoft.com/en-us/windows/hardware/download-symbols
二、WinDbg命令
命令 | 含義 | 實例 |
---|---|---|
x | 顯示所有上下文中符合某種模式的符號 | x Test!m* |
bp | 設置一個或多個軟件斷點;可通過 組合、地址、條件、選項來設置多種類型的斷點 |
bp Test!main |
u | 顯示出內存里某段程序的匯編 | u Test!main |
g | 開始執行指令的進程或線程;當出現下列情況,執行停止: 1、進程結束;2、執行到斷點;3、某個時間導致調試器終止 |
|
dd | 顯示指定范圍內存單位的內容(雙字dword) | dd esp L 8 |
da | 顯示指定內存開始的字符串 | da 004020dc |
db | 單字節顯示內存 | db esp |
t | 執行一條指令或一行代碼,並顯示出所有寄存器和狀態的值 | Trace |
p | 執行一條指令或一行代碼,並顯示出所有寄存器和狀態的值 當函數調用或中斷發生時,也只是作為一條指令執行 |
Step |
kbn | 顯示指定線程的棧幀,並顯示相關信息 | |
.frame n | 切換到棧幀n | |
ln | 查找就近的符號 | ln 004010e0 |
dt -v 結構體名 地址 | 打印結構體 | dt -v _HEAP_ENTRY 00730000 |
!heap -a | 查看堆信息 | |
!heap -x 地址 | 查看地址所屬的堆塊信息 | !heap -x 00730000 |
!heap -hf 地址 | 查看堆上所有的堆塊 | !heap -hf 00730000 |
三、函數棧幀
1、棧內存布局
返回地址ret
上一棧幀地址fp
局部變量
從右往左壓入參數
返回地址ret
......
2、棧溢出
往局部變量中寫數據越界,導致淹沒了fp和ret,函數返回時ebp、eip就會被寫入非法值
此時fp/ret對的鏈表關系被破壞,調試器無法顯示正確的函數棧
3、棧的規律
- esp的值不會被覆蓋,永遠指向棧頂
- fp/ret對的鏈表沒有被完全破壞,往高地址的一些地方還是保持這種關系
- 從棧頂到棧底,內存地址由低到高。如果幀指針fp1的內容是fp2,fp2的內容是fp3,那么存在:esp<fp1<fp2<fp3
- 如果兩對(fp1,ret2)、(fp2,ret2)符合條件:fp1的內容剛好是fp2,那么它們除了滿足第3點外,還滿足:ln ret1和ln ret2都能列出函數符號,ret2的上一條指令一定是調用ret1所在的函數
4、定位棧溢出問題的經驗方法
- dd esp查看由esp開始的內存
- 找到某個內容比esp的值稍大、數值相差不是太遠的內存單元,稱為FP1。下一個單元稱為RET1
- ln查看RET1的內容。如果沒有顯示函數符號,跳過FP1,回到第2步
- dd *FP1 L 2得到兩個內存單元(FP2,RET2)。如果FP2的內容小於FP1的內容,跳過FP1,回到第2步
- ln查看RET2的內容。如果沒有顯示函數符號,跳過FP1,回到第2步;如果OK,跳到FP2,回到第4步
四、函數逆向
匯編代碼中跳轉和循環為函數的骨架,要優先尋找
五、C內存布局
1、基本類型
類型 | 特征 |
---|---|
char | byte ptr 擠在一起 |
short | word ptr 占用四字節空間 |
int | dword ptr |
long | dword ptr win64采用LLP64標准,Windows系統中long和int是相同的 |
float | dword ptr 單精度占四字節,要配合浮點計算指令確認 |
double | qword ptr 雙精度占八字節,要配合浮點計算指令確認 |
指針 | lea |
2、數組類型
類型 | 特征 |
---|---|
char | 基地址 + 索引值 * 1 |
short | 基地址 + 索引值 * 2 數組時會擠在一起,注意與基本類型的區別 |
int | 基地址 + 索引值 * 4 |
long | 基地址 + 索引值 * 4 |
float | 基地址 + 索引值 * 4 單精度占四字節,要配合浮點計算指令確認 |
double | 基地址 + 索引值 * 4 雙精度占八字節,要配合浮點計算指令確認 |
指針 | 基地址 + 索引值 * 4 |
3、結構體
- 成員全是基本類型的結構體: 先把一個基地址放到某寄存器中,訪問成員時在基址上加上前面所有成員的大小,每個成員與基址的偏移量不固定
- 復合類型構成的結構體: 同上,沒有特別
- 結構體數組: 找到數組的首地址;根據索引找到每個元素的地址;以每個元素的地址作為結構體基址,獲取成員變量的地址
六、C++內存布局
1、類的內存布局
類的成員變量排列與結構體相同
2、this指針
調用類的成員函數時,this指針放在ecx寄存器中傳遞,不入棧
- 調用函數時的匯編代碼:
- lea ecx,[ebp-1]
- call Test!Test::print (00ec1030)
- 被調函數開始的匯編代碼:
- mov dword ptr [ebp-4],ecx
- mov eax,dword ptr [ebp-4]
- push eax
3、虛函數表及虛表指針

4、單繼承
子類先調用基類的構造函數,再初始化自己的成員變量,然后設置虛表指針
子類虛函數表分布規律:
- 重載基類的虛函數,按照基類虛函數聲明順序排列,和子類聲明順序無關
- 子類獨有的虛函數,按照虛函數的聲明順序排列,追加在重載虛函數的后面
5、多繼承(無公共基類)
- 子類對象的大小等於各個基類的大小(虛表指針+成員變量)加上自身成員變量的大小
- 各基類在子類里的“隱含對象”是按照繼承順序來排列的,和基類的聲明、定義順序無關
- 每個基類都(盡可能)有自己的虛函數表;子類獨有的虛函數追加到第一個虛函數表的后面;子類重載所有虛表中的同名虛函數
- 子類對象指針轉換成基類指針,實際上是把子類對象包含的對應基類的“隱含對象”的地址賦值給基類指針
- 當一個虛函數在多個虛表中都出現時,實際上只會完全重載第一個虛表中的該函數。其余虛表中重載代碼是通過調整this指針為子類的地址,然后跳轉到子類對應函數來實現
- 0:000> u 012e10a8 L 5
- Test![thunk]:Child::print`adjustor{12}':
- 012e10a8 83e90c sub ecx,0Ch // ecx是基類“隱含對象”的地址,需調整
- 012e10ab e9e0ffffff jmp Test!Child::print (012e1090)
七、STL容器內存布局
1、vector
一個vector在棧上占三個單元(指針):
第一個_Myfirst指向vector元素的開始
第二個_Mylast指向vector元素結束的下一個位置
第三個_Myend指向vector空間結束的位置
注意:vector的begin()、end()、push_back()等成員函數的匯編,最后是:ret 4

2、list
- list有兩個成員:第一個_Myhead指向鏈表的頭部節點,第二個_Size表明鏈表中的節點元素個數
- 鏈表中的每個節點包含三個成員:第一個_Next指向下一個節點,第二個_Prev指向前一個節點,第三個_Myval存儲節點的值
- list初始化時會生成一個頭節點
- 頭節點的_Next指向鏈表第一個節點,鏈表最后一個節點的_Next指向頭節點
- 頭節點的_Prev指向鏈表最后一個節點,鏈表第一個節點的_Prev指向頭節點
注意:圖中_Prev指針應該都指向節點起始位置,而不是_Prev指針的位置。這里只是為了突出兩個鏈路

3、map
- map有兩個成員:_Myhead指向頭節點,_Mysize表明map包含的元素個數
- 頭節點的三個指針分別指向樹的最左節點、樹的根節點、樹的最右節點
- 樹的根節點的_Parent指向頭節點
- 樹的葉子節點的_Left、_Right指向頭節點

4、set
由於map、set本身的定義都沒有聲明任何成員變量,所有的成員變量都是從_Tree繼承過來的,唯一的區別是traits的定義不一樣,因此:set的特征和map類似
- template<class _Kty,
- class _Pr = less<_Kty>,
- class _Alloc = allocator<_Kty> >
- class set : public _Tree<_Tset_traits<_Kty, _Pr, _Alloc, false> >
- { ...... }
-
- template<class _Kty,
- class _Ty,
- class _Pr = less<_Kty>,
- class _Alloc = allocator<pair<const _Kty, _Ty> > >
- class map : public _Tree<_Tmap_traits<_Kty, _Ty, _Pr, _Alloc, false> >
- { ...... }
5、iterator
- vector的iterator只有一個成員_Ptr,取值范圍:vec._Myfirst <= _Ptr < vec._Mylast
- list的iterator也只有一個成員_Ptr,指向list中的每個節點(頭節點除外)
- map和set的iterator也只有一個成員_Ptr,指向map或set的節點,且iterator的遍歷采用中序遍歷
實際調試中,set的iterator指向節點的值在for循環中是按照0、1、2、3......、f的順序遍歷

6、string
string有三個成員:聯合體_Bx,緊接着的是字符串長度_Mysize,預留空間大小_Myres
- 當_Mysize < _BUF_SIZE(16)時,字符串存儲在_Bx的_Buf里
- 當_Mysize >= _BUF_SIZE(16)時,字符串存儲在_Bx的_Ptr指向的內存中
- // TEMPLATE CLASS _String_val
- template<class _Val_types>
- class _String_val : public _Container_base
- { // base class for basic_string to hold data
- public:
- ......
- enum { // length of internal buffer, [1, 16]
- _BUF_SIZE = 16 / sizeof (value_type) < 1
- ? 1
- : 16 / sizeof (value_type)
- };
- ......
- union _Bxty { // storage for small buffer or pointer to larger one
- value_type _Buf[_BUF_SIZE];
- pointer _Ptr;
- char _Alias[_BUF_SIZE]; // to permit aliasing
- } _Bx;
- size_type _Mysize; // current length of string
- size_type _Myres; // current storage reserved for string
- };
八、堆結構
1、NT內核堆的改造
文檔中介紹的堆結構是XP環境下的。MS從Vista開始對NT內核做了較大改動,其中包括堆的改造。最直觀的改造:
- _HEAP中采用鏈表方式管理_HEAP_SEGMENT,解除數組的限制
- _HEAP_ENTRY結構進行了編碼,引入隨機性,增強堆的安全性
- 取消空閑堆塊鏈表的頭節點數組,直接使用鏈表管理空閑堆塊,即_HEAP中FreeLists從[128]的_LIST_ENTRY數組改為單個元素
2、Win7下堆的結構
- struct _HEAP_ENTRY {
- SHORT Size; /* 當前塊的大小。
- 直接dt -v打出來的值是經過了編碼的,
- 實際值需要和_HEAP中的Encoding做一次異或,取最低的兩個字節。
- 真正的塊的字節數還需要Size*8。
- 這個值包含了這個堆塊頭結構_HEAP_ENTRY的8字節
- */
- // ……省略若干字段
- SHORT PreviousSize;
- // ……省略若干字段
- BYTE UnusedBytes; // 未使用的字節數
- // ……省略若干字段
- }; /* 整個_HEAP_ENTRY結構共8字節,
- 后面緊接着申請出來的內存塊,malloc或new返回的也是指向這個內存塊的指針,
- 這個指針的值一定是8的倍數,
- 再后面緊接着的就是下一個堆塊
- */
-
- struct _HEAP_SEGMENT {
- _HEAP_ENTRY Entry;
- UINT SegmentSignature;
- UINT SegmentFlags;
- _LIST_ENTRY SegmentListEntry; /* segment list的入口,
- 指示當前_HEAP_SEGMENT節點在segment list中的位置,
- 各_HEAP_SEGMENT通過這個字段連接 */
- _PHEAP Heap; /* 指向所屬的_HEAP */
- // ……省略若干字段
- _HEAP_ENTRY* FirstEntry;
- _HEAP_ENTRY* LastValidEntry;
- // ……省略若干字段
- _LIST_ENTRY UCRSegmentList;
- };
-
- struct _HEAP {
- _HEAP_SEGMENT Segment; /* 第一個段 */
- // ……省略若干字段
- UINT EncodeFlagMask; /* 是否啟用編碼功能 */
- _HEAP_ENTRY Encoding; /* 編碼key,
- 用這個結構和每個堆塊頭結構_HEAP_ENTRY做異或
- */
- // ……省略若干字段
- _LIST_ENTRY SegmentList; /* segment list的頭節點,
- 分別指向第一個、最后一個_HEAP_SEGMENT的SegmentListEntry
- */
- // ……省略若干字段
- _LIST_ENTRY FreeLists; /* 空閑堆塊鏈表頭節點,
- 分別指向第一個、最后一個_HEAP_FREE_ENTRY的FreeList,
- XP中這里是[128]的_LIST_ENTRY數組
- */
- // ……省略若干字段
- _HEAP_TUNING_PARAMETERS TuningParameters;
- };
-
- /* 空閑堆塊結構 */
- struct _HEAP_FREE_ENTRY {
- _HEAP_ENTRY Entry;
- _LIST_ENTRY FreeList; /* free list的入口,
- 指示當前空閑堆塊在鏈表中的位置
- 各空閑堆塊通過這個字段連接
- 如果兩個節點在內存中連續則合並
- */
- };

3、堆塊的調試
獲取一個地址所屬堆塊的信息
- 0:000> !heap -x 00730000
- Entry User Heap Segment Size PrevSize Unused Flags
- -----------------------------------------------------------------------------
- 00730000 00730008 00730000 00730000 588 0 1 busy
- 注意:這里的Size為588是16進制
獲取一個堆塊的大小
- 0:000> dt -v _HEAP_ENTRY 00730000 直接打印_HEAP_ENTRY結構
- ntdll!_HEAP_ENTRY
- struct _HEAP_ENTRY, 19 elements, 0x8 bytes
- +0x000 Size : 0x9496 顯然不對
- ……
- +0x007 UnusedBytes : 0x1 ''
- ……
- 0:000> dd 00730000+0x050 L 4 獲取Encoding結構體。Encoding相對_HEAP的偏移是0x050
- 00730050 47329427 000024e0 3b5c5f17 00000000 Encoding低4字節的值為47329427
- 0:000> dd 00730000 L 4 打印_HEAP_ENTRY結構的值
- 00730000 f7339496 010024e0 ffeeffee 00000000 打印出的值f7339496是原始值和Encoding經過異或后得到的
- 0:000> ? f7339496 ^ 47329427 要求原始值只需要當前值和Encoding再異或一遍
- Evaluate expression: -1342111567 = b00100b1 低地址的兩字節就是原始的Size
- 0:000> ? 00b1 * 8 實際堆塊的字節數還要Size*8
- Evaluate expression: 1416 = 00000588
4、heap corruption問題
常見原因:
- free導致coredump:
free了野指針:需要檢查指針的正確性。例如:是否在!heap -hf所列范圍內、是否是8的倍數等
堆塊寫越界:需要檢查前后堆塊的Size和PreSize - malloc導致coredump:
一般是因為堆塊寫越界,破壞了空閑堆塊的結構。!heap -hf可以找到同一個堆塊即是free又是busy的狀態
九、dll hell問題
dll hell常導致虛函數的漂移,本質上就是一個dll之間版本不匹配的問題。