云计算百科
云计算领域专业知识百科平台

【Windows】《深入浅出Windows API程序设计:核心编程篇》笔记-Chapter8-Windows异常处理

第8章 Windows异常处理

程序执行过程中难免会发生错误。CPU负责捕获类似于访问非法内存地址或者除数为0的错误代码,并抛出相应的异常,由CPU抛出的异常都是硬件异常;操作系统和应用程序也可以抛出异常,这类异常称为软件异常。当异常(包括硬件异常和软件异常)发生后,Windows或应用程序针对所发生错误生成一段处理代码,通常称为异常处理函数或异常处理程序。

发生异常时,如果程序中没有相关的异常处理函数,Windows 就会终止进程,依次弹出原书的图 8.1 所示的两个对话框。

8.1 结构化异常处理

8.1.1 try-except语句

发生异常时,Windows允许应用程序自行处理该异常,微软公司定义了try-except语句用于结构化异常处理(Structured Exception Handling,SEH):

__try
{
// 受保护语句
}
__except (异常过滤表达式)
{
// 异常处理语句
}

// 其他程序语句

try块中存放的是受保护语句,except块中存放的是异常处理语句(异常处理程序)。如果执行受保护语句发生异常时,则Windows会把对程序的控制权转交给程序自身,并根据异常过滤表达式的值决定是否执行except块中的异常处理语句。如果执行受保护语句时没有发生异常,则根本不会执行except块中的异常处理语句。

异常过滤表达式可以是一个常量值、条件表达式或逗号运算符,还可以是一个函数调用。异常过滤表达式也称为异常过滤程序,该表达式返回的值用于确定异常的处理方式,表达式的值及其含义如表8.1所示。

表8.1

表达式的值含义
EXCEPTION_EXECUTE_HANDLER (1) 处理这个异常,执行except块中的异常处理语句,然后继续执行except块后面的其他程序语句。如果受保护语句中发生异常的语句后面还有其他语句,则这些语句是不会继续执行的,因为一旦一条指令执行失败后程序就难以保证继续稳定运行,例如我们调用一个内存分配函数失败,之后对内存指针的操作都不应该执行,否则程序会不停地抛出异常
EXCEPTION_CONTINUE_SEARCH (0) 不处理该异常,Windows继续向上搜索下一个具有最高优先级的异常处理程序(不会执行except块中的异常处理语句)
EXCEPTION_CONTINUE_EXECUTION (-1) 消除异常,重新执行发生异常的那条语句(不会执行except块中的异常处理语句)。因为我们无法保证正确修复发生异常的语句,所以这可能会导致死循环,即在计算异常过滤表达式的值和重新执行出错语句之间无限循环,因此需要谨慎使用EXCEPTION_CONTINUE_EXECUTION

下面看几个有关异常过滤表达式的值EXCEPTION_EXECUTE_HANDLER、EXCEPTION_CONTINUE_SEARCH和EXCEPTION_CONTINUE_EXECUTION的示例。

下面的函数CalcHowManyDelimit用于计算一个字符串中指定字符的个数:

int CalcHowManyDelimit(LPCTSTR lpStrToken, LPCTSTR lpStrDelimit)
{
int nHowManyDelimit = -1; // 返回−1表示失败
LPTSTR lpStrTokenTemp = NULL; // 假设分配临时缓冲区失败
LPTSTR lpToken = NULL; // 指向被分割出部分的指针
LPTSTR lpTokenNext = NULL; // 剩余未被分解的部分指针

__try
{
// 分配一块临时缓冲区lpStrTokenTemp用于存放lpStrToken的副本,因为_tcstok_s会破坏源字符串
lpStrTokenTemp = new TCHAR[_tcslen(lpStrToken) + 1];
StringCchCopy (lpStrTokenTemp, _tcslen(lpStrToken) + 1, lpStrToken);

// 获取第一个分隔符
lpToken = _tcstok_s(lpStrTokenTemp, lpStrDelimit, &lpTokenNext);
// 如果第一个分隔符是字符串中最后一个字符
if (lpTokenNext == lpStrTokenTemp + _tcslen(lpStrToken))
nHowManyDelimit++;

// 循环获取所有分隔符
while (lpToken != NULL)
{
nHowManyDelimit++;

lpToken = _tcstok_s(NULL, lpStrDelimit, &lpTokenNext);
}
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
}

delete[]lpStrTokenTemp;
return nHowManyDelimit;
}

上述代码有可能出错的地方是,调用者在调用CalcHowManyDelimit函数时传入了非法内存地址的字符串参数,或者分配临时缓冲区时可能出现内存分配失败的情况,其他地方出错的可能性很小。例如调用者在调用CalcHowManyDelimit函数时传入了非法内存地址的字符串参数lpStrToken,当执行try块中第一句代码的_tcslen函数时会引发一个访问违规,这时Windows会把对程序的控制权转交给程序自身,异常过滤表达式的值为EXCEPTION_EXECUTE_HANDLER表示处理这个异常,于是执行except块中的异常处理语句(什么也没做),然后继续执行except块后面的delete语句并返回−1,try块中第一句后面的代码不会执行。调用_tcslen函数时会引发一个访问违规,因此new操作符的内存分配工作不会执行,lpStrTokenTemp的值为NULL,为delete操作符传入一个NULL值不会出错。

接下来再看下面的示例,异常过滤表达式可以指定为一个函数调用(异常过滤函数),根据不同的情况返回不同的值:

TCHAR g_szBuf[64] = { 0 };

VOID SomeFunc()
{
LPTSTR lpszBuf = NULL;

__try
{
*lpszBuf = TEXT('A');
// 其他代码
}
__except (ExceptionFilterFunc(&lpszBuf))
{
MessageBox(NULL, TEXT("发生异常"), TEXT("提示"), MB_OK);
}

MessageBox(NULL, TEXT("函数执行完毕"), TEXT("提示"), MB_OK);
}

INT ExceptionFilterFunc(LPTSTR* ppStr)
{
if (*ppStr == NULL)
{
*ppStr = g_szBuf;
return EXCEPTION_CONTINUE_EXECUTION;
}

return EXCEPTION_EXECUTE_HANDLER;
}

字符串指针lpszBuf的初始值为NULL。当执行try块中第一句代码时会引发一个内存写入违规,Windows把对程序的控制权转交给程序自身,异常过滤表达式是一个ExceptionFilterFunc函数调用,于是执行ExceptionFilterFunc函数,如果发现传递过来的字符串指针lpszBuf的值为NULL,就把全局变量缓冲区的地址g_szBuf赋给lpszBuf,然后返回EXCEPTION_CONTINUE_EXECUTION表示重新执行发生异常的*lpszBuf = TEXT('A');语句。我们推断lpszBuf的值等于g_szBuf,重新执行可以正确运行。这时lpszBuf的值确实等于 g_szBuf,但是try块的汇编代码可能是下面的样子:

__try
00C9C56C mov dword ptr [ebp-4], 0
00C9C573 mov eax, dword ptr [lpszBuf]
{
*lpszBuf = TEXT('A');
00C9C576 mov ecx, 41h
00C9C57B mov word ptr [eax], cx
// 其他代码
}

从汇编代码可以看出,*lpszBuf = TEXT('A');语句被汇编为两行汇编语句mov ecx,41h和mov word ptr [eax],cx,重新执行的是00C9C57B一行的mov word ptr [eax],cx语句,lpszBuf指针确实不为NULL,但是mov word ptr [eax],cx语句中的eax的值始终为0。实际结果就是:重新执行mov word ptr [eax],cx指令,还是会发生写入NULL地址这样的违规异常,但是执行ExceptionFilterFunc函数,发现传递过来的字符串指针lpszBuf的值不为NULL,返回EXCEPTION_EXECUTE_HANDLER表示处理这个异常,于是执行except块中的异常处理语句MessageBox,然后继续执行except块后面的MessageBox语句,SomeFunc函数返回。

如果把ExceptionFilterFunc函数进行如下所示的更改,就会形成一个死循环:

INT ExceptionFilterFunc(LPTSTR* ppStr)
{
if (*ppStr == NULL)
*ppStr = g_szBuf;

return EXCEPTION_CONTINUE_EXECUTION;
}

因为我们无法保证正确修复发生异常的语句,所以这可能会导致死循环,即在计算异常过滤表达式的值和重新执行出错语句之间无限循环,因此需要谨慎使用EXCEPTION_CONTINUE_EXECUTION。

再看一个例子,与上面的例子类似,只不过是把SomeFunc函数中的try块中的语句更换为一个具有结构化异常处理能力的函数调用:

TCHAR g_szBuf[64] = { 0 };

VOID SomeFunc()
{
LPTSTR lpszBuf = NULL;

__try
{
FuncInTry(lpszBuf);
// 其他代码
}
__except (ExceptionFilterFunc(&lpszBuf))
{
MessageBox(NULL, TEXT("发生异常"), TEXT("提示"), MB_OK);
}

MessageBox(NULL, TEXT("函数执行完毕"), TEXT("提示"), MB_OK);
}

VOID FuncInTry(LPTSTR pStr)
{
__try
{
*pStr = TEXT('A');
// 其他代码
}
__except (EXCEPTION_CONTINUE_SEARCH)
{
// 不会执行
}
}

INT ExceptionFilterFunc(LPTSTR* ppStr)
{
if (*ppStr == NULL)
{
*ppStr = g_szBuf;
return EXCEPTION_CONTINUE_EXECUTION;
}

return EXCEPTION_EXECUTE_HANDLER;
}

当执行SomeFunc函数中的try块中第一句代码时,调用FuncInTry函数并传入一个NULL指针,在FuncInTry函数内部会引发一个内存写入违规异常,这样就会计算FuncInTry函数中异常过滤表达式的值,这里是EXCEPTION_CONTINUE_SEARCH,该常量表示系统将在调用栈中向上查找前一个包含except块的try块,并计算这个try块对应的异常过滤表达式。第1次执行ExceptionFilterFunc函数会返回EXCEPTION_CONTINUE_EXECUTION,表示重新执行FuncInTry函数中的赋值语句,ExceptionFilterFunc函数中对指针值的修改不会影响FuncInTry函数中的pStr变量,因此重新执行FuncInTry函数中的赋值语句只会导致同一个异常再次发生。第2次执行ExceptionFilterFunc函数会返回EXCEPTION_EXECUTE_HANDLER表示处理该异常,于是执行SomeFunc函数中的except块中的异常处理语句MessageBox,然后继续执行except块后面的MessageBox语句,SomeFunc函数返回。

8.1.2 GetExceptionCode和GetExceptionInformation

如果应用程序无法从异常中完全恢复,我们可以选择显示相关错误信息并捕获应用程序的内部状态,从而帮助诊断问题。结构化异常处理提供了两个可以与try-except语句一起使用的内部函数:GetExceptionCode和GetExceptionInformation。

GetExceptionCode宏用于获取刚刚发生的异常的异常代码,只能在异常过滤表达式或异常处理程序块内调用GetExceptionCode。如果异常过滤表达式调用的是一个函数,则不能在异常过滤函数中调用GetExceptionCode,但是GetExceptionCode的返回值可以作为参数传递给异常过滤函数。GetExceptionCode宏的相关定义如下:

DWORD GetExceptionCode();
#define GetExceptionCode _exception_code
unsigned long __cdecl _exception_code(void);

GetExceptionCode返回一个DWORD类型的异常代码,常见的异常代码及含义如表8.2所示。

表8.2

|
| 异常分类 | 异常代码 | 含义 |
| 与内存相关的异常代码 | EXCEPTION_ACCESS_VIOLATION(0xC0000005) | 试图读取或写入对其没有适当访问权限的内存地址 |
| 与内存相关的异常代码 | EXCEPTION_DATATYPE_MISALIGNMENT(0x80000002) | 试图从没有提供自动对齐机制的硬件中读入没有对齐的数据。例如,16位数据必须在2字节边界对齐,32位数据必须在4字节边界对齐,以此类推 |
| 与内存相关的异常代码 | EXCEPTION_ARRAY_BOUNDS_EXCEEDED(0xC000008C) | 在支持边界检查的硬件上访问越界的数组元素 |
| 与内存相关的异常代码 | EXCEPTION_STACK_OVERFLOW(0xC00000FD) | 用光了系统分配给它的栈空间 |
| 与内存相关的异常代码 | EXCEPTION_PRIV_INSTRUCTION(0xC0000096) | 试图执行在当前机器模式下不允许执行的指令 |
| 与内存相关的异常代码 | EXCEPTION_ILLEGAL_INSTRUCTION(0xC000001D) | 试图执行一条非法指令 |
| 与异常本身相关的异常代码 | EXCEPTION_NONCONTINUABLE_EXCEPTION(0xC0000025) | 异常过滤表达式返回EXCEPTION_CONTINUE_EXECUTION,但是实际上这个类型的异常发生后系统并不允许程序继续执行 |
| 与异常本身相关的异常代码 | EXCEPTION_INVALID_DISPOSITION(0xC0000026) | 异常过滤表达式返回EXCEPTION_EXECUTE_HANDLER、EXCEPTION_CONTINUE_SEARCH和EXCEPTION_CONTINUE_EXECUTION以外的值 |
| 与调试相关的异常代码 | EXCEPTION_BREAKPOINT(0x80000003) | 遇到int 3断点 |
| 与调试相关的异常代码 | EXCEPTION_SINGLE_STEP(0x80000004) | 单步中断 |
| 与整型相关的异常代码 | EXCEPTION_INT_DIVIDE_BY_ZERO(0xC0000094) | 线程试图在整数除法运算中以0作为除数 |
| 与整型相关的异常代码 | EXCEPTION_INT_OVERFLOW(0xC0000095) | 整型运算的结果超出了该类型规定的范围 |

异常代码值的定义有一定规则,每个异常代码值划分为表8.3所示的几个部分(Customer表示用户自定义)。

表8.3

位含义值
31~30 严重性 0=Success;1=Informational;2=Warning;3=error
29 Microsoft/Customer 0=Microsoft所定义的代码;1=Customer所定义的代码
28 保留位 总为0
27~16 设备代码 前256个值为Microsoft所保留
15~0 异常代码 由Microsoft/Customer定义的异常代码

例如EXCEPTION_ACCESS_VIOLATION的值为0xC0000005,对应的二进制形式如下:

11 0 0 000000000000 0000000000000101

第30位和第31位都被设为1,表示这是一个严重错误,线程在这种情况不能继续往下执行;第29位为0,表示这个异常代码由Microsoft定义;第0~15位的值为5,表示Microsoft将访问违规异常代码定义为5。

使用GetExceptionCode的示例如下:

VOID SomeFunc()
{
int n = 0;

__try
{
int nTemp = 10;
nTemp /= n;
}
__except ((GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION ||
GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO) ?
EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
{
switch (GetExceptionCode())
{
case EXCEPTION_ACCESS_VIOLATION:
// 处理访问违规
MessageBox(NULL, TEXT("访问违规"), TEXT("提示"), MB_OK);
break;

case EXCEPTION_INT_DIVIDE_BY_ZERO:
// 处理除零错误
MessageBox(NULL, TEXT("除零错误"), TEXT("提示"), MB_OK);
break;
}
}

MessageBox(NULL, TEXT("函数执行完毕"), TEXT("提示"), MB_OK);
}

GetExceptionInformation宏用于获取刚刚发生的异常的相关信息,只能在异常过滤表达式中调用该宏,如果异常过滤表达式调用的是一个函数,不能在异常过滤函数中调用该宏,但是该宏的返回值可以作为参数传递给异常过滤函数。GetExceptionInformation宏的相关定义如下:

LPEXCEPTION_POINTERS GetExceptionInformation();
#define GetExceptionInformation (struct _EXCEPTION_POINTERS*)_exception_info
void* __cdecl _exception_info();

GetExceptionInformation返回一个指向EXCEPTION_POINTERS结构的指针。EXCEPTION_POINTERS结构在winnt.h头文件中定义如下:

typedef struct _EXCEPTION_POINTERS {
PEXCEPTION_RECORD ExceptionRecord; // 指向包含异常信息的EXCEPTION_RECORD结构的指针
PCONTEXT ContextRecord; // 指向包含线程环境的CONTEXT结构的指针
} EXCEPTION_POINTERS, * PEXCEPTION_POINTERS;

EXCEPTION_RECORD结构和CONTEXT结构的含义在4.6节中已经介绍过。之所以只能在异常过滤表达式中调用GetExceptionInformation,是因为指向EXCEPTION_RECORD结构的指针和指向CONTEXT结构的指针位于栈上,在把控制权转交给异常处理程序后,这些栈上的数据结构就会被销毁。

EXCEPTION_RECORD结构有几个字段在前面没有介绍:

typedef struct _EXCEPTION_RECORD {
DWORD ExceptionCode; // 异常代码
DWORD ExceptionFlags; // 异常标志
struct _EXCEPTION_RECORD* ExceptionRecord;
PVOID ExceptionAddress; // 发生异常的地址
DWORD NumberParameters;
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD, * PEXCEPTION_RECORD;

  • ExceptionFlags字段是异常标志,该字段可以设置为0表示程序可继续执行的异常,也可以设置为EXCEPTION_NONCONTINUABLE表示程序不可继续执行的异常,在不可继续的异常发生后继续执行程序会导致发生EXCEPTION_NONCONTINUABLE_EXCEPTION异常。

  • 如果发生嵌套异常时,则ExceptionRecord字段是一个指向EXCEPTION_RECORD结构的指针,其中包含另一个异常的异常信息;如果没有发生嵌套异常,则该字段为NULL。

  • NumberParameters字段表示与异常关联的参数个数,即ExceptionInformation数组中数组元素的个数,最多为EXCEPTION_MAXIMUM_PARAMETERS(15)个,该字段的值通常为0。

  • ExceptionInformation数组是一组描述异常的附加参数,该字段的值通常为NULL。

EXCEPTION_RECORD结构的最后两个字段(NumberParameters和ExceptionInformation)提供了关于异常的附加信息,目前只有EXCEPTION_ACCESS_VIOLATION和EXCEPTION_IN_PAGE_ERROR异常提供了附加信息,其他所有异常的NumberParameters值均为0,如表8.4所示。

表8.4

异常代码数组元素含义
EXCEPTION_ACCESS_VIOLATION ExceptionInformation[0]包含一个读写标志,指出引发这个非法访问的操作类型,该值为0表示线程试图读取不可访问的内存地址,该值为1表示线程试图写入不可访问的内存地址。当数据执行保护(Data Execution Prevention,DEP)检测到线程执行没有可执行权限的内存页中的代码时,也会抛出EXCEPTION_ACCESS_VIOLATION异常,同时ExceptionInformation[0]的值被设置为8。ExceptionInformation[1]表示不可访问数据的内存地址
EXCEPTION_IN_PAGE_ERROR ExceptionInformation[0]的含义与EXCEPTION_ACCESS_VIOLATION相同。ExceptionInformation[1]同样表示不可访问数据的内存地址。ExceptionInformation[2]表示导致异常的NTSTATUS代码

如果需要在异常处理程序中使用EXCEPTION_RECORD结构和CONTEXT结构,可以按如下方式使用:

VOID SomeFunc()
{
EXCEPTION_RECORD ExceptionRecord;
CONTEXT ContextRecord;

__try
{
}
__except (ExceptionRecord = *((GetExceptionInformation())->ExceptionRecord),
ContextRecord = *((GetExceptionInformation())->ContextRecord), EXCEPTION_EXECUTE_HANDLER)
{
// 使用EXCEPTION_RECORD结构和CONTEXT结构
}

// 其他代码
}

使用GetExceptionInformation的示例如下:

VOID SomeFunc()
{
__try
{
// 代码
}
__except (ExceptionFilterFunc(GetExceptionInformation()))
{
// 代码
}

// 代码
}

INT ExceptionFilterFunc(LPEXCEPTION_POINTERS lpExceptionPointers)
{
TCHAR szBuf[256] = { 0 };

wsprintf(szBuf, TEXT("异常地址:0x%p,异常代码:0x%X"),
lpExceptionPointers->ExceptionRecord->ExceptionAddress,
lpExceptionPointers->ExceptionRecord->ExceptionCode);
MessageBox(NULL, szBuf, TEXT("提示"), MB_OK);

return EXCEPTION_EXECUTE_HANDLER;
}

8.1.3 利用结构化异常处理进行反调试

所有异常处理都从内核底层的异常处理程序开始,底层异常处理程序调用用户层的异常处理程序(如果有)。程序中的每个函数都可以具有自己的异常处理程序,这正是结构化异常处理名称中结构化的含义,随着层层函数调用,所有的异常处理程序会形成一个SEH链表,try-except语句就是在栈中构造一个SEH节点,最后加入的SEH节点位于SEH链表的头部。需要注意,结构化异常处理是基于线程的,每个线程都可以有自己的SEH链表。每个SEH节点实际上是一个EXCEPTION_REGISTRATION_RECORD结构,该结构在winnt.h头文件中定义如下:

typedef struct _EXCEPTION_REGISTRATION_RECORD {
struct _EXCEPTION_REGISTRATION_RECORD* Next; // 指向前一个SEH节点的本结构
PEXCEPTION_ROUTINE Handler; // 异常处理程序
} EXCEPTION_REGISTRATION_RECORD;

try-except语句在栈中构造一个EXCEPTION_REGISTRATION_RECORD结构到SEH链表的头部,这个过程使用汇编代码描述如下:

push ExceptionHandler // 异常处理程序
push fs:[0] // 前一个SEH节点的EXCEPTION_REGISTRATION_RECORD
mov fs:[0] , esp // 当前SEH节点的EXCEPTION_REGISTRATION_RECORD指针放入fs:[0]

// 代码

// 将fs:[0]的值恢复为原来的EXCEPTION_REGISTRATION_RECORD结构的地址
pop fs:[0]
pop eax

fs:[0]也就是fs寄存器偏移0的地址处永远指向当前SEH节点的EXCEPTION_REGISTRATION_RECORD结构。函数返回时应该将fs:[0]的值恢复为原来的EXCEPTION_REGISTRATION_RECORD结构的地址,最后一行代码pop eax仅仅是为了使栈平衡,弹出到eax中的值没有实际用途。执行这两条指令后,栈中的当前SEH节点的EXCEPTION_REGISTRATION_RECORD结构被释放。上述汇编代码是结构化异常处理大致的样子,在不同的操作系统和编译器上结构化异常处理的具体实现会有所不同,这里不再深入研究。

当一个异常发生时,系统会首先查看产生异常的进程是否正在被调试。如果正在被调试,则会向调试器发送一个EXCEPTION_DEBUG_EVENT事件;如果进程没有被调试或者调试器不处理该异常,则会调用用户层的异常处理程序(如果有)。例如,int3是最常用的普通断点,int3断点的原理就是把指令的第1字节修改为0xCC,当执行到该条指令发现第1字节是0xCC时就会触发一个异常并暂停,然后调试器执行异常处理,把该指令的第1字节修改回原来的字节指令,并把指令指针寄存器EIP的值减1以重新执行该指令。因为调试器已经处理int3异常,所以用户层的异常处理程序将不会得到执行。我们可以利用这一点来检测程序自身是否正在被调试,例如下面的函数:

BOOL CheckDebugging()
{
__try
{
RaiseException(EXCEPTION_BREAKPOINT, 0, 0, NULL);
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
return FALSE;
}

return TRUE;
}

try块中调用RaiseException函数触发一个EXCEPTION_BREAKPOINT异常,如果程序正在被调试,则不会执行except块中的return FALSE语句,而是执行except块后面的return TRUE语句。

可以按如下方式使用CheckDebugging函数:

if (CheckDebugging())
MessageBox(hwndDlg, TEXT("进程正在被调试"), TEXT("提示"), MB_OK);

上面的反调试代码很容易被反调试,OD可以安装一个StrongOD插件,该插件中有一个Skip Some Exceptions选项,勾选该选项即可跳过该反调试。

如果把CheckDebugging函数修改如下,会导致一个写NULL地址异常:

BOOL CheckDebugging()
{
__try
{
//RaiseException(EXCEPTION_BREAKPOINT, 0, 0, NULL);
LPDWORD lpdw = NULL;
*lpdw = 0x12345678;
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
return FALSE;
}

return TRUE;
}

取消选中StrongOD插件的Skip Some Exceptions选项,OD载入程序,按F9键运行,这种情况下OD会接管并处理内存访问异常,程序正常运行。

打开OD的选项菜单→调试设置,打开异常选项卡,取消选中原书的图8.2中的6个异常。即发生上述异常时OD不会执行处理操作,而是交给程序自身去处理。按F9键运行,程序中断在原书的图8.3所示的界面。

同时,OD左下角给出提示(见原书的图8.4)。这就是说,OD并没有主动接管并处理异常,按下Shift + F9组合键,会交给程序自身处理异常,程序正常运行。

但是RaiseException函数触发的是软件异常,不属于上述情况,如果取消选中StrongOD插件的Skip Some Exceptions选项,按F9键运行程序,会提示“进程正在被调试”。

8.1.4 软件异常

通常情况下,调用一个函数时,可以通过返回错误代码来指明函数执行失败,其实当一个函数执行失败时也可以使函数抛出一个异常而不是返回错误代码,由异常处理程序来处理程序错误。例如,默认情况下从堆中分配(HeapAlloc)或重新分配(HeapReAlloc)内存块失败时会返回NULL,程序可以在调用 HeapCreate 函数创建私有堆,或者在每次调用 HeapAlloc、HeapReAlloc 函数时,指定HEAP_GENERATE_EXCEPTIONS标志,这样一来每当调用这两个函数时,如果内存分配失败就会抛出一个异常(STATUS_NO_MEMORY或STATUS_ACCESS_VIOLATION)以通知应用程序有错误发生,程序可以通过结构化异常处理程序来捕获这个异常。

由CPU捕获某一事件并抛出的异常都是硬件异常,操作系统和应用程序也可以抛出异常,这类异常称为软件异常。软件异常可以作为一种指明发生的错误,程序还可以通过抛出软件异常来改变程序执行流程,这种方式在加密/解密领域应用得比较多。RaiseException函数用于在调用线程中抛出一个异常:

VOID RaiseException(
_In_ DWORD dwExceptionCode, // 异常代码
_In_ DWORD dwExceptionFlags, // 异常标志,可以为0或EXCEPTION_
// NONCONTINUABLE
_In_ DWORD nNumberOfArguments, // lpArguments数组中的参数个数
_In_ CONST ULONG_PTR* lpArguments); // 附加参数数组

  • dwExceptionCode参数指定异常代码,用户可以自定义异常代码,只要符合前面介绍的Windows异常代码的定义规则即可。

  • dwExceptionFlags参数指定异常标志,该参数可以设置为0表示程序可继续执行的异常,也可以设置为EXCEPTION_NONCONTINUABLE表示程序不可继续执行的异常,在不可继续的异常发生后继续执行程序会导致发生EXCEPTION_NONCONTINUABLE_EXCEPTION异常。一般来说,dwExceptionFlags参数用来指出异常过滤程序在处理该异常时能否返回EXCEPTION_CONTINUE_EXECUTION,如果该参数设置为EXCEPTION_NONCONTINUABLE,则表示这是一个不可恢复的严重错误,这时候如果异常过滤程序返回EXCEPTION_CONTINUE_EXECUTION,系统就会抛出一个新的EXCEPTION_NONCONTINUABLE_EXCEPTION异常。

  • nNumberOfArguments和lpArguments参数用于指定抛出异常的附加信息,通常不需要这两个参数。

8.2 向量化异常处理(全局)

8.2.1 向量化异常处理简介

向量化异常处理(Vectored Exception Handling,VEH)是对结构化异常处理的扩展,向量化异常处理是基于进程全局的,程序可以注册一个函数来监视或处理该程序的所有异常,当进程中的任何一个线程中发生异常时,都去调用注册的这个函数(异常处理程序)。

程序可以通过调用AddVectoredExceptionHandler函数添加或者注册一个向量化异常处理程序,也可以多次调用AddVectoredExceptionHandler函数添加多个异常处理程序,所有向量化异常处理程序形成一个VEH链表。AddVectoredExceptionHandler函数声明如下:

PVOID WINAPI AddVectoredExceptionHandler(
_In_ ULONG FirstHandler, // 调用异常处理程序的顺序,零或非零
_In_ PVECTORED_EXCEPTION_HANDLER VectoredHandler); // 异常处理程序指针,回调函数

  • FirstHandler参数指定调用异常处理程序的顺序,如果该参数为非零,则VectoredHandler参数指定的异常处理程序是第一个要调用的处理程序(VectoredHandler参数指定的异常处理程序放在VEH链表的头部),如果该参数为0,则VectoredHandler参数指定的异常处理程序是最后一个要调用的处理程序(VectoredHandler参数指定的异常处理程序放在VEH链表的尾部)。

  • VectoredHandler 参数是指向异常处理程序的指针,PVECTORED_EXCEPTION_HANDLER 是异常处理程序函数指针类型:

typedef LONG(NTAPI* PVECTORED_EXCEPTION_HANDLER)(struct _EXCEPTION_POINTERS* ExceptionInfo);

向量化异常处理程序的函数定义应该符合如下格式:

LONG CALLBACK VectoredHandler(PEXCEPTION_POINTERS ExceptionInfo);

NTAPI、CALLBACK与WINAPI相同,都是__stdcall函数调用约定。

如果函数执行成功,则返回值是异常处理程序句柄,后期需要删除异常处理程序时会用到该句柄;如果函数执行失败,则返回值为NULL。

当发生异常时,系统在执行结构化异常过滤程序前,会先按照VEH链表顺序逐个调用向量化异常处理程序,如果某个异常处理程序可以修复发生的问题,则应该返回EXCEPTION_CONTINUE_EXECUTION,使抛出异常的指令再次执行,只要某个异常处理程序返回EXCEPTION_CONTINUE_EXECUTION,VEH链表中的其他异常处理程序和结构化异常过滤程序就不会再被执行;如果一个向量化异常处理程序不能修复发生的问题,则应该返回EXCEPTION_CONTINUE_SEARCH,使VEH链表中的其他异常处理程序有机会去处理这个异常。如果所有的向量化异常处理程序都返回EXCEPTION_CONTINUE_SEARCH,则结构化异常过滤程序就会执行。需要注意的是,向量化异常处理程序不能返回EXCEPTION_EXECUTE_HANDLER。

当不再需要之前注册的向量化异常处理程序时,可以调用RemoveVectoredExceptionHandler将其删除:

ULONG WINAPI RemoveVectoredExceptionHandler(_In_ PVOID pHandler);

pHandler参数是先前调用AddVectoredExceptionHandler函数注册的向量化异常处理程序的句柄。

8.2.2 利用向量化异常处理实现基于断点的API Hook

OD调试器的int3断点的原理是,把指令的第1字节修改为0xCC,当执行到该条指令发现第1字节是0xCC时就会触发一个异常并暂停,然后调试器执行异常处理,把该指令的第1字节修改回原来的字节码,并把指令指针寄存器EIP的值减1以重新执行该指令。本节我们编写一个VEHBreakPoint.dll,VEHBreakPoint.dll需要注入其他目标进程中,用于监视目标进程通过调用LoadLibrary函数加载了哪些模块。Kernel32.dll中的LoadLibrary函数需要一个字符串参数lpLibFileName以指定要加载的模块名称。我们在LoadLibrary函数的起始地址处设置一个int3断点,当程序执行到断点地址处时,发现第1字节是0xCC就会触发一个异常并暂停,VEHBreakPoint.dll需要处理这个异常,因此我们需要注册一个向量化异常处理程序LoadLibraryExWBPHandler处理该异常。

在LoadLibraryExWBPHandler函数中,通过ExceptionInfo->ExceptionRecord->ExceptionCode可以得到异常代码。如果异常代码是EXCEPTION_BREAKPOINT,就表示程序执行到了int3断点地址处,此时栈指针esp指向的地址是LoadLibrary函数的返回地址,esp + 4指向的地址是LoadLibrary函数的模块名称字符串参数,这时LoadLibrary函数的实现代码还没有执行,我们可以对函数参数进行各种自定义Hook操作。执行完自定义Hook操作后,需要临时删除int3断点(修改0xCC为原指令字节码)以重新执行发生int3异常的指令,但是删除int3断点后程序开始继续执行,如何拦截下一次LoadLibrary函数调用呢?执行完自定义操作后,我们临时删除int3断点,通过 ExceptionInfo->ContextRecord-> EFlags |= 0x100;语句设置一个单步中断,然后返回EXCEPTION_CONTINUE_EXECUTION以重新执行发生int3异常的指令,这样一来在执行完LoadLibrary函数的第一条指令后即可触发一个EXCEPTION_SINGLE_STEP单步异常并暂停,在处理EXCEPTION_SINGLE_STEP单步异常时恢复LoadLibrary函数起始地址处的int3断点,并返回EXCEPTION_CONTINUE_EXECUTION表示继续执行程序,等待下一次int3异常,触发EXCEPTION_SINGLE_STEP单步异常后系统会自动把标志寄存器的TF位置0,因此这里并不需要再手动置0。

如果一个Windows API函数需要字符串参数,则该函数通常有A和W两个版本,从Windows NT开始,Windows的内核版本完全使用Unicode来构建,微软公司也逐渐开始倾向于只提供API函数的Unicode版本,Kernel32.dll中的LoadLibraryA / LoadLibraryW函数的调用关系如下:

Kernel32.LoadLibraryW→KernelBase.LoadLibraryW→KernelBase.LoadLibraryExW
Kernel32.LoadLibraryA→KernelBase.LoadLibraryA→KernelBase.LoadLibraryExA→KernelBase. LoadLibraryExW

LoadLibraryA/LoadLibraryW函数最终都是对KernelBase.LoadLibraryExW函数的调用,与其对Kernel32.dll中的LoadLibraryA / LoadLibraryW函数设置断点,不如直接对KernelBase.LoadLibraryExW函数设置断点更直接和通用。

VEHBreakPoint.dll不需要导出函数,VEHBreakPoint.cpp源文件的内容如下:

#include <Windows.h>

// 全局变量
LPVOID g_pfnLoadLibraryExWAddress; // LoadLibraryExW函数地址
BYTE g_bOriginalCodeByte; // 保存LoadLibraryExW函数的第一个指令码
HWND g_hwndDlg; // CreateProcessInjectDll程序窗口句柄

// 函数声明
// 设置int3断点(返回原指令码)
BYTE SetBreakPoint(LPVOID lpCodeAddr);
// 移除int3断点
VOID RemoveBreakPoint(LPVOID lpCodeAddr, BYTE bOriginalCodeByte);

// 为LoadLibraryExW函数的int3断点注册一个向量化异常处理程序
LONG CALLBACK LoadLibraryExWBPHandler(PEXCEPTION_POINTERS ExceptionInfo);

// LoadLibraryExW函数int3中断后执行用户所需的自定义操作
VOID LoadLibraryExWCustomActions(LPVOID lpCodeAddr, LPVOID lpStackAddr);

BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
g_hwndDlg = FindWindow(TEXT("#32770"), TEXT("CreateProcessInjectDll"));

// 获取KernelBase.LoadLibraryExW函数的地址
g_pfnLoadLibraryExWAddress = (LPVOID)GetProcAddress(
GetModuleHandle(TEXT("KernelBase.dll")), "LoadLibraryExW");

// 为LoadLibraryExW函数的int3断点注册一个向量化异常处理程序
AddVectoredExceptionHandler(1, LoadLibraryExWBPHandler);
// 在LoadLibraryExW函数上设置一个int3断点
g_bOriginalCodeByte = SetBreakPoint(g_pfnLoadLibraryExWAddress);
break;

case DLL_PROCESS_DETACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
break;
}

return TRUE;
}

// 内部函数
LONG CALLBACK LoadLibraryExWBPHandler(PEXCEPTION_POINTERS ExceptionInfo)
{
DWORD dwExceptionCode = ExceptionInfo->ExceptionRecord->ExceptionCode;

if (dwExceptionCode == EXCEPTION_BREAKPOINT)
{
// 检查是否是我们设置的int3断点,如果不是,将它传递给其他异常处理程序
if (ExceptionInfo->ExceptionRecord->ExceptionAddress != g_pfnLoad LibraryExWAddress)
return EXCEPTION_CONTINUE_SEARCH;

// 对LoadLibraryExW函数执行用户所需的自定义操作
LoadLibraryExWCustomActions(ExceptionInfo->ExceptionRecord->ExceptionAddress,
(LPVOID)(ExceptionInfo->ContextRecord->Esp));

// 临时移除int3断点
RemoveBreakPoint(g_pfnLoadLibraryExWAddress, g_bOriginalCodeByte);
// 设置单步中断
ExceptionInfo->ContextRecord->EFlags |= 0x100;

// 重新执行发生int3异常的指令,因为设置了单步中断,接下来会单步执行完第一条指令
return EXCEPTION_CONTINUE_EXECUTION;
}
else if (dwExceptionCode == EXCEPTION_SINGLE_STEP)
{
if (ExceptionInfo->ExceptionRecord->ExceptionAddress !=
(LPBYTE)g_pfnLoadLibraryExWAddress + 2)
return EXCEPTION_CONTINUE_SEARCH;

// 已经执行完用户的自定义操作,也已经单步执行完LoadLibraryExW函数的第一条语句,
// 重新设置int3断点,以等待下一次LoadLibraryExW函数调用
SetBreakPoint(g_pfnLoadLibraryExWAddress);

// 继续运行
return EXCEPTION_CONTINUE_EXECUTION;
}

// 非int3断点和单步中断都不处理
return EXCEPTION_CONTINUE_SEARCH;
}

BYTE SetBreakPoint(LPVOID lpCodeAddr)
{
BYTE bOriginalCodeByte;
BYTE bInt3 = 0xCC;

// 读取LoadLibraryExW函数的第一个指令码
ReadProcessMemory(GetCurrentProcess(), lpCodeAddr, &bOriginalCodeByte,
sizeof(bOriginalCodeByte), NULL);

// 设置int3断点
WriteProcessMemory(GetCurrentProcess(), lpCodeAddr, &bInt3, sizeof(bInt3), NULL);

return bOriginalCodeByte;
}

VOID RemoveBreakPoint(LPVOID lpCodeAddr, BYTE bOriginalCodeByte)
{
WriteProcessMemory(GetCurrentProcess(), lpCodeAddr, &bOriginalCodeByte,
sizeof(bOriginalCodeByte), NULL);
}

VOID LoadLibraryExWCustomActions(LPVOID lpCodeAddr, LPVOID lpStackAddr)
{
TCHAR szDllName[MAX_PATH] = { 0 };

ReadProcessMemory(GetCurrentProcess(), (LPVOID)(*(LPDWORD)((LPBYTE)lpStackAddr + 4)),
szDllName, sizeof(szDllName), NULL);

// 动态链接库名称显示到CreateProcessInjectDll程序的编辑控件中
SendDlgItemMessage(g_hwndDlg, 1002, EM_SETSEL, -1, -1);
SendDlgItemMessage(g_hwndDlg, 1002, EM_REPLACESEL, TRUE, (LPARAM)szDllName);
SendDlgItemMessage(g_hwndDlg, 1002, EM_REPLACESEL, TRUE, (LPARAM)TEXT("\\ r\\n"));
}

对VEHBreakPoint.dll进行测试并不需要重新编写一个程序,直接使用Chapter6\\CreateProcessInjectDll项目,把CreateProcessInjectDll.cpp源文件中CreateProcessAndInjectDll函数的szDllPath变量修改为Chapter8\\VEHBreakPoint\\Debug\\VEHBreakPoint.dll即可,当然,CreateProcessInjectDll程序需要添加一个多行编辑控件用于显示目标进程加载的模块名称,如原书的图8.5所示。

另外,获取KernelBase.LoadLibraryExW函数的地址使用的是GetProcAddress (GetModuleHandle(TEXT (“KernelBase.dll”)), “LoadLibraryExW”);语句。虽然 CreateProcessInjectDll 程序使用的是CREATE_SUSPENDED标志调用CreateProcess函数创建的目标进程,此时子进程还没有初始化完毕、入口代码还没有执行,但是一些核心模块例如Kernel32.dll、User32.dll、Gdi32.dll和KernelBase.dll等都已经成功加载。

LoadLibraryEx函数同样用于将指定的模块加载到调用进程的地址空间中,与LoadLibrary函数相比,该函数可以指定加载选项:

HMODULE WINAPI LoadLibraryEx(
_In_ LPCTSTR lpLibFileName, // 模块名称,可以使用相对路径或绝对路径
_Reserved_ HANDLE hFile, // 保留参数,必须为NULL
_In_ DWORD dwFlags); // 加载选项

  • lpLibFileName参数指定模块名称,可以使用相对路径或绝对路径,该函数采用与CreateProcess函数的lpCommandLine参数相同的搜索顺序来搜索模块文件。

  • dwFlags参数指定加载选项,如果该参数设置为0,则相当于LoadLibrary函数。dwFlags参数常用的值如表8.5所示。

表8.5

常量含义
DONT_RESOLVE_DLL_REFERENCES(0x00000001) 通常不使用该标志。如果指定了该标志,并且lpLibFileName参数指定的是DLL模块,则系统不会调用DllMain进行进程和线程的初始化与清理工作,另外,系统不会加载lpLibFileName模块引用的其他模块(通常,被加载模块很可能还会加载其他模块)
LOAD_LIBRARY_AS_DATAFILE(0x00000002) 把lpLibFileName参数指定的模块作为数据文件来映射到调用进程的虚拟地址空间中,映射后该模块没有可执行属性,指定该标志来加载.exe或.dll文件通常是为了使用其中的资源,LoadLibraryEx函数会返回一个模块句柄,然后可以通过使用返回的模块句柄来调用相关的加载资源函数。该标志可以与LOAD_LIBRARY_AS_IMAGE_RESOURCE一起使用
LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE(0x00000040) 与LOAD_LIBRARY_AS_DATAFILE相似,不同之处在于模块文件是以独占访问模式打开,从而禁止任何其他程序在当前程序使用该模块文件的时候对其进行修改。该标志可以与LOAD_LIBRARY_AS_IMAGE_RESOURCE一起使用
LOAD_LIBRARY_AS_IMAGE_RESOURCE(0x00000020) 把lpLibFileName参数指定的模块作为映像文件来映射到调用进程的虚拟地址空间中,系统会对模块中的相对虚拟地址(Relative Virtual Address,RVA)进行修复,这样一来用户可以直接使用模块中的虚拟地址,不必再根据模块实际映射到的内存地址对它们进行转换

当不再需要所加载的模块时,应该调用FreeLibrary函数释放该模块,FreeLibrary函数会减少引用计数。如果引用计数为0,系统会从进程的地址空间中取消模块的映射,模块句柄不再有效。

通过前面的学习我们知道,当一个异常发生时,系统会首先查看产生异常的进程是否正在被调试。如果正在被调试,则会向调试器发送一个EXCEPTION_DEBUG_EVENT事件;如果进程没有被调试或者调试器不处理该异常,则会调用用户层的异常处理程序(如果有),调试器也相当于一个异常处理程序并被首先调用。如果进程没有被调试或调试器不处理该异常,我们学习过结构化异常处理和基于进程全局的向量化异常处理,随着层层函数调用,所有的栈上结构化异常处理程序会形成一个SEH链表,程序也可以通过多次调用AddVectoredExceptionHandler函数来添加多个向量化异常处理程序形成一个向量化链表,在这种情况下,系统先按照VEH链表顺序逐个调用向量化异常处理程序,如果某个向量化异常处理程序可以处理异常,则VEH链表中的其他异常处理程序和结构化异常过滤程序不会再被执行;如果所有的VEH异常处理程序都不能处理异常,则结构化异常过滤程序就会执行。关于异常处理优先级,读者可以自行测试ExceptionHandlingPriority项目。

8.3 顶层未处理异常过滤(全局)

当一个异常发生时,如果进程中所有的向量化异常处理程序都不处理这个异常,当前线程中所有的结构化异常处理程序也不处理这个异常,就会产生一个未处理异常,Windows对未处理异常的默认处理方式是结束进程。不处理这个异常指的是没有相关异常处理程序(或异常过滤程序),或者异常处理程序(或异常过滤程序)返回EXCEPTION_CONTINUE_SEARCH。

程序可以通过调用SetUnhandledExceptionFilter函数设置一个顶层未处理异常过滤器,当进程中发生未处理异常时,会调用指定的顶层未处理异常过滤器函数lpTopLevelExceptionFilter:

LPTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter(
LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter); // 顶层未处理异常过滤器函数的指针

前面说过所有的异常处理都从内核底层的异常处理程序开始,底层异常处理程序再去调用用户层的异常处理程序(如果有),所谓顶层指的是异常处理优先级,我们知道用户层有VEH、SEH和UEF,当一个异常发生时,用户层异常处理的优先级为VEH → SEH → UEF。

LPTOP_LEVEL_EXCEPTION_FILTER数据类型在errhandlingapi.h头文件中定义如下:

typedef LONG(WINAPI* PTOP_LEVEL_EXCEPTION_FILTER)(PEXCEPTION_POINTERS ExceptionInfo);
typedef PTOP_LEVEL_EXCEPTION_FILTER LPTOP_LEVEL_EXCEPTION_FILTER;

顶层未处理异常过滤器函数lpTopLevelExceptionFilter应该定义为如下格式:

LONG WINAPI TopLevelUnhandledExceptionFilter(PEXCEPTION_POINTERS ExceptionInfo);

顶层未处理异常过滤器函数可以返回EXCEPTION_EXECUTE_HANDLER、EXCEPTION_CONTINUE_SEARCH或EXCEPTION_CONTINUE_EXECUTION(见表8.6)。

表8.6

返回值含义
EXCEPTION_EXECUTE_HANDLER 实际上用户设置的顶层未处理异常过滤器函数是由Kernel32.dll中的UnhandledExceptionFilter函数调用的,如果顶层未处理异常过滤器函数返回EXCEPTION_EXECUTE_HANDLER,那么系统会从Kernel32.UnhandledExceptionFilter函数返回,并不加提示地终止进程
EXCEPTION_CONTINUE_SEARCH 继续正常执行Kernel32.UnhandledExceptionFilter函数,顶层未处理异常过滤器是Windows提供给用户的最后处理异常的机会,返回EXCEPTION_CONTINUE_SEARCH表示异常没有得到任何处理,因此会执行系统默认的未处理异常程序(终止进程,有已终止工作错误提示)
EXCEPTION_CONTINUE_EXECUTION 从Kernel32.UnhandledExceptionFilter函数返回,并重新执行发生异常的指令,我们可以通过修改顶层未处理异常过滤器函数的ExceptionInfo参数指向的异常信息来修复异常

SetUnhandledExceptionFilter函数的返回值是先前的顶层未处理异常过滤器函数的地址,如果先前没有设置顶层未处理异常过滤器函数,则返回值为NULL。顶层未处理异常过滤器是基于进程全局的,进程中只可以设置一个顶层未处理异常过滤器函数,当调用SetUnhandledExceptionFilter函数设置一个新的过滤器时,就会替换掉先前的过滤器函数。如果SetUnhandledExceptionFilter函数的lpTopLevelExceptionFilter参数设置为NULL表示执行UnhandledExceptionFilter函数的系统默认异常处理程序(即将顶层未处理异常过滤器函数恢复设置为UnhandledExceptionFilter)。

顶层未处理异常过滤器基于进程全局。当一个异常到达这里时,进程内存通常已经处于被破坏的状态,所以顶层未处理异常过滤器函数通常不适合返回EXCEPTION_CONTINUE_EXECUTION以重新执行发生异常的指令,在顶层未处理异常过滤器函数中通常应该获取异常信息向服务器发送错误报告,或者dump错误信息到本地。

还有很重要的一点,如果进程正在被调试,则不会执行顶层未处理异常过滤器函数的,利用这一特性可以进行反调试。但是前面说过实际上用户设置的顶层未处理异常过滤器函数是由Kernel32.dll中的UnhandledExceptionFilter函数调用的,UnhandledExceptionFilter函数会判断当前进程是否正在被调试(通过调用Ntdll.ZwQueryInformationProcess函数),如果正在被调试就不会调用用户设置的顶层未处理异常过滤器函数,利用这一特性可以实现反反调试。

8.4 向量化继续处理(全局)

与向量化异常处理类似,还有一个向量化继续处理(Vectored Continue Handling,VCH)。程序可以通过调用AddVectoredContinueHandler函数添加或者注册一个向量化继续处理程序,多次调用AddVectoredContinueHandler函数添加多个继续处理程序,所有向量化继续处理程序都会被添加到链表中。AddVectoredContinueHandler与AddVectoredExceptionHandler函数的声明是相同的:

PVOID WINAPI AddVectoredContinueHandler(
_In_ ULONG FirstHandler, // 调用继续处理程序的顺序,零或非零
_In_ PVECTORED_EXCEPTION_HANDLER VectoredHandler); // 继续处理程序指针,回调函数

当不再需要之前注册的向量化继续处理程序时,可以调用RemoveVectored ContinueHandler删除该程序:

ULONG WINAPI RemoveVectoredContinueHandler(_In_ PVOID pHandler);

pHandler参数是先前调用AddVectoredContinueHandler函数注册的向量化继续处理程序的句柄。

向量化继续处理和向量化异常处理的相关函数使用方法完全相同,在此不再重复叙述。但是,向量化继续处理和向量化异常处理的行为不同,如原书的图8.6所示。

在图8.6中,无法处理异常指的是没有相关异常处理程序(或异常过滤程序),或者异常处理程序(或异常过滤程序)返回EXCEPTION_CONTINUE_SEARCH;可以处理异常是指正确修复了发生异常的指令并返回EXCEPTION_CONTINUE_EXECUTION。可以看到,只有在VEH、SEH或UEF其中之一可以处理异常的情况下才会执行向量化继续处理程序。

当一个异常发生时,异常处理过程如下(涉及内核层次的处理过程这里不作研究)。

(1)通知调试器。当一个异常发生时,系统会首先查看产生异常的进程是否正在被调试。如果正在被调试,则会向调试器发送一个EXCEPTION_DEBUG_EVENT事件;如果进程没有被调试或者调试器不处理该异常,则会调用用户层的异常处理程序(如果有)。

(2)执行向量化异常处理程序。系统在执行结构化异常过滤程序前,会先按照VEH链表顺序逐个调用向量化异常处理程序,如果某个向量化异常处理程序可以修复发生的问题,则应该返回EXCEPTION_CONTINUE_EXECUTION,使抛出异常的指令再次执行,只要某个向量化异常处理程序返回EXCEPTION_CONTINUE_EXECUTION,VEH链表中的其他异常处理程序和结构化异常过滤程序就不会再被执行;如果一个向量化异常处理程序不能修复发生的问题,则应该返回EXCEPTION_CONTINUE_SEARCH,以便VEH链表中的其他异常处理程序有机会去处理该异常。如果所有的向量化异常处理程序都返回EXCEPTION_CONTINUE_SEARCH,则结构化异常过滤程序会被执行。需要注意的是,向量化异常处理程序不能返回EXCEPTION_EXECUTE_HANDLER。

(3)执行结构化异常处理程序。所有的栈上结构化异常处理程序会形成一个SEH链表,如果一个结构化异常过滤程序返回EXCEPTION_EXECUTE_HANDLER,则表示处理这个异常,执行except块中的异常处理语句,其他结构化异常过滤程序不会被执行,顶层未处理异常过滤器更不会被执行;如果一个结构化异常过滤程序返回EXCEPTION_CONTINUE_SEARCH,则表示不处理这个异常,Windows继续向上搜索下一个具有最高优先级的结构化异常过滤程序;如果一个结构化异常过滤程序返回EXCEPTION_CONTINUE_EXECUTION,则表示重新执行发生异常的指令(不会执行except块中的异常处理语句),其他结构化异常过滤程序不会被执行,顶层未处理异常过滤器更不会被执行。如果异常过滤程序可以正确修复发生异常的指令,则整个程序可以继续正常运行,否则会导致相同的异常重复发生。

(4)执行顶层未处理异常过滤器函数(进程被调试时不会被执行)。如果进程中所有的向量化异常处理程序都不处理这个异常,当前线程中所有的结构化异常处理程序也不处理这个异常,就会产生一个未处理异常,程序可以通过调用SetUnhandledExceptionFilter函数设置一个顶层未处理异常过滤器,当进程中发生未处理异常时,会调用指定的顶层未处理异常过滤器函数。

(5)执行向量化继续处理程序。

接下来实现一个演示异常处理过程的ExceptionHandlingProcess程序,程序运行效果如原书的图8.7所示。

ExceptionHandlingProcess.cpp源文件的内容如下。

#include <windows.h>
#include "resource.h"

// 全局变量
HWND g_hwndDlg;

// 函数声明
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);

LONG CALLBACK VectoredExceptionHandler(PEXCEPTION_POINTERS ExceptionInfo);
// 向量化异常处理程序
DWORD StructuredExceptionFilter(PEXCEPTION_POINTERS ExceptionInfo);
// 结构化异常过滤程序
LONG WINAPI TopLevelUnhandledExceptionFilter(PEXCEPTION_POINTERS ExceptionInfo);
// 顶层未处理异常过滤器程序
LONG CALLBACK VectoredContinueHandler(PEXCEPTION_POINTERS ExceptionInfo);
// 向量化继续处理程序
DWORD WINAPI ThreadProc(LPVOID lpParameter);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_MAIN), NULL, DialogProc, NULL);
return 0;
}

INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
static LPVOID lpHandler, lpHandlerContinue;
HANDLE hThread;
int n = 10, m = 0, x;
TCHAR szBuf[32] = { 0 };

switch (uMsg)
{
case WM_INITDIALOG:
g_hwndDlg = hwndDlg;

// 向量化异常处理
lpHandler = AddVectoredExceptionHandler(1, VectoredExceptionHandler);
// 向量化继续处理
lpHandlerContinue = AddVectoredContinueHandler(1, VectoredContinueHandler);
// 顶层未处理异常过滤器
SetUnhandledExceptionFilter(TopLevelUnhandledExceptionFilter);
return TRUE;

case WM_COMMAND:
switch (LOWORD(wParam))
{
case IDC_BTN_OK:
__try
{
// 会发生EXCEPTION_INT_DIVIDE_BY_ZERO除零异常
x = n / m;
wsprintf(szBuf, TEXT("%d / %d = %d"), n, m, x);
MessageBox(hwndDlg, szBuf, TEXT("已从异常中恢复"), MB_OK);
}
__except (StructuredExceptionFilter(GetExceptionInformation()))
{
// 除非结构化异常过滤程序返回EXCEPTION_EXECUTE_HANDLER,否则这里不会执行
MessageBox(hwndDlg, TEXT("结构化异常处理程序"), TEXT("SEH提示"), MB_OK);
}
break;

case IDC_BTN_OK2:
hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
if (hThread)
CloseHandle(hThread);
break;

case IDCANCEL:
RemoveVectoredExceptionHandler(lpHandler);
RemoveVectoredContinueHandler(lpHandlerContinue);
EndDialog(hwndDlg, 0);
break;
}
return TRUE;
}

return FALSE;
}

LONG CALLBACK VectoredExceptionHandler(PEXCEPTION_POINTERS ExceptionInfo)
{
MessageBox(g_hwndDlg, TEXT("向量化异常处理程序"), TEXT("VEH提示"), MB_OK);

//ExceptionInfo->ContextRecord->Ecx = 2; // 把除数设置为2
return EXCEPTION_CONTINUE_SEARCH;
}

DWORD StructuredExceptionFilter(PEXCEPTION_POINTERS ExceptionInfo)
{
MessageBox(g_hwndDlg, TEXT("结构化异常过滤程序"), TEXT("SEH提示"), MB_OK);

//ExceptionInfo->ContextRecord->Ecx = 2; // 把除数设置为2
return EXCEPTION_CONTINUE_SEARCH;
}

LONG WINAPI TopLevelUnhandledExceptionFilter(PEXCEPTION_POINTERS ExceptionInfo)
{
MessageBox(g_hwndDlg, TEXT("顶层未处理异常过滤器程序"), TEXT("UEF提示"), MB_OK);

//ExceptionInfo->ContextRecord->Ecx = 2; // 把除数设置为2
return EXCEPTION_CONTINUE_SEARCH;
}

LONG CALLBACK VectoredContinueHandler(PEXCEPTION_POINTERS ExceptionInfo)
{
MessageBox(g_hwndDlg, TEXT("向量化继续处理程序"), TEXT("VCH提示"), MB_OK);

return EXCEPTION_CONTINUE_SEARCH;
}

DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
int n = 10, m = 0, x;
TCHAR szBuf[32] = { 0 };

__try
{
// 会发生EXCEPTION_INT_DIVIDE_BY_ZERO除零异常
x = n / m;
wsprintf(szBuf, TEXT("%d / %d = %d"), n, m, x);
MessageBox(g_hwndDlg, szBuf, TEXT("已从异常中恢复"), MB_OK);
}
__except (StructuredExceptionFilter(GetExceptionInformation()))
{
// 除非结构化异常过滤程序返回EXCEPTION_EXECUTE_HANDLER,否则这里不会执行
MessageBox(g_hwndDlg, TEXT("来自辅助线程:结构化异常处理程序"), TEXT("SEH提示"), MB_OK);
}

return 0;
}

因为除数m为0,所以执行x = n/m会导致线程发生一个EXCEPTION_INT_DIVIDE_BY_ZERO除零异常。如果程序编译为Release版本,x = n / m语句会被编译为以下汇编语句:

mov eax, 0Ah
cdq
xor ecx, ecx
idiv eax, ecx

在汇编语言中,idiv指令是有符号数除法指令,xor ecx, ecx用于把ecx清零,ecx作为除数,因此执行idiv eax, ecx会导致发生除零异常,在异常处理程序(或异常过滤程序)中可以通过改变ExceptionInfo->ContextRecord->Ecx的值以修复异常。

编译ExceptionHandlingProcess程序为Release版本,单击“在主线程中触发一个异常”或者“在辅助线程中触发一个异常”按钮,都会按照原书的图8.8所示的顺序弹出消息框。

因为向量化异常处理、结构化异常处理和顶层未处理异常过滤都返回 EXCEPTION_CONTINUE_SEARCH,所以向量化继续处理程序不会被执行。如果上述 3 个之一可以处理异常,都会导致执行向量化继续处理程序。

赞(0)
未经允许不得转载:网硕互联帮助中心 » 【Windows】《深入浅出Windows API程序设计:核心编程篇》笔记-Chapter8-Windows异常处理
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!