记一次 .NET 某企业审批系统 崩溃分析

一:背景

1. 讲故事

今年年初有位朋友在微信上找到我,说他们的系统在客户这边崩掉了,在代码中也加了全局异常处理但还是崩,不知道咋回事,让朋友在客户那边拿程序dump,拿到dump之后开始分析。

二:崩溃分析

1. 为什么会崩溃

既然是崩溃,那就用 !analyze -v 命令观察下windbg给我们整理的崩溃信息,看看可有蛛丝马迹。

C# 复制代码
0:000> !analyze -v
*******************************************************************************
*                                                                             *
*                        Exception Analysis                                   *
*                                                                             *
*******************************************************************************

CONTEXT:  (.ecxr)
eax=006fdb40 ebx=00000005 ecx=00000005 edx=00000000 esi=006fdc04 edi=00000001
eip=768f9132 esp=006fdb40 ebp=006fdb9c iopl=0         nv up ei pl nz ac po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000212
KERNELBASE!RaiseException+0x62:
768f9132 8b4c2454        mov     ecx,dword ptr [esp+54h] ss:002b:006fdb94=005b1570
Resetting default scope

EXCEPTION_RECORD:  (.exr -1)
ExceptionAddress: 768f9132 (KERNELBASE!RaiseException+0x00000062)
   ExceptionCode: e0434352 (CLR exception)
  ExceptionFlags: 00000001
NumberParameters: 5
   Parameter[0]: 80131604
   Parameter[1]: 00000000
   Parameter[2]: 00000000
   Parameter[3]: 00000000
   Parameter[4]: 72e90000

PROCESS_NAME:  xxx.exe

EXCEPTION_CODE_STR:  8007000e

FAULTING_THREAD:  ffffffff

STACK_TEXT:  
006fde30 07a0941c System_Drawing!System.Drawing.Graphics.FromHdcInternal+0x4c
006fde40 1173bbdb System_Drawing!System.Drawing.Font.GetHeight+0x5b
006fde70 1173bb4b System_Drawing!System.Drawing.Font.get_Height+0xb
006fde80 178658d4 DevExpress_Utils_v13_1!DevExpress.Utils.Frames.NotePanel.CalcSizes+0x304
006fdf4c 17864f4f DevExpress_Utils_v13_1!DevExpress.Utils.Frames.NotePanel.OnPaint+0x9f
006fe0b0 0705ab89 System_Windows_Forms!System.Windows.Forms.Control.PaintWithErrorHandling+0x89
006fe0e0 07058d75 System_Windows_Forms!System.Windows.Forms.Control.WmPaint+0x47d
006fe1d8 07a003db System_Windows_Forms!System.Windows.Forms.Control.WndProc+0x39b
006fe218 0707f821 System_Windows_Forms!System.Windows.Forms.Control+ControlNativeWindow.OnMessage+0x11
006fe220 0707f7f8 System_Windows_Forms!System.Windows.Forms.Control+ControlNativeWindow.WndProc+0xa0
006fe234 0707f227 System_Windows_Forms!System.Windows.Forms.NativeWindow.Callback+0x5f

从卦中可以看到 ExceptionCode: e0434352,这很明显是一个 托管异常,既然是 托管异常 那为什么朋友的代码拦截不到呢?这就比较有意思了,那到底是什么类型的托管异常,用 !t 观察一下。

C# 复制代码
0:000> !t
ThreadCount:      13
UnstartedThread:  0
BackgroundThread: 11
PendingThread:    0
DeadThread:       1
Hosted Runtime:   no
                                                                         Lock  
       ID OSID ThreadOBJ    State GC Mode     GC Alloc Context  Domain   Count Apt Exception
   0    1 68b4 00970248     26020 Preemptive  2BB06608:00000000 0096ad08 1     STA System.Reflection.TargetInvocationException 2baf5884 (nested exceptions)
   2    2 8b90 0098a220     2b220 Preemptive  00000000:00000000 0096ad08 0     MTA (Finalizer) 
   5    4  1cc 05e3f308   202b220 Preemptive  00000000:00000000 0096ad08 0     MTA 
   ...

从卦中的 System.Reflection.TargetInvocationException 可以看到这是一个 调用目标异常,由于卦象上有 (nested exceptions) 标记,使用 !pe -nested 命令观察异常链走势。

C# 复制代码
0:000> !pe -nested 
Exception object: 2baf5884
Exception type:   System.Reflection.TargetInvocationException
Message:          调用的目标发生了异常。
InnerException:   System.ComponentModel.Win32Exception, Use !PrintException 96e70c07 to see more.
StackTrace (generated):
    SP       IP       Function
    00000000 00000001 mscorlib_ni!System.RuntimeMethodHandle.SerializationInvoke(System.IRuntimeMethodInfo, System.Object, System.Runtime.Serialization.SerializationInfo, System.Runtime.Serialization.StreamingContext ByRef)+0xffffffff8d074f41
    xxx
    006FDFF4 71891968 mscorlib_ni!System.Resources.RuntimeResourceSet.GetObject(System.String, Boolean, Boolean)+0x1e8
    006FE074 7189B263 mscorlib_ni!System.Resources.RuntimeResourceSet.GetObject(System.String, Boolean)+0x13
    006FE07C 7189331B mscorlib_ni!System.Resources.ResourceManager.GetObject(System.String, System.Globalization.CultureInfo, Boolean)+0x22b
    006FE0DC 7194FD5B mscorlib_ni!System.Resources.ResourceManager.GetObject(System.String)+0xf
    006FE0E0 07CB92F8 xxx.frmBase.InitializeComponent()+0x68
    006FE0F0 0707308A xxx.frmBase..ctor()+0x42
    006FE100 05202B3D xxx.frmErrorMessage..ctor()+0xd
    006FE10C 05202A03 xxx.ExceptionHandler.HandlerErr(System.Exception)+0x23
    006FE11C 052029C9 xxx.UnhandledExceptionManager.Application_ThreadException(System.Object, System.Threading.ThreadExceptionEventArgs)+0x9
    006FE120 052027E8 System_Windows_Forms!System.Windows.Forms.Application+ThreadContext.OnThreadException(System.Exception)+0x88
    006FE15C 05202746 System_Windows_Forms!System.Windows.Forms.Control.WndProcException(System.Exception)+0x16
    006FE168 0520271A System_Windows_Forms!System.Windows.Forms.Control+ControlNativeWindow.OnThreadException(System.Exception)+0xa
    006FE16C 0707F256 System_Windows_Forms!System.Windows.Forms.NativeWindow.Callback(IntPtr, Int32, IntPtr, IntPtr)+0x8e

StackTraceString: <none>
HResult: 80131604

Nested exception -------------------------------------------------------------
Exception object: 2baeb3b0
Exception type:   System.OutOfMemoryException
Message:          内存不足。
InnerException:   <none>
StackTrace (generated):
    SP       IP       Function
    006FDE30 07A0941C System_Drawing!System.Drawing.Graphics.FromHdcInternal(IntPtr)+0x4c
    006FDE40 1173BBDB System_Drawing!System.Drawing.Font.GetHeight()+0x5b
    006FDE70 1173BB4B System_Drawing!System.Drawing.Font.get_Height()+0xb
    006FDE80 178658D4 DevExpress_Utils_v13_1!DevExpress.Utils.Frames.NotePanel.CalcSizes(System.Drawing.Graphics)+0x304
    006FDF4C 17864F4F DevExpress_Utils_v13_1!DevExpress.Utils.Frames.NotePanel.OnPaint(System.Windows.Forms.PaintEventArgs)+0x9f
    006FE0B0 0705AB89 System_Windows_Forms!System.Windows.Forms.Control.PaintWithErrorHandling(System.Windows.Forms.PaintEventArgs, Int16)+0x89
    006FE0E0 07058D75 System_Windows_Forms!System.Windows.Forms.Control.WmPaint(System.Windows.Forms.Message ByRef)+0x47d
    006FE1D8 07A003DB System_Windows_Forms!System.Windows.Forms.Control.WndProc(System.Windows.Forms.Message ByRef)+0x39b
    006FE218 0707F821 System_Windows_Forms!System.Windows.Forms.Control+ControlNativeWindow.OnMessage(System.Windows.Forms.Message ByRef)+0x11
    006FE220 0707F7F8 System_Windows_Forms!System.Windows.Forms.Control+ControlNativeWindow.WndProc(System.Windows.Forms.Message ByRef)+0xa0
    006FE234 0707F227 System_Windows_Forms!System.Windows.Forms.NativeWindow.Callback(IntPtr, Int32, IntPtr, IntPtr)+0x5f

StackTraceString: <none>
HResult: 8007000e

从卦象看,程序是先抛出了 OutOfMemoryException 异常,在全局异常处理中再次抛出 TargetInvocationException 引发程序崩溃,接下来看下为什么会抛 OutOfMemoryException 异常呢?导出源代码观察,简化后如下:

C# 复制代码
public float GetHeight()
{
    IntPtr dC = UnsafeNativeMethods.GetDC(NativeMethods.NullHandleRef);
    float num = 0f;
    try
    {
        using Graphics graphics = Graphics.FromHdcInternal(dC);
        return GetHeight(graphics);
    }
    finally
    {
        UnsafeNativeMethods.ReleaseDC(NativeMethods.NullHandleRef, new HandleRef(null, dC));
    }
}

public static Graphics FromHdcInternal(IntPtr hdc)
{
    IntPtr graphics = IntPtr.Zero;
    int num = SafeNativeMethods.Gdip.GdipCreateFromHDC(new HandleRef(null, hdc), out graphics);
    if (num != 0)
    {
        throw SafeNativeMethods.Gdip.StatusException(num);
    }
    return new Graphics(graphics);
}

internal static Exception StatusException(int status)
{
    return status switch
    {
        2 => new ArgumentException(SR.GetString("GdiplusInvalidParameter")),
        3 => new OutOfMemoryException(SR.GetString("GdiplusOutOfMemory")),
        ...
    };
}

从卦象看,应该就是 GdipCreateFromHDC 方法返回 num=3,继而代码上手工 throw OutOfMemoryException,这里不是引发程序崩溃的直接原因,所以暂时就不深究了,转头关注核心的 HandlerErr 方法。

2. 全局异常处理崩溃探究

接下来回头看为什么 Application_ThreadException -> HandlerErr 全局异常处理中会再次抛异常?观察源代码发现是在初始化 frmErrorMessage 的时候抛错的。。。无语了。。。难怪拦截不到,截图如下:

细心的朋友会发现上面还有一句话 System.ComponentModel.Win32Exception, Use !PrintException 96e70c07 to see more.,即 TargetInvocationException 的内部还有一个异常 Win32Exception,可以用 !pe 显示出来。

C# 复制代码
0:000> !PrintException /d 2baf5360
Exception object: 2baf5360
Exception type:   System.ComponentModel.Win32Exception
Message:          操作成功完成。
InnerException:   <none>
StackTrace (generated):
    SP       IP       Function
    006FDA9C 07CBA3BA System_Drawing!System.Drawing.Icon.Initialize(Int32, Int32)+0x83a
    006FDB6C 07CB9B49 System_Drawing!System.Drawing.Icon..ctor(System.Runtime.Serialization.SerializationInfo, System.Runtime.Serialization.StreamingContext)+0xc1

StackTraceString: <none>
HResult: 80004005
There are nested exceptions on this thread. Run with -nested for details

到这里逻辑基本就搞清楚了。

  1. 业务代码在 System.Drawing.Graphics.FromHdcInternal 处抛了一个 OutOfMemoryException 进入全局异常拦截。
  2. 在全局异常拦截中,又不幸在 new frmErrorMessage()System.Drawing.Icon.Initialize 处抛出了 Win32Exception 异常。

最后就是 Icon.Initialize 函数为什么会抛异常?说实话我也搞不清楚,在 stackoverflow https://stackoverflow.com/questions/2356580/system-drawing-icon-constructor-throwing-operation-completed-successfully-exce 上找到了这样的解读,截图如下:

最后给到朋友的建议:

  1. 设置多尺寸的 icon 图片,再观察效果。
  2. 修改 frmErrorMessage,避免双异常。

三:总结

本次事故是 多异常的联合作战 成功在高压的全局异常拦截方法Application_ThreadException中逃逸,是不是非常的有意思,示警大家,警惕被偷家!