zoukankan      html  css  js  c++  java
  • 一次有教益的程序崩溃调试 (上)

    在系统上线前夕,我们将所有的子系统部署在预发布环境中做集成测试。集成测试模拟真实的业务场景,不停断地使用真实数据测试组成系统的各个服务(service)。

     预发布环境的Windows系统都启动了自动内存转储(auto memory dump)功能:当程序崩溃(crash)时,Windows会调用Windbg生成该程序的内存转储,以方便事后调试(postmortem debugging)。其技术原理是修改注册表,更改Windows对程序崩溃的默认行为。具体细节请参考“Auto Memory Dump on Crash of an Application”和我制作的注册表文件aedebug.reg

    在某个周五,我发现一个Windows服务程序崩溃,生成了内存转储文件。于是,我用Windbg打开该转储文件,做事后调试。由于这是一个用C#编写的托管程序,我加载了调试扩展项sossosex

    初步的调查看似没有提供任何信息。

    1. !threads显示这个程序有17个托管线程,但是没有一个线程拥有导致崩溃的异常。
    2. 利用!dumpheap搜索托管堆,没有发现导致崩溃的异常。
    3. !eestack没有提供有价值的信息。
    4. !analyze –v 没有提供有价值的信息。

    我感到有些束手无策,不过一个有经验的开发人员帮助了我。他用~*k列出所有线程栈,仔细查看。后来我才意识这其中的道理:托管程序的崩溃可能发生在托管栈帧(managed stack frame)中,也可能发生在非托管栈帧(unmanaged stack frame)中。如果托管代码看上去没问题,那么崩溃很可能发生在非托管代码中。

    一段时间后,他发现了导致崩溃的栈。利用!clrstack查看托管栈,一切正常。

    0:013> !clrstack
    OS Thread Id: 0x2630 (13)
    Child-SP         RetAddr          Call Site
    000000001d3cd2e0 000007fef48dd8ec System.Data.SqlClient.SqlCachedStream.get_TotalLength()
    000000001d3cd330 000007fef723a453 System.Data.SqlTypes.SqlXmlStreamWrapper.get_Length()
    000000001d3cd370 000007fef74a12d8 System.Xml.XmlReader.CalcBufferSize(System.IO.Stream)
    000000001d3cd3b0 000007fef980d502 System.Xml.XmlReader.CreateSqlReader(System.IO.Stream,
            System.Xml.XmlReaderSettings, System.Xml.XmlParserContext)
    000000001d3cdcf0 000007fef892ae46 System.Reflection.RuntimeMethodInfo.Invoke(System.Object,
            System.Reflection.BindingFlags, System.Reflection.Binder, System.Object[], System.Globalization.CultureInfo, Boolean)
    000000001d3cde90 000007fef4704211 System.Reflection.RuntimeMethodInfo.Invoke(System.Object,
            System.Reflection.BindingFlags, System.Reflection.Binder, System.Object[], System.Globalization.CultureInfo)
    000000001d3cdee0 000007fef4703fc2 System.Data.SqlTypes.SqlXml.CreateReader()
    000000001d3cdf60 000007fef4a010db System.Data.SqlTypes.SqlXml.get_Value()
    000000001d3cdfb0 000007fef43fe396 System.Data.SqlClient.SqlBuffer.get_Value()
    000000001d3ce080 000007fef43fe5a2 System.Data.SqlClient.SqlDataReader.GetValueInternal(Int32)
    000000001d3ce0d0 000007fef4419d37 System.Data.SqlClient.SqlDataReader.GetValues(System.Object[])
    000000001d3ce150 000007fef4419c47 System.Data.ProviderBase.DataReaderContainer
            +CommonLanguageSubsetDataReader.GetValues(System.Object[])
    000000001d3ce180 000007fef4411883 System.Data.ProviderBase.SchemaMapping.LoadDataRow()
    000000001d3ce1d0 000007fef4411727 System.Data.Common.DataAdapter.FillLoadDataRow(System.Data.ProviderBase.SchemaMapping)
    000000001d3ce260 000007fef4411584 System.Data.Common.DataAdapter.FillFromReader
            (System.Data.DataSet, System.Data.DataTable, System.String,
            System.Data.ProviderBase.DataReaderContainer, Int32, Int32, System.Data.DataColumn, System.Object)
    000000001d3ce330 000007fef441266d System.Data.Common.DataAdapter.Fill(System.Data.DataSet,
            System.String, System.Data.IDataReader, Int32, Int32)
    000000001d3ce3f0 000007fef44124fd System.Data.Common.DbDataAdapter.FillInternal
            (System.Data.DataSet, System.Data.DataTable[], Int32, Int32, System.String, 
            System.Data.IDbCommand, System.Data.CommandBehavior)
    000000001d3ce4b0 000007fef4412266 System.Data.Common.DbDataAdapter.Fill(System.Data.DataSet, Int32,
            Int32, System.String, System.Data.IDbCommand, System.Data.CommandBehavior)
    000000001d3ce560 000007ff001ab365 System.Data.Common.DbDataAdapter.Fill(System.Data.DataSet)
    000000001d3ce5f0 000007ff001aaca3 MyApp.DAL.DatabaseHelper.ExecuteDataSet(System.String, System.Data.CommandType)
    000000001d3ce660 000007ff001a99ad MyApp.DAL.ReportDAO.GetPendingAndRunningReportJobs()
    000000001d3ce6c0 000007fef88cdd38 MyApp.Report.CheckJobStatus(System.Object)
    000000001d3ce930 000007fef980d502 System.Threading.ExecutionContext.runTryCode(System.Object)
    000000001d3cf1e0 000007fef890eb56 System.Threading.ExecutionContext.Run
            (System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object)
    000000001d3cf230 000007fef980d502 System.Threading._TimerCallback.PerformTimerCallback(System.Object)

    但是,用k显示非托管栈帧则发现了问题。

    0:013> k
    Child-SP          RetAddr           Call Site
    00000000`1d3cb648 000007fe`fcf113a6 ntdll!NtWaitForMultipleObjects+0xa
    00000000`1d3cb650 00000000`76b3f190 KERNELBASE!WaitForMultipleObjectsEx+0xe8
    00000000`1d3cb750 000007fe`f9b303c9 kernel32!WaitForMultipleObjects+0xb0
    00000000`1d3cb7e0 000007fe`f9a2c553 mscorwks!Debugger::EnsureDebuggerAttached+0xe9
    00000000`1d3cb870 000007fe`f97e0563 mscorwks!`string'+0x842b3
    00000000`1d3cb8e0 000007fe`f9c0d9de mscorwks!EEPolicy::LogFatalError+0x1af
    00000000`1d3cc060 000007fe`f964fa55 mscorwks!EEPolicy::HandleFatalError+0x6e
    00000000`1d3cc0b0 000007fe`f9752cac mscorwks!CLRVectoredExceptionHandlerPhase3+0xcd
    00000000`1d3cc0f0 000007fe`f9752c33 mscorwks!CLRVectoredExceptionHandlerPhase2+0x30
    00000000`1d3cc160 000007fe`f964f686 mscorwks!CLRVectoredExceptionHandler+0xff
    00000000`1d3cc1e0 00000000`76d68a8f mscorwks!CLRVectoredExceptionHandlerShim+0x42
    00000000`1d3cc220 00000000`76d659b2 ntdll!RtlpCallVectoredHandlers+0xa8
    00000000`1d3cc290 00000000`76d9fe48 ntdll!RtlDispatchException+0x22
    00000000`1d3cc970 000007fe`f97a904e ntdll!KiUserExceptionDispatcher+0x2e
    00000000`1d3ccf30 000007fe`f96e3401 mscorwks!ArrayClass::GetApproxArrayElementTypeHandle+0x1a
    00000000`1d3ccf70 000007fe`f967a908 mscorwks!ArrayBase::GetArrayElementTypeHandle+0x61
    00000000`1d3ccfa0 000007fe`f99e3e95 mscorwks!ArrayBase::GetTypeHandle+0x18
    00000000`1d3ccff0 000007fe`f967e79d mscorwks!`string'+0x3bbf5
    00000000`1d3cd020 000007fe`f9be7936 mscorwks!ObjIsInstanceOf+0x41
    00000000`1d3cd0b0 000007fe`f9a2ab9c mscorwks!JITutil_ChkCastAny+0xe6
    00000000`1d3cd290 000007fe`f48b2a5e mscorwks!`string'+0x828fc
    00000000`1d3cd2e0 000007fe`f48dd8ec System_Data_ni!System.Data.SqlClient.SqlCachedStream.get_TotalLength()+0x4e
    00000000`1d3cd330 000007fe`f723a453 System_Data_ni!System.Data.SqlTypes.SqlXmlStreamWrapper.get_Length()+0x3c
    ...

    托管栈的栈顶函数是ADO.NET的SqlCachedStream.get_TotalLength。非托管栈显示它调用即时编译(JIT)相关的函数JITutil_ChkCastAny,似乎在坚持对象的类型转换(cast)。不幸的是,该检查触发了内核异常分派函数KiUserExceptionDispatcher,该函数回调CLR的异常处理函数CLRVectoredExceptionHandler。CLR认为此异常是致命错误(Fatal),程序无法继续运行,于是崩溃。

    值得一提的是,sosex提供的调试命令!mk可以同时查看托管栈帧与非托管栈帧,对于调试很有帮助。在如下输出中,M标记了托管栈帧,U标记了非托管栈帧。

    0:013> !mk
    Thread 13:
         ESP              EIP
    00:U 000000001d3cb648 0000000076da046a ntdll!NtWaitForMultipleObjects+0xa
    01:U 000000001d3cb650 000007fefcf113a6 KERNELBASE!WaitForMultipleObjectsEx+0xe8
    02:U 000000001d3cb750 0000000076b3f190 kernel32!WaitForMultipleObjects+0xb0
    03:U 000000001d3cb7e0 000007fef9b303c9 mscorwks!Debugger::EnsureDebuggerAttached+0xe9
    04:U 000000001d3cb870 000007fef9a2c553 mscorwks!`string'+0x842b3
    05:U 000000001d3cb8e0 000007fef97e0563 mscorwks!EEPolicy::LogFatalError+0x1af
    06:U 000000001d3cc060 000007fef9c0d9de mscorwks!EEPolicy::HandleFatalError+0x6e
    07:U 000000001d3cc0b0 000007fef964fa55 mscorwks!CLRVectoredExceptionHandlerPhase3+0xcd
    08:U 000000001d3cc0f0 000007fef9752cac mscorwks!CLRVectoredExceptionHandlerPhase2+0x30
    09:U 000000001d3cc160 000007fef9752c33 mscorwks!CLRVectoredExceptionHandler+0xff
    0a:U 000000001d3cc1e0 000007fef964f686 mscorwks!CLRVectoredExceptionHandlerShim+0x42
    0b:U 000000001d3cc220 0000000076d68a8f ntdll!RtlpCallVectoredHandlers+0xa8
    0c:U 000000001d3cc290 0000000076d659b2 ntdll!RtlDispatchException+0x22
    0d:U 000000001d3cc970 0000000076d9fe48 ntdll!KiUserExceptionDispatcher+0x2e
    0e:U 000000001d3ccf30 000007fef97a904e mscorwks!ArrayClass::GetApproxArrayElementTypeHandle+0x1a
    0f:U 000000001d3ccf70 000007fef96e3401 mscorwks!ArrayBase::GetArrayElementTypeHandle+0x61
    10:U 000000001d3ccfa0 000007fef967a908 mscorwks!ArrayBase::GetTypeHandle+0x18
    11:U 000000001d3ccff0 000007fef99e3e95 mscorwks!`string'+0x3bbf5
    12:U 000000001d3cd020 000007fef967e79d mscorwks!ObjIsInstanceOf+0x41
    13:U 000000001d3cd0b0 000007fef9be7936 mscorwks!JITutil_ChkCastAny+0xe6
    14:U 000000001d3cd290 000007fef9a2ab9c mscorwks!`string'+0x828fc
    15:M 000000001d3cd2e0 000007fef48b2a5e System.Data.SqlClient.SqlCachedStream.get_TotalLength()(+0x2d IL)(+0x4e Native)
    16:M 000000001d3cd330 000007fef48dd8ec System.Data.SqlTypes.SqlXmlStreamWrapper.get_Length()(+0x0 IL)(+0x3c Native)
    ...

    基于以上分析,似乎是函数SqlCachedStream.get_TotalLength出了问题。这是ADO.NET的函数,难以获得源代码。但是,可以利用Reflector.exe,用“反编译”的方式查看代码。

    image

    由源代码可知,该函数对this._catchedBytes的每一个元素做类型转换。于是进一步调查this._catchedBytes的内容。先用!dso在栈上找到SqlCachedStream对象,然后用!do查看其成员。

    0:013> !dso
    OS Thread Id: 0x2630 (13)
    RSP/REG          Object           Name
    000000001d3cba20 00000000399675d8 System.Threading.ExecutionContext
    000000001d3cbd50 0000000039abd0b0 System.Data.SqlTypes.SqlXmlStreamWrapper
    000000001d3cbd60 0000000039abd0f8 System.Xml.XmlReaderSettings
    ...
    000000001d3cd1d8 0000000039abd0b0 System.Data.SqlTypes.SqlXmlStreamWrapper
    000000001d3cd1f8 0000000001c29d58 System.String
    000000001d3cd260 0000000039abd0b0 System.Data.SqlTypes.SqlXmlStreamWrapper
    000000001d3cd270 0000000002aae5f8 <unknown type>
    000000001d3cd2e0 0000000039ab9960 System.Data.SqlClient.SqlCachedStream
    ...

    0:013> !do 0000000039ab9960
    Name: System.Data.SqlClient.SqlCachedStream
    MethodTable: 000007fef4977ce0
    EEClass: 000007fef4301f70
    Size: 80(0x50) bytes
    (C:\Windows\assembly\GAC_64\System.Data\2.0.0.0__b77a5c561934e089\System.Data.dll)
    Fields:
                  MT    Field   Offset                 Type VT     Attr            Value Name
    000007fef89ee580  400018a        8        System.Object  0 instance 0000000000000000 __identity
    000007fef91ec3a8  4001bbe       10 ...ream+ReadDelegate  0 instance 0000000000000000 _readDelegate
    000007fef91ec4b0  4001bbf       18 ...eam+WriteDelegate  0 instance 0000000000000000 _writeDelegate
    000007fef8a32d68  4001bc0       20 ...ng.AutoResetEvent  0 instance 0000000000000000
        _asyncActiveEvent
    000007fef89f5f00  4001bc1       28         System.Int32  1 instance                1
        _asyncActiveCount
    000007fef89f1c40  4001bbd      ae8     System.IO.Stream  0   shared           static Null
    000007fef89f5f00  400196a       2c         System.Int32  1 instance                0
        _currentPosition
    000007fef89f5f00  400196b       40         System.Int32  1 instance                0
        _currentArrayIndex
    000007fef89f5b78  400196c       30 ...ections.ArrayList  0 instance 0000000002cd40a0 _cachedBytes
    000007fef89f21b0  400196d       38         System.Int64  1 instance 0 _totalLength

    0:013> !do 0000000002cd40a0
    Name: System.Collections.ArrayList
    MethodTable: 000007fef89f5b78
    EEClass: 000007fef85fe9e8
    Size: 40(0x28) bytes
    (C:\Windows\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
    Fields:
                  MT    Field   Offset                 Type VT     Attr            Value Name
    000007fef89e4748  400094c        8      System.Object[]  0 instance  
    00000000111c9830 _items
    000007fef89f5f00  400094d       18         System.Int32  1 instance            19523 _size
    000007fef89f5f00  400094e       1c         System.Int32  1 instance            19523 _version
    000007fef89ee580  400094f       10        System.Object  0 instance 0000000000000000 _syncRoot
    000007fef89e4748  4000950      388      System.Object[]  0   shared           static emptyArray

    由Windbg的输出可知,this._catchedBytes是一个ArraryList,其元素都保持在数组变量_items中。于是查看该数组。

    0:013> !do 00000000111c9830
    Name: System.Object[]
    MethodTable: 000007fef89e4748
    EEClass: 000007fef85fb660
    Size: 262176(0x40020) bytes
    Array: Rank 1, Number of elements 32768, Type CLASS
    Element Type: System.Object
    Fields:
    None

    该数组可容纳32768个元素,当前已容纳26217个元素。那么是哪个元素的类型转换触发了异常呢?用!dumparray查看数组内容。

    0:013> !DumpArray -details 00000000111c9830

    输出的内容很长,可用.logopen将其保持在文本日志中。我删除了日志开始处的一些文本,获得一个5M大小的文本文件,其内容如下。

    image

    然后,我编写了一个Python脚本对该文本文件进行分析,以发现有问题的元素。

    lines = open('windbg.log').readlines()
    start = 1
    step = 9
    for i in range(26217):
        lineno = start + step * i
        if 'Name: System.Byte[]' not in lines[lineno]:
            print 'error (index:%i, line:%i): %s' % (i, lineno, lines[lineno])
            break

    该Python脚本发现第7736号元素有问题。

    [7735] 0000000002aadde0
        Name: System.Byte[]
        MethodTable: 000007fef89f6cd0
        EEClass: 000007fef85ff188
        Size: 2072(0x818) bytes
        Array: Rank 1, Number of elements 2048, Type Byte
        Element Type: System.Byte
        Fields:
        None
    [7736] 0000000002aae5f8
        Free Object
        Size 8088(0x1f98) bytes
    [7737] 0000000002aaed48
        Name: System.Byte[]
        MethodTable: 000007fef89f6cd0
        EEClass: 000007fef85ff188
        Size: 2072(0x818) bytes
        Array: Rank 1, Number of elements 2048, Type Byte
        Element Type: System.Byte
        Fields:
        None

    用dc查看第7735号和第7737号对象所在内存,可知它们都是字符串片段。也就是说_catchedBytes是一个字符串缓存。但是缓冲区的第7736号对象已经被垃圾回收,成为自由对象(Free Object)。当程序依次处理缓冲区的对象时,该自由对象导致了程序崩溃。

    0:013> dc 0000000002aadde0
    00000000`02aadde0  f89f6cd0 000007fe 00000800 00000000  .l..............
    00000000`02aaddf0  0066002c 00630061 00730074 00730020  ,.f.a.c.t.s. .s.
    00000000`02aade00  00610070 00650063 0066002c 00630061  p.a.c.e.,.f.a.c.
    00000000`02aade10  00730074 00730020 00610074 00690074  t.s. .s.t.a.t.i.
    00000000`02aade20  00740073 00630069 002c0073 00610066  s.t.i.c.s.,.f.a.
    00000000`02aade30  00740063 00200073 00740073 00610072  c.t.s. .s.t.r.a.
    00000000`02aade40  0067006e 002c0065 00610066 00740063  n.g.e.,.f.a.c.t.
    00000000`02aade50  00200073 00790073 00740073 006d0065  s. .s.y.s.t.e.m.

    0:013> dc 0000000002aaed48
    00000000`02aaed48  f89f6cd0 000007fe 00000800 00000000  .l..............
    00000000`02aaed58  00610066 00720069 00700020 006f0068  f.a.i.r. .p.h.o.
    00000000`02aaed68  006f0074 002c0073 00610066 00720069  t.o.s.,.f.a.i.r.
    00000000`02aaed78  00700020 006f0072 0065006a 00740063   .p.r.o.j.e.c.t.
    00000000`02aaed88  0066002c 00690061 00200072 00720070  ,.f.a.i.r. .p.r.
    00000000`02aaed98  006a006f 00630065 00200074 00640069  o.j.e.c.t. .i.d.
    00000000`02aaeda8  00610065 002c0073 00610066 00720069  e.a.s.,.f.a.i.r.
    00000000`02aaedb8  00700020 006f0072 0065006a 00740063   .p.r.o.j.e.c.t.

    为什么在一个缓存区中会存在被释放的对象?我们推测,这很可能是垃圾回收漏洞(GC hole),是垃圾回收器错误地释放了被使用的内存。于是我们向CLR支持团队(CLR Support Team)发邮件,描述了我们遇到的问题。对方的答复是:我们知道在垃圾回收过程中存在缺陷,但是我们尚未修复。没有修复的原因之一是该问题极难复现(repro)。

    经过漫长的分析,终于找到错误的根源。暂且打住,总结一下收获。

    1. 托管程序的错误可能发生在托管栈帧中(利用!clrstack查看),也可能发生在非托管栈帧中(利用k查看)。
    2. 调试时,如果托管栈(帧)和托管内存看上去没有问题,那么问题很可能在非托管栈(帧)和非托管内存中。
    3. sosex的!mk可以同时查看托管栈帧和非托管栈帧,对调试很有帮助。
    4. Reflector.exe可以查看.NET Framework Library的代码,有助于检查.NET程序的底层代码。
    5. 有时需要对Windbg的输出进行自动解析,以快速发现调试线索。
    6. CLR和.NET Framework Library都可能存在缺陷。
  • 相关阅读:
    go 基本包
    go 包
    算法笔记--数据结构--链表
    算法笔记--数据结构--队列
    算法笔记--标准模板库STL--pair
    算法笔记--标准模板库STL--stack
    算法笔记--标准模板库STL--priority_queue
    算法笔记--标准模板库STL--queue
    初识pair
    lower_bound实现离散化
  • 原文地址:https://www.cnblogs.com/liangshi/p/1818554.html
Copyright © 2011-2022 走看看