Smart Testing

All about smart testing

Archive for the ‘Debugging’ Category

一次有教益的程序崩溃调试 (中)

with 2 comments

上一篇文章介绍了一个程序崩溃的调试过程。经过细致的分析,我们发现程序崩溃的原因是CLR的缺陷。那么,有没有可能修改产品代码,来避免崩溃呢?

检查崩溃线程的托管栈可知,产品代码是在查询数据库。

0:013> !clrstack
OS Thread Id: 0×2630 (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)

函数CheckJobStatus旨在检查数据库中Job的状态,它调用数据抽象层(DAL)的函数GetPendingAndRunningReportJobs。后者调用函数ExecuteDataSet,返回所有等待(pending)和运行(running)的Job。它们的代码如下。

public static DataSet GetPendingAndRunningReportJobs()
{
    //Status 0 = Not Generated, 1 = pending, 2 = Running
    const String query = "select * from report with(nolock) where [Status] in (0, 1, 2)";
    using (var db = new DatabaseHelper())
    {
        DataSet result = db.ExecuteDataSet(query);

        if (result != null && result.Tables.Count > 0 && result.Tables[0].Rows.Count > 0)
        {
            return result;
        }
        return null;
    }
}

public DataSet ExecuteDataSet(String query, CommandType commandtype)
{
    DbDataAdapter adapter = objFactory.CreateDataAdapter();
    objCommand.CommandText = query;
    objCommand.CommandType = commandtype;
    adapter.SelectCommand = objCommand;
    DataSet ds = new DataSet();
    ds.Locale = CultureInfo.InvariantCulture;
    try
    {
        adapter.Fill(ds);
        return ds;
    }
    catch(SqlException e)
    {
        throw TranslateException(e);
    }
    finally
    {
        objCommand.Parameters.Clear();
        adapter.Dispose();
    }
}

作为通用的辅助函数,ExecuteDataSet没有修改的空间。在GetPendingAndRunningReportJobs中,有一条语句是问题所在。

const String query = "select * from Job with(nolock) where [Status] in (0, 1, 2)";

该语句存在以下问题。

  1. 违反了基本的编程规则:不要在产品代码中使用非限定性查询"select * from …"。
  2. 在业务代码中硬编码查询,使得业务代码与持久层紧耦合。而非限定性查询使得耦合更加隐晦,为产品演化引入了风险。
  3. 非限定性查询返回了所有列。对于CheckJobStatus而言,许多列是不需要的。这样做引入了不必要的性能开销。
  4. 从托管栈可知,崩溃发生在读取XML数据的过程中。在表Job中,只有一列的数据类型是XML,它存放了Job的定义。然而CheckJobStatus并不需要Job的定义。也就是说,读取无用数据导致了程序崩溃。

我将以上情况反映给程序的开发人员。由于临近发布,我们决定不做大的修改,只是将查询字符串修改为:

const String query = "select Result, ReportId, Status from Job with(nolock) where [Status] in (0, 1, 2)";

开发人员改完代码后,提供了一份私有版本(private build)给我,让我在预发布环境中测试,以检查该修改确实能避免崩溃。所谓私有版本只是一个修改后的DLL(托管程序集),我用它替换了有问题的DLL,并重启了服务。

问题发现在周五上午。当我替换完DLL,已经是傍晚。考虑到该问题的重现可能需要几天的时间(在崩溃之前,服务运行了近一个星期),我待服务重启完毕,未作仔细检查,就去吃晚饭了。晚饭之后,我打算检查一下系统状态,就回家欢度周末。将Windbg附加到被测试服务,猛然发现有许多异常。原来开发人员有一个笔误,他在查询字符串中多写了一个逗号:

const String query = "select Result, ReportId, Status, from Job with(nolock) where [Status] in (0, 1, 2)";

恰恰是这个多余的逗号使得GetPendingAndRunningReportJobs的每一次执行都会抛出异常。这导致引发崩溃的代码路径(code path)无法被测试覆盖,验证私有版本的任务不能继续。

为什么GetPendingAndRunningReportJobs抛出异常,服务还能继续?这是因为开发人员注册了一个全局的异常处理函数,它吞没了所有的托管异常。虽然CheckJobStatus被定时调用,它所导致的异常并不会使服务崩溃。

我急忙再去找开发人员,发现他已经先我一步去欢度周末了。我原想回滚这次更新,但是他的私有版本中还含有另一个重要修正。如果回滚到旧版本,就不能利用周末的时间来压力测试这个修正了。于是,我决定保持当前状态,等到周一再继续测试。

回顾这一天的情况,我有如下思考。

  1. 要遵循基本的编程规则,例如不在产品代码中硬编码查询,不使用非限定性查询等。遵循这些规则可以提高正确性,可维护性,避免不必要的性能损失和莫名的崩溃。
  2. 不要吞没异常。特别是,不要在全局的缺陷处理函数中,不区分具体情况就吞没全部异常。Andrew Hunt和David Thomas说:要崩溃,不要破坏(trash)。当”不可能情况“(例如SQL查询抛出语法异常)出现时,程序应该崩溃。
  3. 如果确实要吞没异常,应该详细记录异常细节,并利用邮件、系统日志、管理员页面等手段通知运营和开发团队。
  4. 在替换私有版本后,应该立即做必要的测试。要尽快发现严重的问题。
  5. 周五傍晚是一个奇妙的时段,不要做高风险操作。

到了周一,我向开发人员反映问题,重新获得了一份私有版本的DLL。经过一周的压力测试,服务没有再崩溃。说明这次的修改是有效的。

Written by liangshi

September 7, 2010 at 1:31 pm

Posted in Debugging

一次有教益的程序崩溃调试 (上)

with one comment

在系统上线前夕,我们将所有的子系统部署在预发布环境中做集成测试。集成测试模拟真实的业务场景,不停断地使用真实数据测试组成系统的各个服务(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: 0×2630 (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+0×30
00000000`1d3cc160 000007fe`f964f686 mscorwks!CLRVectoredExceptionHandler+0xff
00000000`1d3cc1e0 00000000`76d68a8f mscorwks!CLRVectoredExceptionHandlerShim+0×42
00000000`1d3cc220 00000000`76d659b2 ntdll!RtlpCallVectoredHandlers+0xa8
00000000`1d3cc290 00000000`76d9fe48 ntdll!RtlDispatchException+0×22
00000000`1d3cc970 000007fe`f97a904e
ntdll!KiUserExceptionDispatcher+0x2e
00000000`1d3ccf30 000007fe`f96e3401 mscorwks!ArrayClass::GetApproxArrayElementTypeHandle+0x1a
00000000`1d3ccf70 000007fe`f967a908 mscorwks!ArrayBase::GetArrayElementTypeHandle+0×61
00000000`1d3ccfa0 000007fe`f99e3e95 mscorwks!ArrayBase::GetTypeHandle+0×18
00000000`1d3ccff0 000007fe`f967e79d mscorwks!`string’+0x3bbf5
00000000`1d3cd020 000007fe`f9be7936 mscorwks!ObjIsInstanceOf+0×41
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+0×30
09:U 000000001d3cc160 000007fef9752c33 mscorwks!CLRVectoredExceptionHandler+0xff
0a:U 000000001d3cc1e0 000007fef964f686 mscorwks!CLRVectoredExceptionHandlerShim+0×42
0b:U 000000001d3cc220 0000000076d68a8f ntdll!RtlpCallVectoredHandlers+0xa8
0c:U 000000001d3cc290 0000000076d659b2 ntdll!RtlDispatchException+0×22
0d:U 000000001d3cc970 0000000076d9fe48 ntdll!KiUserExceptionDispatcher+0x2e
0e:U 000000001d3ccf30 000007fef97a904e mscorwks!
                                       ArrayClass::GetApproxArrayElementTypeHandle+0x1a
0f:U 000000001d3ccf70 000007fef96e3401 mscorwks!ArrayBase::GetArrayElementTypeHandle+0×61
10:U 000000001d3ccfa0 000007fef967a908 mscorwks!ArrayBase::GetTypeHandle+0×18
11:U 000000001d3ccff0 000007fef99e3e95 mscorwks!`string’+0x3bbf5
12:U 000000001d3cd020 000007fef967e79d mscorwks!ObjIsInstanceOf+0×41
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()(+0×0 IL)(+0x3c Native)

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

image

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

0:013> !dso
OS Thread Id: 0×2630 (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(0×50) bytes
(C:WindowsassemblyGAC_64System.Data2.0.0.0__b77a5c561934e089System.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(0×28) bytes
(C:WindowsassemblyGAC_64mscorlib2.0.0.0__b77a5c561934e089mscorlib.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(0×40020) 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(0×818) 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(0×818) 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都可能存在缺陷。

Written by liangshi

September 4, 2010 at 6:06 am

Posted in Debugging

用Windbg调试Silverlight应用死锁

with one comment

测试一个Silverlight应用时,突然整个IE窗口失去响应(Not Responding)。这时,IE和内嵌的Silverlight应用不响应任何Windows事件,似乎只有杀死IE进程,才能进一步测试。但是,简单地杀死进程,很可能导致问题无法复现(repro)。于是,我将Windbg附加(attach)到IE进程上,做现场调试(live debugging)。

调试.NET程序,需要在Windbg中加载调试扩展项sos.dll。Silverlight的运行时(runtime)是CoreCLR,因此必须加载CoreCLR对应的sos.dll(不能使用.NET Framework自带的sos.dll)。获得正确sos.dll的方法是安装Silverlight Tools for Visual Studio(3.0, 4.0)。如果安装了正确的sos.dll,在Windbg中可用如下命令将其加载。

0:023> .loadby sos coreclr

对于任何一个Windows窗口程序,只有UI线程绘制用户界面。IE窗口失去响应,这暗示UI线程停止工作。于是用!threads命令查看所有线程。

0:023> !threads
ThreadCount:      9
UnstartedThread:  0
BackgroundThread: 7
PendingThread:    0
DeadThread:       2
Hosted Runtime:   yes
                                   PreEmptive   GC Alloc                Lock
       ID  OSID ThreadOBJ    State GC           Context       Domain   Count APT Exception
   4    1   608 0a7098b8   2000220 Enabled  1bf9090c:1bf915dc 109e7fb8     1 STA
  33    2  14e0 0a67fc78      b220 Enabled  00000000:00000000 0a567760     0 MTA (Finalizer)
  34    3   504 0a680b68      1220 Enabled  00000000:00000000 0a567760     0 Ukn
  35    4   46c 0e8b7798   1000220 Enabled  00000000:00000000 0a567760     0 Ukn (Threadpool Worker)
  39    5  2350 0a41bf60   3001220 Enabled  1bf5fe18:1bf615dc 109e7fb8     1 Ukn (Threadpool Worker)
XXXX    a       0a41c970   1011820 Enabled  00000000:00000000 0a567760     0 Ukn (Threadpool Worker)
XXXX    9       0a41a638   1011820 Enabled  00000000:00000000 0a567760     0 Ukn (Threadpool Worker)
  47    6  1618 0a41ce78   3001220 Enabled  1bf91700:1bf935dc 109e7fb8     0 Ukn (Threadpool Worker)
  42    8  2754 0a41c468   3001220 Enabled  1bf93710:1bf955dc 109e7fb8     0 Ukn (Threadpool Worker)

由于UI线程是STA线程,所以可以断定4号线程是UI线程。于是切换到4号线程,用!CLRStack命令检查其托管栈。

0:023> ~4s
eax=0000539e ebx=0244c8b8 ecx=0c4a19f4 edx=00000005 esi=00000001 edi=00000000
eip=776464f4 esp=0244c868 ebp=0244c904 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200246
ntdll!KiFastSystemCallRet:
776464f4 c3              ret

0:004> !CLRStack
OS Thread Id: 0×608 (4)
Child SP IP       Call Site
0244cac4 776464f4 [GCFrame: 0244cac4]
0244cbdc 776464f4 [HelperMethodFrame_1OBJ: 0244cbdc] System.Threading.Monitor.Enter
                  (System.Object)
0244cc24 024b117f MyApp.Common.Logger.Log(MyApp.GeneralServiceReference.Level,
                  System.String, MyApp.GeneralServiceReference.ClientLoggingAttributes)
0244cc84 024b0d82 MyApp.Common.UsersUsageTracer.Log(MyApp.Common.UsageCategoryType, 
                  MyApp.Common.Event, MyApp.Common.TraceLevel,
                  MyApp.Common.UsageParametes[])
0244cd18 067ddc3b MyApp.BookInfo.BookInfoPresenter.set_Book
                  (MyApp.BookServiceReference.BookInfo)

由托管栈可知,UI线程正在调用函数Monitor.Enter,以请求一把排他锁。由于UI线程始终无法获得这把锁,于是UI线程停止工作。申请锁的函数是Logger.Log。这是可以理解的:大多数日志类(Logger)都会在记录日志时,申请排它锁,以确保日志内容的多线程安全。那么这把挂起UI线程的锁是被谁持有呢?

CLR会为每一个被加锁的对象分配一个同步块(sync block),以记录必要的同步信息。用调试器命令!SyncBlk可以查看进程的所有同步块。

0:004> !SyncBlk
Index SyncBlock MonitorHeld Recursion Owning Thread Info  SyncBlock …
1364 1ae8c1a4            3         1 0a41bf60 2350  39   0c4a19f4 …

很幸运,进程只有一个同步块,它应该是挂起UI线程的“元凶”。现在,它被线程2350(操作系统线程号)所持有。于是切换到这个工作线程,用!CLRStack命令检查其托管栈。

0:004> ~~[2350]s
eax=00000001 ebx=15c4eb00 ecx=00000001 edx=00000000 esi=00000001 edi=00000000
eip=776464f4 esp=15c4eab0 ebp=15c4eb4c iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
ntdll!KiFastSystemCallRet:
776464f4 c3              ret

0:039> !CLRStack
Child SP IP       Call Site
15c4ed10 776464f4 [HelperMethodFrame_1OBJ: 15c4ed10] System.Threading.WaitHandle.WaitOneNative
                 
(System.Runtime.InteropServices.SafeHandle, UInt32, Boolean, Boolean)
15c4ed84 0e279584 System.Threading.WaitHandle.InternalWaitOne
                  (System.Runtime.InteropServices.SafeHandle, Int64, Boolean, Boolean)
15c4eda0 0e279552 System.Threading.WaitHandle.WaitOne(Int64, Boolean)
15c4edb4 0e279440 System.Threading.WaitHandle.WaitOne(Int32, Boolean)
15c4edc8 0e2794d0 System.Threading.WaitHandle.WaitOne()
15c4edd0 5e59223d System.Windows.Threading.Dispatcher.Invoke
                  (System.Windows.Threading.DispatcherPriority,
                  System.Delegate, System.Object[])
15c4edf0 5e5ed437 System.Windows.Threading.DispatcherSynchronizationContext.Send
                  (System.Threading.SendOrPostCallback, System.Object)
15c4ee04 5e638404 System.Net.Browser.AsyncHelper.BeginOnUI
                  (System.Threading.SendOrPostCallback, System.Object)
15c4ee18 5e6395a9 System.Net.Browser.BrowserHttpWebRequestCreator.Create(System.Uri)
15c4ee4c 0406764c System.ServiceModel.Channels.HttpChannelFactory.GetWebRequest
                  (System.ServiceModel.EndpointAddress, System.Uri, System.TimeSpan)

15c4f204 067dd666 MyApp.Services.GeneralServiceHandler.CallToServer(ServiceCallDelegate)
15c4f258 067dd50c MyApp.Services.GeneralServiceHandler.RetrieveLoggingProperties
                  (System.EventHandler`1<MyApp.GeneralServiceReference. 
                  RetrieveLoggingPropertiesCompletedEventArgs>)
15c4f270 067dd42b MyApp.Common.Logger.LoadLoggingProperties()

该工作线程调用函数Logger.LoadLoggingProperties。从函数名推断,该函数是加载日志的属性。从调用栈分析,它使用WCF(调用了名空间System.ServiceModel的类)以获得服务端数据。令人惊讶的是,WCF竟然调用函数Browser.AysncHelper.BeignOnUI,从而将计算任务分配给UI线程。而BeginOnUI调用函数Threading.Dispatcher.Invoke,这是一个对UI线程的同步调用:只有UI线程完成了对System.Delegate对象的调用,函数Invoke才返回。

于是,获知死锁的原因:

  1. UI线程请求一把锁,该锁被工作线程锁持有。UI线程等待工作线程释放锁。
  2. 工作线程同步调用UI线程,等待UI线程完成任务。
  3. UI线程与工作线程相互等待,形成死锁。

到目前为止,我不清楚为什么Silverlight中的WCF会同步调用UI线程。事实上,程序员只能用异步函数调用UI线程,为什么框架本身要同步函数调用UI线程?也许,这是Silverlight团队的一个特殊设计,用于解决更底层的问题。那么,如何解决当前的死锁问题?由于死锁是由函数LoadLoggingProperties引发的,于是检查其实现。

private static void LoadLoggingProperties()
{
    lock (loggingMsgs)
    {
        GeneralServiceHandler.RetrieveLoggingProperties(
            delegate(object sender, RetrieveLoggingPropertiesCompletedEventArgs e)
                {
                    loggingProperties = e.Error == null
                                            ? e.Result
                                            : new LoggingProperties
                                                  {
                                                      LevelThreshold = Level.Warning,
                                                      SendClientLogsToServer = true,
                                                      SendClientUsageLogsToServer = false
                                                  };
                    HandleLogMsgs(null);
                });
    }
}

坦白地说,我不喜欢使用匿名委托(anonymous delegate)的程序。它们虽然看上去很酷,但是往往有一些明显的缺点。

  1. 可测试性差。我无法直接测试匿名委托,这导致测试粒度的增大与测试复杂性的提高。
  2. 可理解性差。由于是“匿名”的,我无法使用“命名”这一强大的工具来展示程序的意图。
  3. 增加耦合性。匿名委托往往导致不同目标、不同抽象级别的代码混杂在一个函数中。

这次死锁的根本原因是不正确的加锁范围,诱因是匿名委托导致的耦合。函数LoadLoggingProperties完成了两件任务:(1) 调用WCF,获得服务端数据;(2) 利用服务端数据,更新客户端数据(loggingProperties)。只有任务(2)是需要加锁保护的。不幸的是,两个任务被匿名委托耦合为一个语句。程序员没有经过深思熟虑,就对整个语句加锁,导致执行任务(1)时引发死锁。

根据以上分析,不难获得解除死锁的方法:缩小加锁范围,将lock语句移入异步WCF调用的回调函数,从而只对任务(2)加锁。

对于这次调试过程,做一个小结:

  1. 不使用调试器,很可能会漏掉一些难以复现的缺陷。
  2. !SyncBlk可以查看所有的同步块,是调试死锁的好帮手。
  3. 了解应用的线程模型对于调试死锁有帮助。例如,UI线程是STA线程,Threading.Dispatcher.Invoke是同步调用等。
  4. 加锁范围要尽可能的小。这是一条基本原则,但是常常被程序员遗忘。
  5. 尽量避免对第三方代码加锁,因为你往往不知道它会执行什么操作。

Written by liangshi

August 21, 2010 at 4:10 pm

Posted in Debugging

用Windbg调试.NET程序的资源泄漏

leave a comment »

在产品环境中的一个Windows服务出现了异常情况。这是一个基于WCF的.NET程序,它向网络应用(Web Application)提供WCF服务,同时也调用其他WCF服务以完成任务。突然,它不能响应网络应用的WCF调用。在它的日志文件中,我发现如下异常记录:

System.Net.Sockets.SocketException: An operation on a socket could not be performed because the system lacked sufficient buffer space or because a queue was full.

该异常暗示进程耗尽了系统的socket资源。开发人员告诉我,以前他们曾经发现这个程序有句柄泄漏的情况,但是没能够修复,这次的问题很可能是句柄泄漏造成的。由于问题发生在产品环境,我没有权限进行现场调试(live debugging)。于是,我请运营团队的同事用Windbg生成该程序的内存转储文件(memory dump)。具体命令如下,其中wcf_service.exe是发生问题的程序。

c:\debuggers\windbg.exe -pn wcf_service.exe -c ".dump /ma /u C:wcf_service.dmp;qd"

在获得内存转储文件后,我用Windbg打开该文件,并加载Windbg调试扩展项Psscor2。Psscor2是调试扩展项SOS的增强版,在原有调试命令的基础上,又增加了一批实用的命令,是.NET程序调试的利器。

首先,调用!handle,检查句柄实用情况。

0:025>!handle
 
27604 Handles
Type             Count
None             9
Event            360
Section          55
File             16439
Directory        2
Mutant           6
Semaphore        64
Key              44
Token            10539
Thread           51
IoCompletion     5
Timer            5
KeyedEvent       1
TpWorkerFactory  24

该程序竟然拥有1万6千多个文件句柄(File)、1万多个令牌句柄(Token),果然存在严重的句柄泄漏。这些句柄对应了操作系统的本地资源(native resource),对于.NET程序,它们被称为非托管资源(unmanaged resource)。大多数.NET程序不直接向操作系统申请、释放非托管资源,它们通过调用.NET Framework Library的类来完成相应的操作。这些类大多实现了特殊的终结函数(Finalize函数)和Dispose惯用法。Finalize函数供CLR的Finalizer线程调用,Dispose函数供程序员调用,它们都会释放非托管资源(技术细节请参考《Effective C#》条款18:实现标准Dispose模式)。

实现了Finalize函数的对象被称为可终结对象。CLR(Common Language Runtime)在创建可终结对象时候,会在一个特殊的全局队列中保持它们的引用,这个队列被称为终结队列(Finalization Queue)。

于是,调用!FinalizeQueue来查看Finalization Queue,看看进程中有多少可终结对象。

0:025> !FinalizeQueue

SyncBlocks to be cleaned up: 0
MTA Interfaces to be released: 0
STA Interfaces to be released: 0
———————————-
generation 0 has 56 finalizable objects (0000000020099428->00000000200995e8)
generation 1 has 36 finalizable objects (0000000020099308->0000000020099428)
generation 2 has 98971 finalizable objects (000000001ffd7e30->0000000020099308)
Ready for finalization 0 objects (00000000200995e8->00000000200995e8)
Statistics:
        MT    Count    TotalSize Class Name
0x000007fef8dea958        1           24 System.Threading.OverlappedDataCache
0x000007fef8dd90c0        1           32 System.Security.Cryptography.SafeProvHandle
0x000007fef820e5d8        1           32 Microsoft.Win32.SafeHandles.SafeProcessHandle

0x000007fef8dbf730       16        1,664 System.Threading.Thread
0x000007fef81f7280       12        2,208 System.Net.Sockets.OverlappedAsyncResult
0x000007fef8db7a90      151        4,832 System.WeakReference
0x000007fef8db7b30      125        8,000 System.Threading.ReaderWriterLock
0x000007fef81efea0      254       14,224 System.Net.AsyncRequestContext
0x000007fef89994d8      256       30,720 System.Threading.OverlappedData
0x000007fef8dfa148      500       36,000 System.Reflection.Emit.DynamicResolver
0x000007fef8dd9298   10,539      337,248 Microsoft.Win32.SafeHandles.SafeTokenHandle
0x000007fef81f8730   16,341      522,912 System.Net.SafeCloseSocket+InnerSafeCloseSocket
0x000007fef81f93a8   16,339      653,560 System.Net.SafeCloseSocket
0x000007fef81f2850   16,362      785,376 System.Net.SafeFreeCredential_SECURITY
0x000007fef820d7a0    5,288      888,384 System.Diagnostics.PerformanceCounter
0x000007fef81f2be0   16,333      914,648 System.Net.SafeDeleteContext_SECURITY

0x000007fef81f84c8   16,339    1,960,680 System.Net.Sockets.Socket
Total 99,063 objects, Total size: 6,169,424

由输出可知,该程序拥有16个线程对象(线程也是非托管资源)、1万多个SafeTokenHandle对象、1万6千多个Socket对象。联系!handle的输出可以推知:SafeTokenHandle对象对应于非托管的令牌句柄(该程序对WCF调用实施Windows认证,这些令牌应该是认证所需要的安全令牌),Socket对象对应于非托管的文件句柄。这些对象的拥有者没有及时调用它们的Dispose方法,Finalizer线程也没有调用其Finalize方法,导致非托管资源没有得到及时地释放,终于导致socket资源耗尽。

那么是谁拥有这些对象呢?调用!dumpheap命令,获得所有Socket对象的地址。

0:025> !dumpheap -mt 0x000007fef81f84c8
Loading the heap objects into our cache.
         Address               MT     Size
0000000001200270 000007fef81f84c8      120    2 System.Net.Sockets.Socket

00000000012401e8 000007fef81f84c8      120    2 System.Net.Sockets.Socket
00000000012544d8 000007fef81f84c8      120    2 System.Net.Sockets.Socket

对于其中一个Socket对象,调用!gcroot命令,看看它是否被栈变量或全局变量所引用。

0:025> !gcroot 0000000001200270

DOMAIN(0000000000ABF100):HANDLE(Pinned):121790:Root:  00000000111c78e8(System.Object[])->
  00000000011f43e8(System.ServiceModel.ChannelFactoryRefCache`1[[IJobServiceServer, JobServiceClientProxy]])->
  000000000121b480(System.ServiceModel.ChannelFactoryRef`1[[IJobServiceServer, JobServiceClientProxy]])->
  00000000011f4578(System.ServiceModel.ChannelFactory`1[[IJobServiceServer, JobServiceClientProxy]])->
  00000000016f6710(System.ServiceModel.Channels.ServiceChannelFactory
+ServiceChannelFactoryOverDuplexSession)->
  0000000001672ea0(System.ServiceModel.Channels.TcpChannelFactory`1[[System.ServiceModel.Channels.IDuplexSessionChannel, System.ServiceModel]])->
  0000000001515978(System.ServiceModel.Channels.TcpConnectionPoolRegistry
+TcpConnectionPool)-> 
  … 
  00000000012097b8(System.ServiceModel.Channels.BufferedConnection)->
  0000000001209678(System.ServiceModel.Channels.SocketConnection)->
  0000000001200270(System.Net.Sockets.Socket)

这些对象被WCF的ChannelFactoryRefCache所引用。它是一个全局的缓存,保存了可用的WCF通道(channel)。这些通道所对应的WCF调用与JobService有关。JobService是一个WCF服务,出问题的程序要定时轮询该服务以获取计算结果。IJobServiceServer是JobService的客户端代理对象所实现的接口。于是用“IJobServiceServer”在源代码树上搜索,找到了代理对象的定义:

class JobServiceServerClient : System.ServiceModel.ClientBase<IJobServiceServer>, IJobServiceServer { …

JobServiceServerClient继承了WCF提供的ClientBase, 拥有非托管的socket资源,并实现了Finalize函数和Dispose函数。于是,搜索调用JobServiceServerClient的代码。很快,在源代码树上发现如下代码:

private static void CheckJobStatus(object state)
{
    JobServerClient client = new JobServerClient ();
    …

该函数被定时地调用,以轮询JobService的计算结果。不幸的是,每次调用都会产生非托管资源的泄露,积少成多以至于难以为继。当JobServiceServerClient的对象创建时,它会被加入ChannelFactoryRefCache。由于程序员忘记调用Dispose函数,该对象拥有的scoket资源没有被释放。忘记调用Dispose函数的另一个后果是,该对象没有从ChannelFactoryRefCache中移除。这使得该对象始终是可达对象(reachable object),垃圾回收器(Garbage Collector) 不会处理它,这导致Finalizer线程不会调用它的Finalize函数,使得socket资源始终得不到释放。关于垃圾回收和Finalizer线程的技术细节请参考《CLR via C#》(第3版,第21章)。

对于函数CheckJobStatus,正确的实作是利用C#的using语句,确保对象client在退出using作用域时,其Dispose函数被CLR调用。

private static void CheckJobStatus(object state)
{
    using(JobServerClient client = new JobServerClient ())
    {
    …

实际上,所有涉及非托管资源管理的文献,几乎都“严厉要求”正确地实现Dispose模式,并尽可能地利用using语句来确保Dispose函数被及时调用。这次遇到的问题就是违反了这条基本的资源管理规则。

回顾这次调试过程,可获得如下小结。

  1. 命令!handle可以查看本地资源(非托管资源)的句柄。
  2. 命令!FinalizeQueue可以查看终结队列,对于调试非托管资源泄漏很有帮助。
  3. 正确的实现Dispose模式并严格地使用using语句,可以避免非托管资源的泄露。

最后,介绍一个调试句柄泄漏的好工具ProceExp。以管理员权限启动该程序,选中目标进程,按下快捷键Ctrl+H(或点击View > Lower Pane View > Handles),可以在下方面板中看到该进程所拥有的全部句柄。此时,按下快捷键Ctrl + S (或点击 File > Save),可以将目标进程及其句柄信息保存为文本文件,以供深入分析。

Written by liangshi

August 15, 2010 at 9:44 am

Posted in Debugging

Follow

Get every new post delivered to your Inbox.