下面的示例展示如何使用 CrashRpt API 函数和结构在控制台 C++ 应用程序中启用崩溃报告支持。为了简单起见,我们使用控制台应用程序,但通常应用程序也可以是基于 WinAPI/MFC/ATL/WTL 的。

1.配置项目的生成设置

编译完 CrashRpt 后,需要修改你的项目设置。

1.1.包含目录和库目录

要让 Visual C++ 编译器和链接器知道 CrashRpt include 和 lib 文件的位置,请执行以下操作。

打开项目属性管理器窗口,多选解决方案中的所有项目,然后右键单击选择项。在出现的对话框中,打开配置属性 -> VC++ 目录。

  • 在 “Show directory for” 组合框中,选择 “Include files”,然后将 <CRASHRPT_HOME>/include 目录的路径添加到列表中。
  • 在 “Show directory for” 组合框中,选择 “Library files”,然后将 <CRASHRPT_HOME>/lib 目录的路径添加到列表中。

在这里,<CRASHRPT_HOME> 应该替换为你解压缩 CrashRpt 发行版到的目录的实际路径。

在开始在程序中使用 CrashRpt API 函数之前,不要忘记在代码开头包含 CrashRpt.h 头文件:

// Include CrashRpt header
#include "CrashRpt.h"

还需要将 CrashRpt 库文件添加到项目的输入库列表中。在“解决方案资源管理器”窗口中,右键单击项目并从上下文菜单中选择“属性项”。然后打开配置属性->链接器->输入->附加依赖项,然后添加 CrashRptLIB.lib

1.2. 使用 CRT 作为 Multi-Threaded DLL (/MD) 在 Release 配置

注意:MT 不需要配置此项。

重要的是,配置你的项目使用 C 运行时库(CRT)作为 Multi-threaded DLL (/MD) 进行 Release 配置。这是MSDN中推荐的方式。有关更多信息,请参见异常处理和CRT链接部分。

在“解决方案资源管理器”窗口中,右键单击项目并打开项目属性。然后选择配置属性->C/C++ ->代码生成。在 Runtime Library 字段中,选择 Multi-threaded DLL (/MD)。这应该对解决方案中的所有项目执行,以共享单个 CRT DLL。

1.3.对所有应用程序模块使用相同版本的 CRT

确保应用程序中存在的所有模块使用相同版本的 CRT。如果某些依赖模块是使用较旧版本的 CRT 编译的,则应该重新编译它们,以确保使用单一版本或 CRT DLL。

1.4.在 Release 配置中启用 Program Database (/Zi, /DEBUG)

为了能够从 crash minidump 中恢复堆栈跟踪,调试器需要应用程序的调试符号(PDB文件)。

启用 PDB 文件的生成的步骤是:

  1. 在“解决方案资源管理器”窗口中,右键单击项目并打开项目属性。然后选择 Configuration Properties->C/C++->General. 在 Debug Information Format 字段,选择 Program Database (/Zi)
  2. 选择配置属性- >链接器- >调试。在 Generate Debug Info 字段中,选择 Yes (/DEBUG)

应该对解决方案中支持程序数据库 (EXE, DLL)的所有项目执行步骤1和步骤2。

1.5.禁用帧指针遗漏优化

我们建议在发布版本配置中关闭省略帧指针(FPO)优化,因为这种优化并没有真正带来可观的收益,但是极大地使转储的分析复杂化:/Oy 编译器选项使使用调试器更加困难,因为编译器抑制了帧指针信息。此外,在 Visual Studio 2010 中,默认情况下该优化是禁用的。

禁用 省略帧指针(FPO )优化的步骤是:

  1. 在“解决方案资源管理器”窗口中,右键单击项目并打开项目属性。
  2. 选择配置属性 -> C/C++ ->优化。
  3. Omit Frame Pointers 字段,选择 No (/Oy-)

2.CrashRpt API 使用示例

首先创建一个控制台 Win32 应用程序并将其命名为 MyApp。然后按照上面配置项目的构建设置中所述配置 MyApp 应用程序。

让我们假设 MyApp 应用程序有两个线程。第一个线程,即应用程序线程,将是主线程。main() 函数将在这个线程中执行,与用户的交互也将在这个线程中执行。第二个线程是工作线程,通常用于在不阻塞应用程序线程的情况下完成一些耗时的计算工作。

MyApp 应用程序将创建一个日志文件,该文件将包含在崩溃时的错误报告中。在调试崩溃时,日志文件通常很有帮助。

让我们创建代码模板。

#include <windows.h>
#include <stdio.h>
#include <tchar.h>

FILE* g_hLog = NULL; // Global handle to the application log file

// The following function writes an entry to the log file
void log_write(LPCTSTR szFormat, ...)
{
  if (g_hLog == NULL) 
    return; // Log file seems to be closed

  va_list args;
  va_start(args); 
  _vftprintf_s(g_hLog, szFormat, args);
  fflush(g_hLog);
}

// Thread procedure
DWORD WINAPI ThreadProc(LPVOID lpParam)
{
  log_write(_T("Entering the thread proc\n"));

  // Define the infinite loop where some processing will be done 
  for(;;)
  {
    // There is a hidden error somewhere inside of the loop...
    int* p = NULL;
    *p = 13; // This results in Access Violation
  }    
   
  log_write(_T("Leaving the thread proc\n"));

  return 0;
}

int _tmain(int argc, _TCHAR* argv[])
{
  // Open log file
  errno_t err = _tfopen_s(&g_hLog, _T("log.txt"), _T("wt"));
  if(err!=0 || g_hLog==NULL)
  {
    _tprintf_s(_T("Error opening log.txt\n"));
    return 1; // Couldn't open log file
  }

  log_write(_T("Started successfully\n"));

  // Create the worker thread
  HANDLE hWorkingThread = CreateThread(NULL, 0, 
           ThreadProc, (LPVOID)NULL, 0, NULL);

  log_write(_T("Created working thread\n"));

  // There is a hidden error in the main() function
  // Call of _tprintf_s with NULL parameter
  TCHAR* szFormatString = NULL;
  _tprintf_s(szFormatString);

  // Wait until the worker thread is exited
  WaitForSingleObject(hWorkingThread, INFINITE);

  log_write(_T("Working thread has exited\n"));

  // Close the log file
  if(g_hLog!=NULL)
  {
    fclose(g_hLog);
    g_hLog = NULL;// Clean up handle
  }

  // Exit
  return 0;
}

我们故意插入了会在两个线程中引起异常的代码。在实际的程序中,这样的代码总是存在的,即使在非常仔细地测试应用程序时也是如此。

为了在应用程序中启用崩溃报告支持,我们需要在代码的开头包含 CrashRpt.h 头文件:

// Include CrashRpt Header 
#include "CrashRpt.h"

接下来,我们定义崩溃回调函数并将其命名为 CrashCallback()。崩溃发生时,CrashRpt 将调用崩溃回调函数,因此我们将能够关闭日志文件的句柄。有关崩溃回调的更多信息,请参见 PFNCRASHCALLBACK() 原型。

// Define the callback function that will be called on crash
int CALLBACK CrashCallback(CR_CRASH_CALLBACK_INFO* pInfo)
{  
  // The application has crashed!

  // Close the log file here
  // to ensure CrashRpt is able to include it into error report
  if(g_hLog!=NULL)
  {
    fclose(g_hLog);
    g_hLog = NULL;// Clean up handle
  }

  // Return CR_CB_DODEFAULT to generate error report
  return CR_CB_DODEFAULT;
}

因为我们这是一个多线程应用程序,所以需要使用一些 CrashRpt 函数来为工作线程设置异常处理程序。在本例中,我们分别使用 crInstallToCurrentThread2()crUninstallFromCurrentThread() 函数在线程过程开始时设置异常处理程序,并在线程过程结束时取消设置。

// Thread procedure
DWORD WINAPI ThreadProc(LPVOID lpParam)
{
  // Install exception handlers for this thread
  crInstallToCurrentThread2(0);

  ...

  // Unset exception handlers before exiting the thread
  crUninstallFromCurrentThread();    

  return 0;
}    

接下来,在 main() 函数开始时,我们初始化 CrashRpt 库,并在 crInstall() 函数的帮助下为整个过程安装异常处理程序,并通过 CR_INSTALL_INFO 结构体将配置信息传递给它。如果出现错误,我们可以使用 crGetLastErrorMsg() 函数检查最后一条错误消息。

int _tmain(int argc, _TCHAR* argv[])
{  
  // Define CrashRpt configuration parameters
  CR_INSTALL_INFO info;  
  memset(&info, 0, sizeof(CR_INSTALL_INFO));  
  info.cb = sizeof(CR_INSTALL_INFO);    
  info.pszAppName = _T("MyApp");  
  info.pszAppVersion = _T("1.0.0");  
  info.pszEmailSubject = _T("MyApp 1.0.0 Error Report");  
  info.pszEmailTo = _T("myapp_support@hotmail.com");    
  info.pszUrl = _T("http://myapp.com/tools/crashrpt.php");  
  info.uPriorities[CR_HTTP] = 3;  // First try send report over HTTP 
  info.uPriorities[CR_SMTP] = 2;  // Second try send report over SMTP  
  info.uPriorities[CR_SMAPI] = 1; // Third try send report over Simple MAPI    
  // Install all available exception handlers
  info.dwFlags |= CR_INST_ALL_POSSIBLE_HANDLERS;
  // Restart the app on crash 
  info.dwFlags |= CR_INST_APP_RESTART; 
  info.dwFlags |= CR_INST_SEND_QUEUED_REPORTS; 
  info.pszRestartCmdLine = _T("/restart");
  // Define the Privacy Policy URL 
  info.pszPrivacyPolicyURL = _T("http://myapp.com/privacypolicy.html"); 
  
  // Install crash reporting
  int nResult = crInstall(&info);    
  if(nResult!=0)  
  {    
    // Something goes wrong. Get error message.
    TCHAR szErrorMsg[512] = _T("");        
    crGetLastErrorMsg(szErrorMsg, 512);    
    _tprintf_s(_T("%s\n"), szErrorMsg);    
    return 1;
  } 

接下来,我们想通过调用 crSetCrashCallback() 来设置崩溃回调函数。

  // Set crash callback function
  crSetCrashCallback(CrashCallback, NULL);

接下来,我们想要将错误日志文件添加到崩溃报告中。我们使用 crAddFile2() 函数来实现这一点。

  // Add our log file to the error report
  crAddFile2(_T("log.txt"), NULL, _T("Log File"), CR_AF_MAKE_FILE_COPY);   

当应用程序崩溃时,我们可以在崩溃报告中包含屏幕截图。我们使用 crAddScreenshot2() 函数来实现这一点。

  // We want the screenshot of the entire desktop is to be added on crash
  crAddScreenshot2(CR_AS_VIRTUAL_SCREEN, 0);   

下面的代码将一个命名属性添加到崩溃描述 XML 文件中(请参阅 crAddProperty() 函数)。这个属性告诉终端用户的计算机上安装了什么图形适配器(为了简单起见,它是硬编码的,但是你通常使用 Windows 管理工具动态地确定适配器的模型)。

  // Add a named property that means what graphics adapter is
  // installed on end user's machine
  crAddProperty(_T("VideoCard"), _T("nVidia GeForce 8600 GTS"));

main() 函数的最后,我们使用 crUninstall() 函数来初始化 CrashRpt 并解除异常处理程序的设置。

  // Uninitialize CrashRpt before exiting the main function
  crUninstall();

下面是我们的完整代码:

#include <windows.h>
#include <stdio.h>
#include <tchar.h>
// Include CrashRpt Header 
#include "CrashRpt.h"

FILE* g_hLog = NULL; // Global handle to the application log file

// Define the callback function that will be called on crash
int CALLBACK CrashCallback(CR_CRASH_CALLBACK_INFO* pInfo)
{  
  // The application has crashed!

  // Close the log file here
  // to ensure CrashRpt is able to include it into error report
  if(g_hLog!=NULL)
  {
    fclose(g_hLog);
    g_hLog = NULL;// Clean up handle
  }

  // Return CR_CB_DODEFAULT to generate error report
  return CR_CB_DODEFAULT;
}

// The following function writes an entry to the log file
void log_write(LPCTSTR szFormat, ...)
{
  if (g_hLog == NULL) 
    return; // Log file seems to be closed

  va_list args; 
  va_start(args); 
  _vftprintf_s(g_hLog, szFormat, args);
  fflush(g_hLog);
}

// Thread procedure
DWORD WINAPI ThreadProc(LPVOID lpParam)
{
  // Install exception handlers for this thread
  crInstallToCurrentThread2(0);

  log_write(_T("Entering the thread proc\n"));

  // Define the infinite loop where some processing will be done 
  for(;;)
  {
    // There is a hidden error somewhere inside of the loop...
    int* p = NULL;
    *p = 13; // This results in Access Violation
  }    
   
  log_write(_T("Leaving the thread proc\n"));

  // Unset exception handlers before exiting the thread
  crUninstallFromCurrentThread();    

  return 0;
}

int _tmain(int argc, _TCHAR* argv[])
{  
  // Define CrashRpt configuration parameters
  CR_INSTALL_INFO info;  
  memset(&info, 0, sizeof(CR_INSTALL_INFO));  
  info.cb = sizeof(CR_INSTALL_INFO);    
  info.pszAppName = _T("MyApp");  
  info.pszAppVersion = _T("1.0.0");  
  info.pszEmailSubject = _T("MyApp 1.0.0 Error Report");  
  info.pszEmailTo = _T("myapp_support@hotmail.com");    
  info.pszUrl = _T("http://myapp.com/tools/crashrpt.php");  
  info.uPriorities[CR_HTTP] = 3;  // First try send report over HTTP 
  info.uPriorities[CR_SMTP] = 2;  // Second try send report over SMTP  
  info.uPriorities[CR_SMAPI] = 1; // Third try send report over Simple MAPI    
  // Install all available exception handlers
  info.dwFlags |= CR_INST_ALL_POSSIBLE_HANDLERS;
  // Restart the app on crash 
  info.dwFlags |= CR_INST_APP_RESTART; 
  info.dwFlags |= CR_INST_SEND_QUEUED_REPORTS; 
  info.pszRestartCmdLine = _T("/restart");
  // Define the Privacy Policy URL 
  info.pszPrivacyPolicyURL = _T("http://myapp.com/privacypolicy.html"); 
  
  // Install crash reporting
  int nResult = crInstall(&info);    
  if(nResult!=0)  
  {    
    // Something goes wrong. Get error message.
    TCHAR szErrorMsg[512] = _T("");        
    crGetLastErrorMsg(szErrorMsg, 512);    
    _tprintf_s(_T("%s\n"), szErrorMsg);    
    return 1;
  } 

  // Set crash callback function
  crSetCrashCallback(CrashCallback, NULL);

  // Add our log file to the error report
  crAddFile2(_T("log.txt"), NULL, _T("Log File"), CR_AF_MAKE_FILE_COPY);    

  // We want the screenshot of the entire desktop is to be added on crash
  crAddScreenshot2(CR_AS_VIRTUAL_SCREEN, 0);   

  // Add a named property that means what graphics adapter is
  // installed on user's machine
  crAddProperty(_T("VideoCard"), _T("nVidia GeForce 8600 GTS"));

  // The main code follows...

  // Open log file
  errno_t err = _tfopen_s(&g_hLog, _T("log.txt"), _T("wt"));
  if(err!=0 || g_hLog==NULL)
  {
    _tprintf_s(_T("Error opening log.txt\n"));
    return 1; // Couldn't open log file
  }

  log_write(_T("Started successfully\n"));

  // Create the worker thread
  HANDLE hWorkingThread = CreateThread(NULL, 0, 
           ThreadProc, (LPVOID)NULL, 0, NULL);

  log_write(_T("Created working thread\n"));

  // There is a hidden error in the main() function
  // Call of _tprintf_s with NULL parameter
  TCHAR* szFormatString = NULL;
  _tprintf_s(szFormatString);

  // Wait until the worker thread is exited
  WaitForSingleObject(hWorkingThread, INFINITE);

  log_write(_T("Working thread has exited\n"));

  // Close the log file
  if(g_hLog!=NULL)
  {
    fclose(g_hLog);
    g_hLog = NULL;// Clean up handle
  }

  // Uninitialize CrashRpt before exiting the main function
  crUninstall();

  // Exit
  return 0;
}

不要忘记添加 CrashRptXXXX.lib 文件到项目的输入库列表中(XXXX 是 CrashRpt 版本的占位符)。有关更多信息,请参见上篇文章,配置项目的生成设置。

在运行应用程序之前,你应该将以下文件放置到你的应用程序可执行文件所在的目录中:

  • CrashRptXXXX.dll (这里和下面的XXXX应该替换为CrashRpt的实际版本)
  • CrashSenderXXXX.exe
  • dbghelp.dll
  • crashrpt_lang.ini

复制文件后,运行应用程序。当错误发生时,你应该能够看到一个错误报告窗口。

3.国际化支持

当程序崩溃时,你看到的错误报告窗口默认语言应是英文的。CrashRpt 支持多语言用户界面。为了使 CrashRpt 正常工作,你应该将一个名为 crashrpt_lang.ini的有效语言文件放置到 CrashSender.exe 文件所在的目录中。

还可以选择使用 CR_INSTALL_INFO::pszLangFilePath 结构成员指定定制语言文件名。该语言文件是 UNICODE 格式的文本文档,具有 INI 扩展名。语言文件包含 CrashRpt 对话框使用的本地化字符串。

最简单的快速的办法就是复制 <CRASHRPT_HOME>/lang_files 目录下的 crashrpt_lang_ZH-CN.ini 文件中的内容到 crashrpt_lang.in 中即可。

标签: CrashRpt, CrashFix, 崩溃收集, 崩溃分析

添加新评论