冬男煜亭 發表於 2025-11-13 12:13:00

记一次 .NET 某理财管理客户端 OOM溢出分析

<h2 id="一背景">一:背景</h2>
<h3 id="1-讲故事">1. 讲故事</h3>
<p>这是训练营里的学员找到我的,让我帮忙看下为什么他的客户程序会偶发的出现 <code>报错弹框</code>,由于dump比较敏感,这里就不截图发出来了,由于是错误弹框,并不会出现程序崩溃,而且朋友在日志中也看到了 OOM 异常,就是因为这个 OOM 异常导致了后续流程的 <code>报错弹框</code>,说这个程序的内存还行,在业务代码中用了 try catch 吞掉异常了,让我帮忙看下。</p>
<p>由于 OOM dump没到手,而且代码中使用 <code>try catch</code> 吞掉了,有些人可能就没撤了,其实知道 <code>异常两阶段</code> 的朋友应该知道,我们可以在 <code>first chance</code> 的时候抓dump,即 catch 之前,所以就有了下面的捕获脚本。</p>
<pre><code class="language-C#">
procdump 20860 -e 1 -f PAVException -ma -o D:\testdump\

</code></pre>
<p>顺利拿到dump之后,接下来就是一顿分析了。</p>
<h2 id="二oom分析">二:OOM分析</h2>
<h3 id="1-为什么会-oom">1. 为什么会 OOM</h3>
<p>双击 dump 之后,映入眼帘的就是异常线程的现场信息,参考如下:</p>
<pre><code class="language-C#">
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=0023ss=002bds=002bes=002bfs=0053gs=002b             efl=00000216
KERNELBASE!RaiseException+0x64:
77383874 8b4c2454      mov   ecx,dword ptr ss:002b:2b1aeff4=224e4fd8

</code></pre>
<p>从卦中可以看到 RaiseException 就是托管异常的明证,接下来用 <code>.ecxr ; k</code> 观察异常调用栈。</p>
<pre><code class="language-C#">
0:052&gt; .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=0023ss=002bds=002bes=002bfs=0053gs=002b             efl=00000216
KERNELBASE!RaiseException+0x64:
77383874 8b4c2454      mov   ecx,dword ptr 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
02 2b1af040 52d481a8   coreclr!ThrowOutOfMemory+0x24
03 2b1af074 30b8f91e   coreclr!LargeHeapHandleTable::AllocateHandles
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
07 2b1af14c 5257296e   coreclr!JIT_NewArr1+0xb7
08 2b1af160 52581bcf   System_Private_CoreLib!System.Text.Encoding.GetBytes+0x22
09 2b1af168 263e7ad6   System_Private_CoreLib!System.Text.UTF8Encoding.UTF8EncodingSealed.GetBytes+0x1b
0a 2b1af1a8 263e7a43   xxx!xxx.xxxxHashData+0x46

</code></pre>
<p>从卦中可以清晰的看到,原来是在 <code>xxxxHashData</code> 中执行了 <code>GetBytes</code> 时抛出的 OOM 异常, 那为什么 <code>GetBytes</code> 会抛出异常呢?这个只能结合源代码说话了。</p>
<h3 id="2-getbytes-为什么会抛出-oom">2. GetBytes 为什么会抛出 OOM</h3>
<p>找到 <code>xxxxHashData</code> 下的 <code>GetBytes</code> 方法,截图如下:</p>
<p><img src="https://img2024.cnblogs.com/blog/214741/202511/214741-20251113121245964-1794360798.png" alt="" loading="lazy"></p>
<p>从卦中可以看到参数是一个 string,看样子这就是突破口了,使用 <code>!clrstack -a</code> 观察这个 s 的具体值,参考如下:</p>
<pre><code class="language-C#">
0:052&gt; !clrstack -a
OS Thread Id: 0x4fe8 (52)
Child SP       IP Call Site
2B1AF0E8 77383874
2B1AF154 5257296e System.Text.Encoding.GetBytes(System.String)
    PARAMETERS:
      this (&lt;CLR reg&gt;) = 0x05b5b674
      s (&lt;CLR reg&gt;) = 0x348d1010
    LOCALS:
      &lt;no data&gt;
      &lt;no data&gt;
      &lt;no data&gt;

0:052&gt; !DumpObj /d 348d1010
Name:      System.String
MethodTable: 0568ec98
EEClass:   0569a8c0
Size:      83886094(0x500000e) bytes
String:      &lt;String is invalid or too large to print&gt;
Fields:
      MT    Field   Offset               Type VT   Attr    Value Name
056873b44000212      4         System.Int321 instance 41943040 _stringLength
056854e04000213      8          System.Char1 instance       54 _firstChar
0568ec984000211       60      System.String0   static 05b512b0 Empty

0:052&gt; ? 0x500000e
Evaluate expression: 83886094 = 0500000e

</code></pre>
<p>从卦中看真的是吓一跳,<code>string.length=4194w</code> 真尼玛大,并且 string 的重量高达 <code>83M</code>,就是由于这个 83M 的string,被 clr 直接给屏掉了。。。接下来的问题是为什么 clr 会屏掉呢?</p>
<h3 id="3-clr-为什么会屏掉">3. clr 为什么会屏掉</h3>
<p>有一些 clr 基础知识的朋友应该知道,这种 OOM 异常一般是两种情况。</p>
<ol>
<li>通过 if 语句判断是否超限,这个在训练营里面都有讲到,参考代码如下:</li>
</ol>
<pre><code class="language-C++">
    // Limit the maximum string size to &lt;2GB to mitigate risk of security issues caused by 32-bit integer
    // overflows in buffer size calculations.
    if (cchStringLength &gt; CORINFO_String_MaxLength)
      ThrowOutOfMemory();

</code></pre>
<ol start="2">
<li>向托管堆要指定大小的内存要不到的时候,这个可以用 !ao 命令观察。</li>
</ol>
<pre><code class="language-C#">
0:052&gt; !ao
Didn't have enough memory to allocate an LOH segment
Details: LOH Failed to reserve memory 50,331,648 bytes

</code></pre>
<p>从上面的卦数据来看,是 clr 向大对象堆预定50M的连续地址空间时,结果要不到,clr非常无奈抛出了这个OOM异常。</p>
<p>接下来的问题是为什么要不到呢?</p>
<h3 id="4-为什么托管堆拒绝了">4. 为什么托管堆拒绝了</h3>
<p>有经验的朋友应该知道是咋回事了,对,就是虚拟地址空间不足导致的。。。 可以用 <code>!address -summary</code> 观察虚拟地址空间大小。</p>
<pre><code class="language-C#">
0:052&gt; !address -summary

--- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
&lt;unknown&gt;                              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%

</code></pre>
<p>从卦中可以看到虽然 <code>MEM_RESERVE=272M</code> ,但没有哪一块是大于 50M 的,所以直接导致灾难的发生,到这里该如何解决呢?这其实也是一个经典的问题,即 32bit 程序 2G 地址空间问题,修改办法如下:</p>
<ol>
<li>使用大地址 LargeAddress,让程序尽量吃 4G 内存。</li>
<li>将程序调整到 64bit,让虚拟地址不再捉襟见肘。</li>
</ol>
<h2 id="三总结">三:总结</h2>
<p>这个故障也不算是什么大问题,就像网络时好时坏一样,不过像这种强势部门投放过来的抱怨也是亚历山大的。。。</p>
<img src="https://images.cnblogs.com/cnblogs_com/huangxincheng/345039/o_210929020104最新消息优惠促销公众号关注二维码.jpg" width="700" height="300" alt="图片名称" align="center"><br><br>
来源:https://www.cnblogs.com/huangxincheng/p/19217608
頁: [1]
查看完整版本: 记一次 .NET 某理财管理客户端 OOM溢出分析