65.9K
CodeProject 正在变化。 阅读更多。
Home

自动转储收集和分析

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (22投票s)

2014年7月15日

CPOL

10分钟阅读

viewsIcon

37626

downloadIcon

682

自动转储收集和分析

引言

Windows 应用程序有时会因内部测试期间无法发现的不可预测的原因而在用户端崩溃。对于许多崩溃,很难找出其根本原因,因为它们不容易重现,或者客户不知道如何重现。

而且,如果一些崩溃不再重现,它们可能会在未报告的情况下被埋没。

然而,了解所有这些崩溃并自动找出其根本原因,将有助于我们改进产品并更深入地了解真实用户环境/用户案例。

在本文中,我将提供一个简单的解决方案,用于监视 Windows 应用程序崩溃,以及实现自动的转储生成、自动转储上传和分析。

解决方案全景图

图 1 是解决方案的全景图,我们引入了一个组件——产品监视器 (Product Monitor)。这是一个简单、健壮的应用程序,负责监视产品进程、收集和上传崩溃转储及其环境信息等。

遵循完整的工作流程

  1. 产品监视器为我们关心的进程设置 Windows 错误报告 (Windows Error Report),以便在它们崩溃时生成崩溃转储。
  2. 如果进程异常退出,产品监视器将收集崩溃转储以及其他现场信息,包括二进制信息。对于托管应用程序,这包括 .NET Framework 的 *SOS.dll* 和 *mscordacwks.dll*。
  3. 进程监视器将第 2 步中收集的数据上传到云服务。
  4. 云服务在产品符号数据库 (symbol DB) 中查询正确的产品符号(这对于 C++/C 等非托管应用程序非常重要)。
  5. 云服务分配一个分析工作进程 (analyze worker),并将收集到的数据与符号文件发送给它。分析工作进程是一台装有 Windbg/CDB 等调试/分析工具的 Windows 计算机。
  6. 分析工作进程启动 CDB 命令行来分析崩溃转储,并发送分析结果。

云服务是一个云计算平台,例如 Amazon EC2 或 Microsoft Azure,但在我的代码中,为了简化,我使用 WCF 应用程序充当云服务。

分析工作进程可以运行在虚拟机或物理机上。

图 1

解决方案实现

附带的源代码提供了一个简化的实现,它包含以下组件,如图 2 所示。

图 2

绿色框中的组件是从附带的源代码构建的,蓝色框中的是 Windows 组件/功能。

崩溃的程序

Client.exe 和 *NativeClient.exe* 是要被崩溃监视的进程。*Client.exe* 是用 C# 编写的托管程序,*NativeClient.exe* 是用 C++ 编写的非托管程序。为了演示目的,我使用内存访问冲突来使它们崩溃,以下是导致崩溃的代码。

C#

char[] charArr = new char[10];
charArr[10] = 'a';

C++

char* p = NULL;
*p = 'b';

Sensor.exe

Sensor.exe 是一个用 C++/ATL 编写的进程外 COM 服务器,它作为 NT 服务运行。它充当产品监视器的角色。我选择 NT 服务形式的进程外 COM 是出于以下考虑:

  1. 托管代码和非托管代码都非常容易集成。
  2. 集成与 .NET Framework 依赖项解耦。
  3. COM 提供强大的接口和版本控制。
  4. 使用 NT 服务 COM 服务器,我们只需要一个 *Sensor.exe* 实例来监视所有进程,这是轻量级的。

Sensor.exe 只提供一个接口 `IProcessSensor`,其中包含两个方法 - `RegisterProcess` 和 `UnregisterProcess`。

interface IProcessSensor : IDispatch{
    [id(1), helpstring("Register a process with its ID")] HRESULT RegisterProcess(LONG ProcessID);
    [id(2), helpstring("UnRegister a process with its ID")] HRESULT UnregisterProcess(LONG ProcessID);
};

`RegisterProcess` 使 *Sensor.exe* 能够监视进程是否发生崩溃。

`RegisterProcess` 停止 *Sensor.exe* 监视进程。

因此,如果一个进程希望被监视,对于 C# 来说,只需要添加一行代码。

new ProcessSensor().RegisterProcess(Process.GetCurrentProcess().Id);

对于 C++ 来说,需要更多一点的代码,这本质上是因为 C++ 对程序员来说比 C# 复杂得多。

::CoInitialize(NULL);
SensorLib::IProcessSensor* pSensor = NULL;
HRESULT hr = ::CoCreateInstance(__uuidof(SensorLib::ProcessSensor),
NULL,
CLSCTX_LOCAL_SERVER,
__uuidof(SensorLib::IProcessSensor),(void**)&pSensor);
pSensor->RegisterProcess(::GetCurrentProcessId());
pSensor->Release();
::CoUninitialize();

Sensor.exe 利用 WER (Windows Error Report) 来收集崩溃转储。当进程调用 `RegisterProcess` 时,*Sensor.exe* 将在注册表中配置 WER 项目,以启用该进程的转储生成。

有关 WER 崩溃设置的更多信息,请参阅此链接

注册后,Sensor 将进程句柄添加到受监视进程句柄数组中,并对该句柄数组调用 `WaitForMultipleObjects`。如果受监视的进程退出,`WaitForMultipleObjects` 将返回,然后 *Sensor.exe* 调用 `GetExitCodeProcess` 来获取已退出进程的退出代码。如果其退出代码不等于预定义的退出代码(即 0),*Sensor.exe* 将认为该进程异常退出,然后转到 WER 设置中定义的转储文件夹检查是否生成了崩溃转储。

监视逻辑实现在 `CProcessWatcher::WorkFunc` 中。实际上,除了等待这些进程句柄之外,`WaitForMultipleObjects` 还等待另外两个事件——`stop` 和 `New-Process-Registration`。

`Stop` 事件用于通知 *Sensor.exe* 即将停止,例如,用户在 *Services.msc* 中停止服务。

`New-Process-Registration` 用于通知一个新的进程已注册要被监视。在这种情况下,*Sensor.exe* 会停止 `WaitForMultipleObjects` 并将相关的进程句柄添加到进程句柄数组中,然后再次开始等待。

转储上传器

如果 *Sensor.exe* 检测到崩溃,它将启动 *DumpUploader.exe* 来收集和上传崩溃转储。

DumpUploader.exe 首先会检查崩溃程序的转储文件夹(在 WER 注册表中配置),找到最新的转储。如果崩溃程序是托管应用程序,它还会收集 *SOS.dll* 和 *mscordacwks.dll*,它们随 .NET Framework 一起分发。这是因为对于托管应用程序,如果其崩溃转储在与崩溃发生不同的机器上进行分析,则需要现场的 *SOS.dll* 和 *mscordacwks.dll*。

这两个 DLL 可以在 *%windir%\Microsoft.NET\Framework\vx.x.xxxx* 或 *%windir%\Microsoft.NET\Framework64\vx.x.xxxx* 中找到。它们的路径取决于崩溃程序的平台,也就是说,如果它是 x86,则路径是 *%windir%\Microsoft.NET\Framework\vx.x.xxxx*,而如果它是 x64 或 AnyCPU 并且您的 Windows 是 64 位,则路径是 *%windir%\Microsoft.NET\Framework64\vx.x.xxxx*。

如果崩溃的程序是原生应用程序(即用 C 或原生 C++ 编写),则不需要收集 *SOS.dll* 和 *mscordacwks.dll*。

DumpUploader.exe 能够分析崩溃进程的映像,推断它是托管的还是非托管的,并找出在托管的情况下在哪里可以找到 *SOS.dll* 和 *mscordacwks.dll*。

最后,*DumpUploader.exe* 将所有文件打包成一个 zip 文件(使用 *Ionic.zip*),并通过 WCF 服务上传到服务器端。

HostingService.exe

HostingService.exe 充当云计算平台,它托管一个 WCF 服务,该服务以 `NETWORK_SERVICE` 上下文下的 NT 服务身份运行。WCF 服务合同在 `PrivateChannel.Interface` 项目中定义,并在 `PrivateChannel` 项目中实现。

DumpUploader.exe 调用 `Upload()` 来上传转储包。

namespace PrivateChannel.Interface
{
    [MessageContract]
    public class UploadEvent
    {
        [MessageHeader]
        public string FileName { get; set; }

        [MessageHeader]
        public int Type { get; set; }

        [MessageBodyMember]
        public Stream FileData { get; set; }
    }

    [ServiceContract(Name="PrivateChannelServer",Namespace = "http://www.danhu.com")]
    public interface IChannelServer
    {
        [OperationContract(IsOneWay = true)]
        void Upload(UploadEvent e);
    }
}

在收到转储包后,*HostingService.exe* 将启动 *Analyzer.exe* 进行转储分析。

Analyzer.exe

Analyzer.exe 充当分析工作进程。它首先解压上传的 zip 文件(使用 *Ionic.zip*),然后检查转储类型(原生代码、托管代码),接着使用 `-z` 参数启动 *CDB.exe* 进行转储分析。

如果是托管应用程序的转储,它需要告诉 *CDB.exe* 加载上传的 *sos.dll* 和 *mscordacwks.dll*。假设这两个 DLL 放在 dmp 文件所在的同一文件夹中,下面的代码构造了 *CDB.exe* 的命令行参数。

string sosFile = Path.Combine(dmpPath, "SOS.dll");
string arguments = string.Format("-z {0} -y {1} -logo {2} -c \".load {3};.cordll
-ve -se -u -lp {4};!analyze -v;q\"",dmpFile, dmpPath, outputFile, sosFile, dmpPath);

组合后的命令行如下所示:

如果是原生应用程序的转储,*Analyzer.exe* 还需要从符号数据库检索符号文件,并构造另一个 CDB 命令行。

当前代码仅实现了托管应用程序的部分,原生应用程序转储分析的命令行要简单得多。

如您所见,分析报告将输出到一个 txt 文件。然后 *Analyzer.exe* 可以可视化分析报告,例如将其放在网页上,或者通过电子邮件发送给相关人员。我完全没有网页开发经验,所以在我提供的示例代码中,我选择了邮件通知。*Analyzer.exe.config* 包含了 CDB 路径和电子邮件设置。

在您的环境中尝试整个解决方案。

为了简单尝试,您只需要一台机器,它同时充当客户端和服务器。

客户端

  1. 将客户端组件放在一个文件夹中。

    如果您使用 Visual Studio 2010 构建解决方案,所有客户端组件都将放在 *%(SolutionDir)ClientDebug*。

  2. 在 *DumpUploader.exe.config* 中配置 WCF 服务终结点。

    由于我们将使用一台机器同时充当客户端和服务器,请将终结点 IP 设置为 127.0.0.1 或 localhost。但是,如果您想尝试跨不同机器进行部署,请在此处设置正确的 FQDN 或 IP。

  3. 安装 Sensor 服务

    请以管理员身份运行 *Sensor.exe /Service*(启动 *cmd.exe*,选择“以管理员身份运行”,然后执行 *Sensor.exe /Service*),之后您将在 *services.msc* 中看到 *Sensor.exe*。

服务器端

  1. 将服务器端组件放在一个文件夹中。

    如果您使用 Visual Studio 2010 构建解决方案,所有服务器端组件都将放在 *%(SolutionDir)ServerDebug*。

  2. 在 *Analyzer.exe.config* 中配置 CDB 路径和电子邮件设置。

  3. CDB 是 windbg 的命令行工具,如果您没有安装它,请先安装。您可以从 Microsoft 此处下载。

  4. 在 *HostingService.exe.config* 中配置 WCF 服务终结点地址。

    这应该与 *DumpUploader.exe.config* 中的值匹配。

  5. 安装 WCF 服务

    安装 WCF 服务的最简单方法是使用 *InstallUtil.exe*。此工具随 .NET Framework 一起分发,您可以在安装 .NET Framework 的地方找到它。在我的 Windows 2012 Standard(安装了 .NET 4)系统中,它的位置是:

    C:\Windows\Microsoft.NET\Framework64\v4.0.30319

    请执行 *InstallUti.exe /i C:\ServerDebug\HostingService.exe* 来安装 WCF 服务。安装后,您可以在 *services.msc* 中找到它。

启动 WCF 服务

如果您直接在 *services.msc* 中启动它,可能会遇到以下错误。

这意味着 `NETWORK_SERVICE` 账户没有权限在 localhost:8000 上启动 HTTP 服务器。因此,请先运行以下命令授予 `NETWORK_SERVICE` 该权限。此命令也需要由管理员执行。

netsh http add urlacl url=http://+:8000/ChannelService/ user="NETWORK SERVICE"

然后 Channel Service 应该可以成功启动。

可以开始尝试了!

回到客户端(实际上是在同一台机器上!),点击 *client.exe*,它会向 *Sensor.exe* 注册自己以被监视,然后输入任意键使其崩溃。崩溃转储将生成在 *C:\Windows\Temp\Client.exe*。

然后稍等片刻,直到打包和上传完成。

在服务器端,上传的 zip 文件将放置在 *C:\Windows\ServiceProfiles\NetworkService\AppData\Local\Temp\Trap-yyyy-mm-dd-hhmmss*,其中 yyyy-mm-dd-hhmmss 是包接收时间。

分析完成后,您可以在 *C:\Windows\ServiceProfiles\NetworkService\AppData\Local\Temp\Client.exe.3776*(3776 是崩溃进程的进程 ID)找到分析结果 *AnalyzeResult.txt*。

如果您启用了邮件通知,并且您的邮件服务器正常工作并且您是收件人,您还将收到分析结果的邮件,其中我们可以看到 *client.exe* 发生了“索引越界异常”。

备注

如果崩溃的应用程序是针对 .NET Framework 构建的(如 *client.exe*),请确保它是针对 .NET Framework 4.0 或更高版本构建的。

我的单机环境是 Windows 2012 Standard,我认为它应该可以在其他 Windows 2012 版本上运行。

我也尝试了跨机器部署,客户端是 Windows 7,服务器是 Windows 2012,它工作正常,但我的两台机器属于同一域。如果您想尝试跨不同域的机器进行部署,可能需要一些其他的 WCF 设置。

如何使用代码

此解决方案涉及多项 Microsoft 技术,包括 ATL/COM、NT 服务、WCF 编程、转储分析、PE 文件检查等。

Sensor 项目提供了编写 ATL 服务的示例。

`DumpUploader` 项目提供了 WCF 客户端及其方法的示例。

``static bool ManagedPECheck()`` 展示了如何获取托管应用程序的平台和 CLR 版本。

`PrivateChannel.Interface` 和 `PrivateChannel` 项目展示了如何定义和实现 WCF 服务合同。

HostingService 项目提供了在 Windows 服务中托管 WCF 服务的示例。

此解决方案可以使用 Visual Studio 2010 或 2012 构建。如果遇到构建问题,请尝试以“管理员身份运行”启动 Visual Studio。

如果 *client.exe* 构建失败,请先构建 *Sensor.exe*,然后以管理员身份执行 *Sensor.exe /Service*。

然后重新添加 COM 引用 `Sensorlib`。

关注点

我曾经遇到过通过 WCF 通道上传大文件的问题,但通过在 *HostingService.exe.config* 中进行以下设置,我已经解决了这个问题。

历史

  • 2014 年 7 月 15 日 初始发布
© . All rights reserved.