wenmo8 发布的文章

简介

本文介绍了在Windows中运行的VisualC++程序中处理异常和错误的标准技术。
异常(或严重错误或崩溃)通常意味着程序停止正常工作,需要停止执行。例如,由于程序访问无效的内存地址(如空指针)、无法分配内存缓冲区(内存不足)、C运行时库(CRT)检测到错误并请求程序终止等,可能会发生异常。
C++程序可以处理几种例外:SEH异常,通过操作系统的结构化异常处理机制产生,由C运行库产生的CRT错误,最后是信号。每种错误类型都需要安装异常处理程序函数,该函数将截获异常并执行一些错误恢复操作。
如果应用程序有多个执行线程,事情可能会更复杂。有些异常处理程序可用于整个进程,但有些仅用于当前线程。所以必须在每个线程中安装异常处理程序。
应用程序中的每个模块(EXE或DLL)都链接到CRT库(静态或动态)。异常处理技术在很大程度上依赖于CRT链接类型。
错误类型的多样性、在多线程程序中处理异常的差异,以及异常处理对CRT链接的依赖性,需要大量的工作来处理应用程序允许处理的所有异常。本文旨在帮助您更好地理解异常处理机制,并在C++应用程序中有效地使用异常处理。
本文附带了一个小型控制台演示应用程序ExceptionHandler。演示程序可以引发和捕获不同类型的异常,并生成一个崩溃小型转储文件,允许查看发生异常的代码行。

背景

不久前,我需要一种方法来拦截我的一个开源项目CrashRpt的异常,CrashRpt是一个用于Windows应用程序的崩溃报告库。CrashRpt库处理应用程序中发生的异常,收集有关错误的技术信息(如崩溃小型转储、错误日志、桌面截图),并提供用户通过Internet发送错误报告(图1)。

图1-CrashRpt库的错误报告窗口和错误报告详细信息对话框

也许您已经看到Windows错误报告窗口(图2)突然出现在您的桌面上,CrashRpt库也做了同样的事情,只是它将错误报告发送到您自己的web服务器,而不是Microsoft的服务器。

浏览MSDN时,我得到了SetUnhandledExceptionFilter()函数,该函数用于处理访问冲突。但很快我发现我的应用程序中的一些异常不知怎么地没有被处理,而Watson博士的窗口仍然出现,而不是崩溃的窗口。我又浏览了MSDN,发现许多其他CRT提供的函数可以用来处理CRT错误。下面是此类函数的一些示例:set_terminate(),_set_invalid_parameter_handler(),_set_purecall_handler()。然后我发现有些CRT处理程序只对当前线程有效,但有些处理程序对进程的所有线程都有效。继续我的研究,我发现开发人员必须理解许多细微差别才能有效地使用异常处理。我的研究结果如下。

关于例外的几句话

如您所知,异常或严重错误通常意味着程序停止正常工作,需要停止其执行。例如,可能由于以下原因发生异常:

  • 程序访问无效的内存地址(例如空指针)
  • 无限递归导致堆栈溢出
  • 大数据块被写入一个小缓冲区
  • C++类的纯虚方法称为C++类。
  • 无法分配内存缓冲区(内存不足)
  • 无效参数传递给C++系统函数
  • C运行时库检测错误并请求程序终止

有两种例外,它们有不同的性质:SEH异常(结构化异常处理、SEH)和类型化C++异常。操作系统提供结构化异常处理机制(这意味着所有Windows应用程序都可以引发和处理SEH异常)。SEH例外最初是为C语言设计的,但它们也可以用在C++中。SEH异常是使用_try{}_except(){}构造处理的。程序的main()函数由这样的构造保护,因此默认情况下,所有未处理的SEH异常都会被捕获并调用Dr.Watson。SEH异常是VisualC++编译器特有的。如果编写可移植代码,则应使用#ifdef/#endif保护结构化异常处理构造。

下面是一个代码示例:

int* p = NULL;   //pointer to NULL
__try
{
//Guarded code *p = 13; //causes an access violation exception }
__except(EXCEPTION_EXECUTE_HANDLER)
//Here is exception filter expression {//Here is exception handler//Terminate program ExitProcess(1);
}

在WinDBG的Command窗口中, 默认情况下, 你输入的命令很容易和该命令以及附近命令的输出混杂在一起. 在寻找之前命令的结果的时候, 眼睛看起来会很累.

WinDBG提供了一个选项, 通过这个选项, 你可以把命令输入与输出的颜色进行设置.

选项名:

  • Prompt level command window text
  • Prompt level command windows text background

 

结果如下:

 


怎样打印某函数调用关系

命令功能适用范围
uf /c /D 地址 打印当前函数对其他函数的调用 用户态/内核态
# 函数名 起始地址 l长度 打印在某段地址范围内代码对该函数的引用 内核态/用户态

  1. 看清楚是何种异常导致的崩溃.
  2. 对齐symbol, 找到发生崩溃的函数名字, 以及对应的汇编代码和源代码.
  3. 列出callstack
  4. 检查callstack是否合理
  5. 检查发生崩溃的函数是否得到了正确的参数.
  6. 检查发生崩溃的函数使用的数据是否正确.
  7. 结合上面的信息, 构思来龙去脉, 然后用资料来证明, 或者反驳自己的猜想.
  8. 通过进一步的操作来获取更有意义的资料. 比如激活pageheap后, 重新抓取dump, 或者干脆进行Live debug