一:背景
1. 講故事
前些天有位朋友找到我,說他的程序幾天內存就要爆一次,不知道咋回事,找不出原因,讓我幫忙看一下,這種問題分析dump是最簡單粗暴了,拿到dump後接下來就是一頓分析。
二:WinDbg 分析
1. 程序爲什麼會暴
程序既然會爆,可能是虛擬地址受限,也可能是系統內存不足,可以用 !address -summary
觀察下。
0:037> !address -summary
--- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
<unknown> 866 53577000 ( 1.302 GB) 69.38% 65.11%
Image 2244 16ee2000 ( 366.883 MB) 19.09% 17.91%
Heap 222 8adc000 ( 138.859 MB) 7.23% 6.78%
Free 460 7e14000 ( 126.078 MB) 6.16%
Stack 255 5150000 ( 81.312 MB) 4.23% 3.97%
TEB 85 db000 ( 876.000 kB) 0.04% 0.04%
Other 20 79000 ( 484.000 kB) 0.02% 0.02%
PEB 1 3000 ( 12.000 kB) 0.00% 0.00%
...
--- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_COMMIT 2900 64906000 ( 1.571 GB) 83.72% 78.57%
MEM_RESERVE 793 138d6000 ( 312.836 MB) 16.28% 15.28%
MEM_FREE 460 7e14000 ( 126.078 MB) 6.16%
...
從卦中可以明顯的看出,這又是一例經典的32bit程序受到了2G的內存限制,按往期經驗來說解決辦法比較簡單,改成大地址或者x64即可。
哈哈,既然要分享這篇,自然就不是這麼簡單的事情,這需要我們排查這個溢出是不是程序的bug導致的,如果是那還得繼續找原因。
2. 是程序bug導致的嗎
要想搞清楚這個問題,需要去分析各處的內存佔用,比如託管堆,可以用 !eeheap -gc
觀察。
0:037> !eeheap -gc
Number of GC Heaps: 1
generation 0 starts at 0x49fd10a8
generation 1 starts at 0x49fd1000
generation 2 starts at 0x03381000
ephemeral segment allocation context: none
segment begin allocated size
03380000 03381000 0437ff88 0xffef88(16773000)
23e60000 23e61000 24e5ff88 0xffef88(16773000)
0b510000 0b511000 0c50ff88 0xffef88(16773000)
...
7be20000 7be21000 7cbbdb60 0xd9cb60(14273376)
49fd0000 49fd1000 4afcfe08 0xffee08(16772616)
Large object heap starts at 0x04381000
segment begin allocated size
04380000 04381000 04a67b50 0x6e6b50(7236432)
Total Size: Size: 0x39738ad4 (963873492) bytes.
------------------------------
GC Heap Size: Size: 0x39738ad4 (963873492) bytes.
從卦中可以看到,託管堆佔用963M,並且產生了很多的16M的segment,這就表明當前的託管堆喫掉了內存,接下來的問題是爲什麼託管堆吃了那麼多的內存呢?那就只能用 !dumpheap -stat
去觀察下託管堆的對象佈局咯。
0:037> !dumpheap -stat
Statistics:
MT Count TotalSize Class Name
...
717c8b4c 264594 11642136 System.Threading.ExecutionContext
717cd044 265930 13034088 System.Collections.Hashtable+bucket[]
717ccff4 265854 13824408 System.Collections.Hashtable
71761c34 268005 17152320 System.Threading.OverlappedData
70d73c10 264469 26446900 System.Net.Sockets.OverlappedAsyncResult
717cdd04 280225 293649193 System.Byte[]
013a9f98 269886 540566904 Free
Total 3880354 objects
從卦中可以看到當前託管堆有 26.8w
的 OverlappedData 對象,這是一個非常明顯的異常信號,熟悉這塊的朋友應該知道,這個東西常常和異步打交道,也就表示當前程序可能有高達 26.8w 的異步請求可能沒有得到響應,要想找到這個答案,就需要對 OverlappedData 進行穿刺。
3. OverlappedData 穿刺檢查
對 OverlappedData
穿刺的目的就是要活檢內部的 AsyncCallback
回調函數,看看到底是良性還是惡性的,相關命令如下:
0:037> !dumpheap -stat
...
34f38ac4 71761c34 64
34f39088 71761c34 64
...
0:037> !mdt 34f39088
34f39088 (System.Threading.OverlappedData)
m_asyncResult:33e8aafc (System.Net.Sockets.OverlappedAsyncResult)
m_iocb:03c077a0 (System.Threading.IOCompletionCallback)
...
m_nativeOverlapped:(System.Threading.NativeOverlapped) VALTYPE (MT=7176dfe0, ADDR=34f390b0)
0:037> !mdt 33e8aafc
33e8aafc (System.Net.Sockets.OverlappedAsyncResult)
m_AsyncObject:03c71d44 (System.Net.Sockets.Socket)
m_AsyncState:33e8aaec (xxx)
m_AsyncCallback:03e8f214 (System.AsyncCallback)
...
0:037> !mdt 03e8f214
03e8f214 (System.AsyncCallback)
_target:03c065a8 (xxx)
_methodPtr:19432480 (System.IntPtr)
0:037> u 19432480
19432480 e933932102 jmp 1b64b7b8
19432485 5f pop edi
...
0:037> !ip2md 1b64b7b8
MethodDesc: 131605ac
Method Name: xxxDevices.ReceiveCallback(System.IAsyncResult)
卦中的信息量還是蠻大的,可以看到這是一個和 Socket 相關的異步函數,並且也成功找到了 xxxDevices.ReceiveCallback
回調函數,接下來就是檢查下這個方法附近的業務邏輯,由於代碼會涉及到一些隱私,我就多模糊一點,請見諒,截圖如下:
仔細閱讀這段代碼,他是想用異步的方式一次次的用byte[1024]
去丈量一段可能的大數據,直到這個 Stream 不能再讀了,所以用了 if (stream.CanRead)
判斷。
對 Socket 編程比較熟悉的朋友相信很快就能發現問題,判斷 Stream 中的數據是否讀完應該用 DataAvailable
屬性,而不是 CanRead
,比如下面這段正確的代碼:
最後再貼VS中對 CanRead
和 DataAvailable
屬性的解釋。
//
// Summary:
// Gets a value that indicates whether the System.Net.Sockets.NetworkStream supports
// reading.
//
// Returns:
// true if data can be read from the stream; otherwise, false. The default value
// is true.
public override bool CanRead { get; }
//
// Summary:
// Gets a value that indicates whether data is available on the System.Net.Sockets.NetworkStream
// to be read.
//
// Returns:
// true if data is available on the stream to be read; otherwise, false.
//
public virtual bool DataAvailable { get; }
三:總結
這個事故非常有意思,一個簡簡單單的 CanRead
誤用就對程序造成了毀滅性的打擊,這也警示大家在用某個屬性某個方法前,一定要先搞清楚它到底是怎麼玩的。