热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

《Windows核心编程》读书笔记二十四章异常处理程序与软件异常

第24章异常处理程序与软件异常本章内容24.1通过实例理解异常过滤程序和异常处理程序24.2EXCEPTION_EXECUTE_HANDLER24.3EXCEPTION_CO
第24章 异常处理程序与软件异常


本章内容

24.1 通过实例理解异常过滤程序和异常处理程序

24.2 EXCEPTION_EXECUTE_HANDLER

24.3 EXCEPTION_CONTINUE_EXECUTION

24.4 EXCEPTION_CONTINUE_SEARCH

24.5 GetExceptionCode

24.6 GetExceptionInformation

24.7 软件异常


除0或者访问NULL地址,会被cpu捕获。称为硬件异常

操作系统和应用程序也可以自行抛出异常,称为软件异常。

一个异常处理程序的语法结构

__try{// Guarded body}__except (exception filter) {// Exception handler}


__except块不能和__finally块同时存在。但是可以嵌套存在


24.1 通过实例理解异常过滤程序和异常处理程序


24.1.1 Funcmeister1 函数


DWORD dwTemp;// 1. Do any processing here.//...__try {// 2. Perform some operation.dwTemp = 0;}__except (EXCEPTION_EXECUTE_HANDLER) {// Handle an exception; this never execute.// ...}// 3. Continue processing.return dwTemp;
}


这个函数不会导致异常,在异常处理程序的try块中可以使用return goto continue和break。因为这些语句不会带来局部展开的开销。


24.1.2 Funcmeister2 函数


DWORD Funcmeister2() {DWORD dwTemp = 0;// 1. Do any processing here.//...__try {// 2. Perform some operation.dwTemp = 5 / dwTemp; // Generates an exceptiondwTemp += 10; // Never executes}__except (/* 3. Evaluate filter. */EXCEPTION_EXECUTE_HANDLER) {// 4. Handle an exception;MessageBeep(0);// ...}// 5. Continue processing.return dwTemp;
}

除零错误会被cpu捕获并抛出一个硬件异常。接着系统定位到except块,并对异常过滤程序的表达式求值。


可能是一下3个值










异常处理的过程:







24.2 EXCEPTION_EXECUTE_HANDLER


异常处理完毕以后,从哪里开始继续执行代码?

1. 可能是导致异常的那条cpu指令之后的第一条指令开始执行。可能导致连锁异常

2. 从产生异常的那条指令本身开始执行。(EXCEPTION_CONTINUE_EXECUTION)

3. 从except块后的第一句代码继续(实际情况如此)




24.2.1 一些有用的例子


一个不恰当的try/except

char* RobustStrCpy(char* strDes, const char* src) {__try {strcpy(strDes, src);}__except (EXCEPTION_EXECUTE_HANDLER) {// Nothing to do here.}return strDes;
}
以上代码虽然捕获了异常但是没有做任何处理。这是不可取的。可能会导致隐患和后续问题。






原则:只处理我们知道怎么处理的异常,并且也不要忘记其他的保护措施。以防止程序状态混乱或者安全漏洞。




一个健壮异常处理的例子。

int RobustHowManyToken(const char * str) {int nHowManyTokens = -1; // -1 indicates failurechar * strTemp = NULL; // Assume failure__try {// Allocate a temporary bufferstrTemp = (char*)malloc(strlen(str) + 1);// Copy the original string to the temporary bufferstrcpy(strTemp, str);// Get the first tokenchar * pszToken = strtok(strTemp, " ");// Iterate through all the tokensfor (; pszToken != NULL; pszToken = strtok(NULL, " "))nHowManyTokens++;nHowManyTokens++; // Add 1 since we started at -1}__except (EXCEPTION_EXECUTE_HANDLER) {// Nothing to do here}// Free the temporary buffer (guaranteed)free(strTemp);return (nHowManyTokens);
}

能过处理常见的内存错误。





另一个内存拷贝例子

PBYTE RobustMemDup(PBYTE pbSrc, size_t cb) {PBYTE pbDup = NULL; // Assume failure__try {// Allocate a buffer for the duplicate memory blockpbDup = (PBYTE)malloc(cb);memcpy(pbDup, pbSrc, cb);}__except (EXCEPTION_EXECUTE_HANDLER) {free(pbDup);pbDup = NULL;}return pbDup;
}




24.2.2 全局展开


当异常过滤程序的计算结果为EXCEPTION_EXECUTE_HANDLER时,系统必须执行全局展开(unwind)。

全局展开导致所有已经开始执行但尚未完成的try-finally块得以继续执行,在调用栈中,这些try-finally块位于对异常进行处理的try-except块下方(Call stack窗口中下方就是趋向栈底的函数)。










一个SEH的例子

void FuncOStimpy1() {// 1. Do any processing here.// ...__try{// 2. Call another function.FuncORen1();// Code here never executes.}__except (/* 6. Evaluate filter. */ EXCEPTION_EXECUTE_HANDLER){// 8. Afte rthe unwind, the exception handler executes.MessageBox(...);}// 9. Exception handled -- continue execution.
}void FuncORen1() {DWORD dwTemp = 0;// 3. Do any processing here.__try{// 4. Request permission to access protected data.WaitForSingleObject(g_hSem, INFINITE);// 5. Modify the data.// An exception is generated here.g_dwProtectedData = 5 / dwTemp;}__finally {// 7. Global unwind occurs because filter evaluated// to EXCEPTION_EXECUTE_HANDLER.// Allow others to use protected data.ReleaseSemaphore(g_hSem, 1, NULL);}// Continue processing -- never executes.
}
代码的执行顺序参考注释。





完整的执行过程详细解释参考p646.






我们的代码执行与系统控制有很大的关联,使得SEH很难理解。代码控制不再是从头到尾,系统根据自己的意图决定代码的每一个部分的执行顺序。

虽然系统执行的顺序很复杂,但是仍然可以推断的。参考图24-1 和图24-2




大概原则:当异常过滤程序返回EXCEPTION_EXECUTE_HANDLER等于告知系统,当前线程的CS,IP应该指向except块中的指令。然而实际上当前指令指针指向的是try块中的指令。当一个线程离开try-finally结构的try部分,系统需要确保finally块中的代码得到执行。当异常发生时,系统用来确保这条规则成立的机制就是全局展开。

(实际上笔者认为这种解释并不够准确,只是方便理解。 本质上来讲操作系统是通过内存注册异常处理回调函数向外训找能处理该异常的回调,一旦找到了一个能处理该异常的回调(EXCEPTION_EXECUTE_HANDLER)会从这条处理链开始从头到尾再遍历一次,这次会进行资源释放,执行finally语句块等操作,最终把栈恢复到处理该异常的那层回调函数所在的调用堆栈。参考深入解析Win32结构化异常)










24.2.3 停止全局展开


可以将return放置于finally块中阻止全局展开这将导致外层函数的except块中的代码直接被跳过。

void FuncMonkey(){__try{FuncFish();}__except (EXCEPTION_EXECUTE_HANDLER) {MessageBeep(0);}
}void FuncFish() {FuncPheasant();MessageBox(NULL, TEXT("FuncFish"), TEXT(""), MB_OK);
}void FuncPheasant() {__try{strcpy(NULL, NULL);}__finally{return;}
}



执行步骤

1.内层函数FuncPheasant触发了一个异常,导致抛出异常。

2. 系统在外层的FuncMonkey发现了try/except语句块,并判断__except中表达式的值’ EXCEPTION_EXECUTE_HANDLER

3. 于是系统进行全局展开,开始在内层搜索(实际上是一个链表)执行过的try语句块是否存在finally块,并执行finally块。

4. 由于内层的FuncPheasant的finally块中存在return语句,直接导致了系统停止全局展开,该异常处理直接跳过了except中的部分,

5. 代码最后直接从except语句块之后的语句开始执行。







不应该写这样的代码,在VS2013中这会产生一个编译错误

直接产生了一个编译错误。

1>------ Build started: Project: SubProcess, Configuration: Debug Win32 ------
1> SubProcess.cpp
1>c:\users\admin\documents\visual studio 2013\projects\consoleapplication3\subprocess\subprocess.cpp(41): error C4532: 'return' : jump out of __finally block has undefined behavior during termination handling
========== Build: 0 succeeded, 1 failed, 2 up-to-date, 0 skipped ==========

24.3 EXCEPTION_CONTINUE_EXECUTION



TCHAR g_szBuffer[100];
LONG OilFilter1(TCHAR ** ppchBuffer);void FunclinRoosevelt1() {int x = 0;TCHAR *pchBuffer = NULL;__try{*pchBuffer = TEXT('J');x = 5 / x;}__except (OilFilter1(&pchBuffer)) {MessageBox(NULL, TEXT("An exception occured"), NULL, MB_OK);}MessageBox(NULL, TEXT("Function completed"), NULL, MB_OK);
}LONG OilFilter1(TCHAR **ppchBuffer) {if (*ppchBuffer == NULL) {*ppchBuffer = g_szBuffer;return EXCEPTION_CONTINUE_EXECUTION;}return EXCEPTION_EXECUTE_HANDLER;
}

1)第一次给pchBuffer地址赋值会触发异常(访问NULL地址),过滤函数中判断出了异常是由于给空地址赋值。


2)过滤函数将pchBuffer指向了一个全局的buffer,并返回EXCEPTION_CONTINUE_EXECUTION

3)except语句判断返回值是EXCEPTION_CONTINUE_EXECUTION将会继续执行触发异常的那条指令。

4)接着因为除0错误又触发了一个异常。

5)异常过滤函数并未处理该异常而是返回EXCEPTION_EXECUTE_HANDLER

6)excpet语句判断是EXCEPTION_EXECUTE_HANDLER,进行全局展开。

7)之后执行excpet语句块中的代码

8)之后执行except语句块后面的代码




使用EXCEPTION_CONTINUE_EXECUTION的时候要特别小心,很有可能堆栈已经被破坏。这将导致进程被挂起或不加任何提示的终止。





谨慎使用EXCEPTION_CONTINUE_EXECUTION




上面的那段代码FunclinRoosevelt1有时候执行结果会依赖cpu

例如有的cpu可能会将*pchBuffer = TEXT('J'); 编译成两条指令




MOV EAX, DWORD PTR[pchBuffer] // Move the address into a register

MOV WORD PTR[EAX], 'J' // Move 'J' into the address

抛出异常的是第二条指令,异常过滤可以捕获这个异常修正pchBuffer的值,并继续让cpu执行第二条指令。

问题寄存器不可能自动更新反应变量pchBuffer的更新, 这就导致了一个死循环。




如果以上代码因为优化成一条指令,修复就可能成功。但这样写代码是很危险的。




一些情况可以利用EXCEPTION_CONTINUE_EXECUTION将保证执行始终能成功。

15章的VMAlloc例子每次调用VirtualAlloc来调拨存储器,

可以改而使用SEH机制来按需调拨存储器。而不是每次都调用VirtualAlloc




16章系统为栈预定了1MB的地址空间,但是并没有立即调拨物理内存。系统创建了一个SEH,当我们试图访问栈中尚未调拨的物理存储器的区域,会因一个异常。

系统内部异常过滤程序捕获这个异常。在其内部调用VirtualAlloc来为线程调拨更多存储,并访问EXCEPTION_CONTINUE_EXECUTION。这样原先抛出异常的指令将再次执行并继续运行下去。




结合虚拟内存技术和结构化异常,可以写出运行速度极快和高效的应用程序。参考25章的电子表格程序。




24.4 EXCEPTION_CONTINUE_SEARCH


TCHAR g_szBuffer[100];
LONG OilFilter3(TCHAR ** ppchBuffer);void FuncAtude3(TCHAR * sz) {__try{*sz = TEXT('\0');}__except (EXCEPTION_CONTINUE_SEARCH) {// This never executes.}
}
void FunclinRoosevelt3() {int x = 0;TCHAR *pchBuffer = NULL;__try{FuncAtude3(pchBuffer);}__except (OilFilter3(&pchBuffer)) {MessageBox(NULL, TEXT("An exception occured"), NULL, MB_OK);}MessageBox(NULL, TEXT("Function completed"), NULL, MB_OK);
}

1)在FuncAtude3中试图写入NULL地址会引发异常。


2)FuncAtude3中的try/except语句块会捕获这条异常。并执行except后面的表达式

3)except判断表达式的结果是EXCEPTION_CONTINUE_SEARCH这将导致异常处理像外层搜索try/except语句块

4)外层的FunclinRoosevelt3中的try/except捕获了这条异常,执行except后面的过滤函数OilFilter3

5)过滤函数判断出这是一个NULL地址访问,修正了pchBuffer的地址。并返回EXCEPTION_CONTINUE_EXECUTION

6 ) except判断到EXCEPTION_CONTINUE_EXECUTION然后重新执行触发异常的指令。(FuncAtude3中的赋值语句)

7)可是由于OilFilter3修正的地址是位于外层的FunclinRoosevelt3,而且指针地址是通过传值的方式到内层的。因此内层的sz变量的地址仍然是NULL

8)这将继续导致触发异常并重复上面的2)5)步骤

9)但是这次OilFilter3判断出pchBuffer地址已经被修正过了。返回EXCEPTION_EXCUTION_HANDLER

10)接着执行FunclinRoosevelt3的except语句块中的代码

11)接着跳出FunclinRoosevelt3的except语句块并执行后面的代码。




24.5 GetExceptionCode


以下代码仅处理除0异常。

int x, y;__try{x = 0;y = 4 / x;}__except ((GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO) ?EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) {// Handle divide by zero exception.}


BOOL GetExceptionCode();

是一个内在函数,它的返回值表明刚刚发生的异常类型。




常见异常类型的定义



















GetExceptionCode只能在异常过滤程序里或者异常处理程序里使用。

例如这样是合法的。

int x, y;__try{x = 0;y = 4 / x;}__except ((GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO) ?EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) { switch (GetExceptionCode()) {case EXCEPTION_INT_DIVIDE_BY_ZERO:// Handle divide by zero exception.break;}}
但是不能在过滤函数内部调用GetExceptionCode()


LONG CoffeeFilter(){// Compilation error: illegal call to GetExceptionCode.return (GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO) ?EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH;
}int _tmain(int argc, TCHAR* argv[], TCHAR * env[])
{int x, y;__try{x = 0;y = 4 / x;}__except (CoffeeFilter()){}return 0;
}
改写过的异常过滤函数


LONG CoffeeFilter(DWORD dwExceptionCode){// Compilation error: illegal call to GetExceptionCode.return (dwExceptionCode == EXCEPTION_INT_DIVIDE_BY_ZERO) ?EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH;
}int _tmain(int argc, TCHAR* argv[], TCHAR * env[])
{int x, y;__try{x = 0;y = 4 / x;}__except (CoffeeFilter(GetExceptionCode())){}return 0;
}




异常代码遵循在WinError.h文件中定义有关错误代码的归则每个DWORD值被划分如下:






目前为止Microsoft所定义的设备代码














例如EXCEPTION_ACCESS_VIOLATION 其值是0xC0000005

展开如下




30~31位被设置成1,表示这是一个严重错误(11 = 3 线程在这种情况下不能继续往下执行)

29位是0 Microsoft已经定义了这个代码

28位是0 保留位

27-16位都是0 表示设备代码FACILITY_NULL (即违规访问异常可以在系统任何设备出现,并不是只发生在一些特定的设备上)

15-0 位的值是5, Microsoft 将访问违规定义成5, 并无其他特殊含义。




24.6 GetExceptionInformation


当发生异常时,操作系统会向发生异常的线程栈压入3个结构

EXCEPTION_RECORD, CONTEXTEXCEPTION_POINTERS




EXCEPTION_RECORD 包含抛出异常的相关信息。内容与具体cpu没有关系


CONTEXT 包含了一组cpu的寄存器信息。

EXCEPTION_POINTERS 包含两个成员。上面介绍的两个

typedef struct _EXCEPTION_POINTERS {PEXCEPTION_RECORD ExceptionRecord;PCONTEXT ContextRecord;
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;


可以使用以下函数GetExceptionInformation来获得EXCEPTION_POINTERS结构。但是注意必须在异常过滤程序中调用。因为只有系统在计算异常过滤的时候才是有效的。

一旦程序控制流被转移到异常处理程序或别的地方,这些栈上的数据就被销毁了。

以下代码演示了如何保持这些临时变量。




void FuncSkunk() {// Declare variables that we can use to save the exception// record and the context if an exception should occur.EXCEPTION_RECORD SavedExceptRec;CONTEXT SavedContext;__try{// ...}__except (SavedExceptRec = *(GetExceptionInformation())->ExceptionRecord,SavedContext =*(GetExceptionInformation())->ContextRecord,EXCEPTION_EXECUTE_HANDLER) {// We can use the SavedExceptRec and SavedContext// variables inside the handler code block.switch (SavedExceptRec.ExceptionCode) {//...}}
}

注意逗号表达式,最终的值是最右边的表达式,前面的表达式会被计算执行。



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;





该结构的最后两个成员NumberParametersExceptionInformation 提供关于异常的附加值。目前只有EXCEPTION_ACCESS_VIOLATION提供附加信息。其他异常都为0.

其中ExceptionInformation[0] 包含一个标志,指出了非法访问的类型。0表示线程试图访问不能访问的数据; 1 表示线程试图写入不能访问的数据。

当数据执行保护(DEP)侦测到线程执行不具备访问权限的内存页中的代码时,这个异常也会抛出,同时ExceptionInformation[0]的值被设置为8




可以编写异常过滤程序来提供关于异常重要的信息。

LONG ExpFltr(LPEXCEPTION_POINTERS pep) {TCHAR szBuf[300], *p;PEXCEPTION_RECORD pER = pep->ExceptionRecord;DWORD dwExceptionCode = pER->ExceptionCode;StringCchPrintf(szBuf, _countof(szBuf),TEXT("Code = %x, Address = %p"),dwExceptionCode, pER->ExceptionAddress);// Find the end of the string.p = _tcschr(szBuf, TEXT('\0'));// I used a switch statement in case Microsoft adds// information for other exception codes in the future.switch (dwExceptionCode) {case EXCEPTION_ACCESS_VIOLATION:StringCchPrintf(p, _countof(szBuf),TEXT("\n--> Attempt to %s data at address %p"),pER->ExceptionInformation[0] ?TEXT("write") : TEXT("read"),pER->ExceptionInformation[1]);break;default:break;}MessageBox(NULL, szBuf, TEXT("Exception"),MB_OK | MB_ICONEXCLAMATION);return EXCEPTION_CONTINUE_SEARCH;
}

例如一下测试代码





int _tmain(int argc, TCHAR* argv[], TCHAR * env[])
{__try{// ...// ...char * p = (char *)0xffffffff;char a = *p;}__except (ExpFltr(GetExceptionInformation())) {// ...}return 0;
}

运行结果:












24.7 软件异常


应用程序内部可以抛出一个自定义的异常,通常称为软件异常。

在传统开发代码中,错误处理通过函数返回值层层像上传递,这给代码维护带来了很大的麻烦。每层都要进行错误的判断和处理。也非常不方便定位发生错误的位置。

使用结构化异常可以在运行出现错误以后显式抛出异常,而在上层安置异常捕获代码try/except。捕获异常以后可以通过EXCEPTION_CONGINUE_EXECUTION等来控制代码流的走向。这极大的方便了对错误的处理和问题的定位。也使得代码的编写更加容易。




但这要求,编写代码的程序员熟悉SEH的使用。(不熟悉而滥用SEH可能会导致严重的后果并且难以被调查)

SEH是Windows操作系统提供的,这使得代码具有平台依赖性不适合进行跨平台移植。




使用一下函数可以抛出自定义的异常。

WINBASEAPI
__analysis_noreturn
VOID
WINAPI
RaiseException(_In_ DWORD dwExceptionCode,_In_ DWORD dwExceptionFlags,_In_ DWORD nNumberOfArguments,_In_reads_opt_(nNumberOfArguments) CONST ULONG_PTR * lpArguments);
第一个参数是dwExceptionCode是抛出异常的标识符ID。


如果要已定义异常代码,需要填充DWORD值的一下5个部分。









dwExceptionFlags 必须设定成0 或者EXCEPTION_NONCONTINUABLE。后者表示遇到了严重错误而不应该让进程继续运行。

如果不给RaiseException传递EXCEPTION_NONCONTINUABLE那么过滤程序就可以返回EXCEPTION_CONTINUE_EXECUTION

MS通过一些机制使得程序从RaiseException函数调用后面继续执行。




如果给RaiseException传递EXCEPTION_NONCONTINUABLE,过滤程序仍然返回了EXCEPTION_CONTINUE_EXECUTION这会导致抛出另一个异常

EXCEPTION_NONCONTINUABLE_EXCEPTION




这样except块捕获到异常时将会是一个链式结构。例如GetExceptionInformation返回的EXCEPTION_POINTERS结构

其ExceptionRecord成员指向EXCEPTION_RECORD结构,而该结构又有一个ExceptionRecord成员,指向另一个EXCEPTION_RECORD包含了之前发生的异常信息。




通常系统一次只处理一个异常。因此ExceptionRecord成员是NULL。除非在异常处理代码中又抛出了异常,这将是一个链式的异常。




RaiseException的第三个和第四个参数用来传递附加信息。通常情况下可以都传递NULL

如果nNumberOfArguments传递非0 值,那么pArguments是一个指向ULONG_PTR数组。

之后在异常处理过程,可以让异常过滤程序查看EXCEPTION_RECORD结构并处理nNumberOfArguments和pArguments参数所包含的信息。




参考MS上的一篇深度解析结构化异常的文章。

https://www.microsoft.com/msj/0197/exception/exception.aspx





推荐阅读
author-avatar
闹闹依旧不闹
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有