wenmo8 发布的文章

低层次思考,我指的是从应用程序内部思考的重要性,有时是在机器代码级别。

大多数人认为,要知道如何调试应用程序,只需要学习如何使用调试器。但事实上,学习如何使用调试器只是解决复杂软件问题所需的一部分。因此,我觉得有必要解释在处理应用程序问题(如挂起、崩溃、内存泄漏、应用程序错误和性能问题)时,低层思考是多么重要。

硬件断点的原理

Intel 80306以上的CPU给我们提供了调试寄存器用于软件调试,硬件断点是通过设置调试寄存器实现的。

 

上图为Intel手册提供的32位操作系统下8个调试寄存器的图示(Intel手册卷3 17章第二节 Debug Registers,有兴趣的朋友可以查阅),根据介绍,DR0-DR3为设置断点的地址,DR4和DR5为保留,DR6为调试异常产生后显示的一些信息,DR7保存了断点是否启用、断点类型和长度等信息。

我们在使用硬件断点的时候,就是要设置调试寄存器,将断点的位置设置到DR0-DR3中,断点的长度设置到DR7的LEN0-LEN3中,将断点的类型设置到DR7的RW0-RW3中,将是否启用断点设置到DR7的L0-L3中。设置硬件断点需要的DR0-DR3很简单,就是下断点的地址,DR7寄存器很复杂,位段信息结构体如下:

typedef struct_DBG_REG7
{
/*// 局部断点(L0~3)与全局断点(G0~3)的标记位*/unsigned L0 :1; //对Dr0保存的地址启用 局部断点 unsigned G0 : 1; //对Dr0保存的地址启用 全局断点 unsigned L1 : 1; //对Dr1保存的地址启用 局部断点 unsigned G1 : 1; //对Dr1保存的地址启用 全局断点 unsigned L2 : 1; //对Dr2保存的地址启用 局部断点 unsigned G2 : 1; //对Dr2保存的地址启用 全局断点 unsigned L3 : 1; //对Dr3保存的地址启用 局部断点 unsigned G3 : 1; //对Dr3保存的地址启用 全局断点 /*// 【以弃用】用于降低CPU频率,以方便准确检测断点异常*/unsigned LE :1;
unsigned GE :
1;/*// 保留字段*/unsigned Reserve1 :3;/*// 保护调试寄存器标志位,如果此位为1,则有指令修改条是寄存器时会触发异常*/unsigned GD :1;/*// 保留字段*/unsigned Reserve2 :2;

unsigned RW0 :
2; //设定Dr0指向地址的断点类型 unsigned LEN0 : 2; //设定Dr0指向地址的断点长度 unsigned RW1 : 2; //设定Dr1指向地址的断点类型 unsigned LEN1 : 2; //设定Dr1指向地址的断点长度 unsigned RW2 : 2; //设定Dr2指向地址的断点类型 unsigned LEN2 : 2; //设定Dr2指向地址的断点长度 unsigned RW3 : 2; //设定Dr3指向地址的断点类型 unsigned LEN3 : 2; //设定Dr3指向地址的断点长度 }DBG_REG7, *PDBG_REG7;

如果用户模式应用程序已经在运行,调试器可以非侵入性地对其进行调试。对于非侵入性调试,您没有那么多的调试操作。但是,您可以最小化调试器对目标应用程序的干扰。如果目标应用程序已停止响应,则非侵入性调试非常有用。
在非侵入性调试中,调试器实际上并不附加到目标应用程序。调试器挂起目标的所有线程,并可以访问目标的内存、寄存器和其他此类信息。但是,调试器无法控制目标,因此g(Go)等命令不起作用。
如果尝试执行非侵入性调试期间不允许的命令,则会收到一条错误消息,指出“The debugger is not attached, so process execution cannot be monitored.(未附加调试器,因此无法监视进程执行。)”

选择要调试的进程

您可以通过进程ID(PID)或进程名指定目标应用程序。如果按名称指定应用程序,则应使用进程的完整名称,包括文件扩展名。如果两个进程具有相同的名称,则必须改用进程ID。

通过WinDbg命令行

要从WinDbg命令行无创地调试正在运行的进程,请使用以下语法指定-pv选项、-p选项和进程ID。

windbg -pv -p ProcessID

或者,要通过指定进程名来无创地调试正在运行的进程,请改用以下语法。

windbg -pv -pn ProcessName

通过WinDbg菜单

当WinDbg处于休眠模式时,通过单击“文件”菜单上的“附加到进程”或按F6键,可以无创地调试正在运行的进程。出现“附加到进程”对话框时,选中“非侵入性”复选框。然后,选择包含所需进程ID和名称的行。(也可以在“进程ID”框中输入进程ID。)最后,单击“确定”。

简介

本文介绍了在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);
}