博客 / 詳情

返回

記一次 .NET 某理財管理客户端 OOM溢出分析

一:背景

1. 講故事

這是訓練營裏的學員找到我的,讓我幫忙看下為什麼他的客户程序會偶發的出現 報錯彈框,由於dump比較敏感,這裏就不截圖發出來了,由於是錯誤彈框,並不會出現程序崩潰,而且朋友在日誌中也看到了 OOM 異常,就是因為這個 OOM 異常導致了後續流程的 報錯彈框,説這個程序的內存還行,在業務代碼中用了 try catch 吞掉異常了,讓我幫忙看下。

由於 OOM dump沒到手,而且代碼中使用 try catch 吞掉了,有些人可能就沒撤了,其實知道 異常兩階段 的朋友應該知道,我們可以在 first chance 的時候抓dump,即 catch 之前,所以就有了下面的捕獲腳本。


procdump 20860 -e 1 -f PAVException -ma -o D:\testdump\

順利拿到dump之後,接下來就是一頓分析了。

二:OOM分析

1. 為什麼會 OOM

雙擊 dump 之後,映入眼簾的就是異常線程的現場信息,參考如下:


This dump file has an exception of interest stored in it.
The stored exception information can be accessed via .ecxr.
(15fc.4fe8): C++ EH exception - code e06d7363 (first/second chance not available)
For analysis of this file, run !analyze -v
eax=2b1aefa0 ebx=19930520 ecx=00000003 edx=00000000 esi=037eebc0 edi=530bb548
eip=77383874 esp=2b1aefa0 ebp=2b1aeffc iopl=0         nv up ei pl nz ac pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000216
KERNELBASE!RaiseException+0x64:
77383874 8b4c2454        mov     ecx,dword ptr [esp+54h] ss:002b:2b1aeff4=224e4fd8

從卦中可以看到 RaiseException 就是託管異常的明證,接下來用 .ecxr ; k 觀察異常調用棧。


0:052> .ecxr;k 
eax=2b1aefa0 ebx=19930520 ecx=00000003 edx=00000000 esi=037eebc0 edi=530bb548
eip=77383874 esp=2b1aefa0 ebp=2b1aeffc iopl=0         nv up ei pl nz ac pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000216
KERNELBASE!RaiseException+0x64:
77383874 8b4c2454        mov     ecx,dword ptr [esp+54h] ss:002b:2b1aeff4=224e4fd8
  *** Stack trace for last set context - .thread/.cxr resets it
 # ChildEBP RetAddr      
00 2b1aeffc 52e3c8fb     KERNELBASE!RaiseException+0x64
01 2b1af02c 52fee8fc     coreclr!_CxxThrowException+0x66 [d:\a01\_work\11\s\src\vctools\crt\vcruntime\src\eh\throw.cpp @ 74] 
02 2b1af040 52d481a8     coreclr!ThrowOutOfMemory+0x24 [D:\a\_work\1\s\src\coreclr\src\utilcode\ex.cpp @ 1044] 
03 2b1af074 30b8f91e     coreclr!LargeHeapHandleTable::AllocateHandles [D:\a\_work\1\s\src\coreclr\src\vm\appdomain.cpp @ 381] 
WARNING: Frame IP not in any known module. Following frames may be wrong.
04 2b1af074 05990114     0x30b8f91e
05 2b1af074 52d452e7     0x5990114
06 2b1af0c8 52d453e7     coreclr!AllocateSzArray+0x227 [D:\a\_work\1\s\src\coreclr\src\vm\gchelpers.cpp @ 427] 
07 2b1af14c 5257296e     coreclr!JIT_NewArr1+0xb7 [D:\a\_work\1\s\src\coreclr\src\vm\jithelpers.cpp @ 2723] 
08 2b1af160 52581bcf     System_Private_CoreLib!System.Text.Encoding.GetBytes+0x22 [_/src/libraries/System.Private.CoreLib/src/System/Text/Encoding.cs @ 667] 
09 2b1af168 263e7ad6     System_Private_CoreLib!System.Text.UTF8Encoding.UTF8EncodingSealed.GetBytes+0x1b
0a 2b1af1a8 263e7a43     xxx!xxx.xxxxHashData+0x46

從卦中可以清晰的看到,原來是在 xxxxHashData 中執行了 GetBytes 時拋出的 OOM 異常, 那為什麼 GetBytes 會拋出異常呢?這個只能結合源代碼説話了。

2. GetBytes 為什麼會拋出 OOM

找到 xxxxHashData 下的 GetBytes 方法,截圖如下:

從卦中可以看到參數是一個 string,看樣子這就是突破口了,使用 !clrstack -a 觀察這個 s 的具體值,參考如下:


0:052> !clrstack -a
OS Thread Id: 0x4fe8 (52)
Child SP       IP Call Site
2B1AF0E8 77383874 [HelperMethodFrame: 2b1af0e8] 
2B1AF154 5257296e System.Text.Encoding.GetBytes(System.String) [/_/src/libraries/System.Private.CoreLib/src/System/Text/Encoding.cs @ 667]
    PARAMETERS:
        this (<CLR reg>) = 0x05b5b674
        s (<CLR reg>) = 0x348d1010
    LOCALS:
        <no data>
        <no data>
        <no data>

0:052> !DumpObj /d 348d1010
Name:        System.String
MethodTable: 0568ec98
EEClass:     0569a8c0
Size:        83886094(0x500000e) bytes
String:      <String is invalid or too large to print>
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
056873b4  4000212        4         System.Int32  1 instance 41943040 _stringLength
056854e0  4000213        8          System.Char  1 instance       54 _firstChar
0568ec98  4000211       60        System.String  0   static 05b512b0 Empty

0:052> ? 0x500000e
Evaluate expression: 83886094 = 0500000e

從卦中看真的是嚇一跳,string.length=4194w 真尼瑪大,並且 string 的重量高達 83M,就是由於這個 83M 的string,被 clr 直接給屏掉了。。。接下來的問題是為什麼 clr 會屏掉呢?

3. clr 為什麼會屏掉

有一些 clr 基礎知識的朋友應該知道,這種 OOM 異常一般是兩種情況。

  1. 通過 if 語句判斷是否超限,這個在訓練營裏面都有講到,參考代碼如下:

    // Limit the maximum string size to <2GB to mitigate risk of security issues caused by 32-bit integer
    // overflows in buffer size calculations.
    if (cchStringLength > CORINFO_String_MaxLength)
        ThrowOutOfMemory();

  1. 向託管堆要指定大小的內存要不到的時候,這個可以用 !ao 命令觀察。

0:052> !ao
Didn't have enough memory to allocate an LOH segment
Details: LOH Failed to reserve memory 50,331,648 bytes

從上面的卦數據來看,是 clr 向大對象堆預定50M的連續地址空間時,結果要不到,clr非常無奈拋出了這個OOM異常。

接下來的問題是為什麼要不到呢?

4. 為什麼託管堆拒絕了

有經驗的朋友應該知道是咋回事了,對,就是虛擬地址空間不足導致的。。。 可以用 !address -summary 觀察虛擬地址空間大小。


0:052> !address -summary

--- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
<unknown>                              1091          3e78b000 ( 999.543 MB)  64.47%   48.81%
Free                                    380          1f183000 ( 497.512 MB)           24.29%
Image                                  1039          17d37000 ( 381.215 MB)  24.59%   18.61%
Stack                                   219           6100000 (  97.000 MB)   6.26%    4.74%
Heap                                     38           4751000 (  71.316 MB)   4.60%    3.48%
TEB                                      73            11a000 (   1.102 MB)   0.07%    0.05%
Other                                    21             3d000 ( 244.000 kB)   0.02%    0.01%
PEB                                       1              3000 (  12.000 kB)   0.00%    0.00%

--- Type Summary (for busy) ------ RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_PRIVATE                            1010          36608000 ( 870.031 MB)  56.12%   42.48%
MEM_IMAGE                              1142          17e6c000 ( 382.422 MB)  24.67%   18.67%
MEM_MAPPED                              330          129f9000 ( 297.973 MB)  19.22%   14.55%

--- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_COMMIT                             1937          4fdd5000 (   1.248 GB)  82.42%   62.40%
MEM_FREE                                380          1f183000 ( 497.512 MB)           24.29%
MEM_RESERVE                             545          11098000 ( 272.594 MB)  17.58%   13.31%

從卦中可以看到雖然 MEM_RESERVE=272M ,但沒有哪一塊是大於 50M 的,所以直接導致災難的發生,到這裏該如何解決呢?這其實也是一個經典的問題,即 32bit 程序 2G 地址空間問題,修改辦法如下:

  1. 使用大地址 LargeAddress,讓程序儘量吃 4G 內存。
  2. 將程序調整到 64bit,讓虛擬地址不再捉襟見肘。

三:總結

這個故障也不算是什麼大問題,就像網絡時好時壞一樣,不過像這種強勢部門投放過來的抱怨也是亞歷山大的。。。

圖片名稱
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.