第24章 SEH结构化异常处理—异常处理及软件异常

Posted 浅墨浓香

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第24章 SEH结构化异常处理—异常处理及软件异常相关的知识,希望对你有一定的参考价值。

24.1  程序的结构

(1)try/except框架

__try{

   //被保护的代码块
       ……
}

__except(except fileter/*异常过滤程序*/){
    //异常处理程序
}

(2)说明

  ①当__try块中的代码发生异常时,__except()中的过滤程序就被调用。

  ②过滤程序可以是一个简单的表达式或一个函数(返回值应为EXCEPTION_CONTINUE_SEARCH、EXCEPT_CONTINUE_EXECUTE或EXCEPT_EXECUTE_HANDLER之一)

  ③过滤表达式中可以调用GetExceptionCode和GetExceptionInformation函数取得正在处理的异常信息。但这两个函数不能在异常处理程序中使用。

  ④与try/finally不同,try/except中可以使用return、goto、continue和break,它们并不会导致局部展开。

24.2 异常过滤程序

(1)返回值

标识

说明

EXCEPTION_EXECUTE_HANDLER

1

执行except花括号内代码,同时执行全局展开。最后程序从except花括号后面的第一句代码继续运行。

EXCEPTION_CONTINUE_SEARCH

0

向外层查找带except的try块,并调用对应的异常过滤程序。

EXCEPTION_CONTINUE_EXECUTE

-1

重新执行导致异常的那条CPU指令本身。

(2)全局展开——异常过滤程序返回EXCEPTION_EXECUTE_HANDLER是会执行全局执开

  ①当某个__try块中的代码触发了异常时(也可能是__try块中调用的函数中引发异常),操作系统会从最靠近引发异常代码的地方开始从下层往上层查找__except块(这里的层是指__try块的嵌套层),对于找到的每一个__except块,会先计算它的异常过滤器,如果过滤器返回EXCEPTION_CONTINUE_SEARCH,则说明此__except块不处理此类异常,需要继续往上层查找,如果某过滤器返回EXCEPTION_EXECUTE_HANDLER则说明此__except块可以处理此类异常,即找到了异常的处理代码,此时停止查找,但是在执行该__except块中的异常处理代码之前,要先进行全局展开。

  ②全局展开的过程与查找__except块的过程类似,只不过这次是查找从底层向上查找__finally块,查找过程中遇到的每一个__finally块中的代码都被执行,直到查找到前面说的处理异常的__except块那一层停止,这时全局展开完成,然后执行__except块中的异常处理代码。

  ③执行完异常处理代码之后,指令流从__except块后的第一条指令开始。从这里也可以看出全局展开也是为了保证__finally语义的正确性,因为指令流从引发异常代码转到到__except异常处理代码时也导致了指令流从__try块嵌套层中所有与__finally对应的__try块中流出,由前面的__finally语义说明可知,此时必须要执行全局展开过程以包成__finally语义的正确性。

(3)停止全局展开——将return置于finally块中可阻止全局展开。【未定义行为,VC2013直接报错了!】

(4)慎用EXCEPTION_CONTINUE_EXECUTION

  ①尝试修复错误,出现失败的实例分析

  *pchBuffer = TEXT("J");//C/C++语句

  //编译后的产生的机器指令
  MOV EAX,DWORD PTR[pchBuffer]
  MOV WORD PTR[EAX], J  //导致异常的指令。当异常过滤程序捕获该异常后,修正
                          //pchBuffer,让其指向一个正确的地址。并让系统重新
                          //执行第二要CPU指令。问题在于寄存器不可能自动更新
                          //以反映变量pchBuffer的更新,于是该异常又致另一个导
                          //异常,程序陷入了死循环

  ②虚拟内存结合SEH可实现按需调拨存储器,有时能写出运行速度快和高效的应用程序(见第15章的《如何预订大块地址空间和为地址空间稀疏调拨存储器》

  ③系统为线程栈建了一个SEH框。当线程试图访问栈中尚未调拨存储器的区域时,会引发一个异常。系统内部的异常过滤程序会捕获到该异常并在内部调用VirtualAlloc为线程栈调拨更多的存储,并且返回EXCEPTION_CONTINUE_EXECUTION让原先抛出异常的指令重新执行下去。

【SEHAndMemory】演示虚拟内存的按需调拨

技术分享

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

#define PAGELIMIT 80
LPBYTE lpNxtPage;
DWORD dwPages = 0;
DWORD dwPageSize;//页面大小,一般为4KB

INT PageFualtExceptionFilter(DWORD dwCode){
    LPVOID lpvResult;

    //不是非法访问内存
    if (dwCode !=EXCEPTION_ACCESS_VIOLATION){
        return EXCEPTION_EXECUTE_HANDLER;//执行except块的异常处理程序代码
    }

    //当超过指定的页面数时
    if (dwPages >=PAGELIMIT){
        return EXCEPTION_EXECUTE_HANDLER;//执行except块的异常处理程序代码
    }

    //非法访问内存,则为预订的空间提交下一页物理存储器
    lpvResult = VirtualAlloc((LPVOID)lpNxtPage, dwPageSize, MEM_COMMIT, PAGE_READWRITE);
    if (lpvResult == NULL){
        return EXCEPTION_EXECUTE_HANDLER;//执行except块的异常处理程序代码
    }

    //提交成功
    dwPages++;
    lpNxtPage += dwPageSize;

    _tprintf(_T("第%d页提交成功!\n"), dwPages);
    return EXCEPTION_CONTINUE_EXECUTION; //重新执行触发异常的那条CPU指令
}

int main(){
    _tsetlocale(LC_ALL, _T("chs"));

    LPVOID lpvBase;LPTSTR lpPtr;BOOL bSuccess;
    SYSTEM_INFO sSysInfo;
    GetSystemInfo(&sSysInfo);
    dwPageSize = sSysInfo.dwPageSize;

    _tprintf(_T("CPU页面大小为%dKB.\n"), sSysInfo.dwPageSize / 1024);

    //预订存储器
    lpvBase = VirtualAlloc(NULL, PAGELIMIT*dwPageSize, MEM_RESERVE, PAGE_NOACCESS);

    lpPtr = (LPTSTR)(lpNxtPage = (LPBYTE)lpvBase);
    for (DWORD i = 0; i < PAGELIMIT*dwPageSize/sizeof(TCHAR);i++){
        __try{
            lpPtr[i] = _T(a);//写入一个字节的数据
        }
        __except (PageFualtExceptionFilter(GetExceptionCode())){
            _tprintf(_T("异常被处理\n"));
            //ExitProcess(GetLastError());
        }
    }

    bSuccess = VirtualFree(lpvBase, 0, MEM_RELEASE);
    _tprintf(_T("释放操作%s.\n"), bSuccess ? _T("成功") : _T("失败"));
    _tsystem(_T("PAUSE"));
    return 0;
}

24.3 GetExceptionCode

(1)GetExceptionCode是个内联函数,其代码直接嵌入到被调用的地方(注意与函数调用的区别),它的返回值表明刚刚发生的异常的类型(定义在WinBase.h中,如EXCEPTION_ACCESS_VIOLATION)

(2)该函数只能在异常过滤程序里(即__except之后的小括号内)或者异常处理程序的代码里调用(__except块后面的花括号内),但不能在异常过滤函数中使用。

//合法代码

__try{

     y = 0;

     x = / y;

}

__except ((GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO) ?

       EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH){ //在__except块的小括内使用,合法

     switch (GetExceptionCode()){ //__except块的花括中使用,合法!

      ......

     }

}

//非法代码

LONG MyFilter(void){}

{

     //在异常过滤函数中使用GetExceptionCode,不合法!

     return ((GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO) ?

         EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH);

}

 

__try{

     y = 0;

     x = 4 / y;

}

__except(MyFilter()){ //可改成将GetExceptionCode作为参数传给MyFilter的形式。

    //处理异常

}

(3)异常错误代码的规则

31-30

29

28

27-16

15-0

内容

严重性

Microsoft/

Customer

保留位

设备代码

异常代码

含义

0=Success

1=Informational

2=Warning

3=error

0=Mircosoft所定义的代码

1=Customer所定义的代码

一直为0

前256个值为Micorsoft所保留。(如FACILITY_NULL(0)表示该异常可以在系统任何设备出现,并不只发生在一些特定的设备上)

由Microsoft/

Customer所定义的代码

24.4 GetExceptionInformation

(1)GetExceptionInformation可获取异常发生时,系统向发生异常的线程栈中压入的EXCEPTION_RECORD、CONTEXT和EXCEPTION_POINTERS结构中的异常信息或CPU有关的信息

(2)这个函数只能在异常过滤程序中调用(即__except块的小括号),因为EXCEPT_RECORD、CONTEXT和EXCEPTION_POINTER数据结构只有在系统计算异常过滤程序时才有效。一旦控制流被转移到其他地方,这些栈上的数据结构会被销毁。但我们可以自己保存他们,以备后用。

//保存栈中异常信息的方法
void FuncSkunk(){
    //声明一些可以保存异常信息的结构体,须在try块外面声明
    EXCEPTION_RECORD SavedExceptRec;
    CONTEXT          SavedContext;
 
    __try{
 
    }
    __except ( //注意逗号表达式,取最后一个表达式为整个表达式的值。
        SavedExceptRec = *(GetExceptionInformation())->ExceptionRecord,
        SavedContext = *(GetExceptionInformation())->ContextRecord,
        EXCEPTION_EXECUTE_HANDLER){
        //异常处理
    }
}

(3)EXCEPTION_RECORD结构体——刚发生的异常的详细信息

字段

说明

DWORD ExceptionCode

异常代码,就是GetExceptionCode函数的返回值

DWORD ExceptionFlags

异常标志

0—表示继续的异常;EXCEPTION_NONCONTINUABLE—不可继续的异常,如果程序试图在一个不可继续的异常之后继续执行,会引发EXCEPTION_NONCONTINUABLE_EXCEPTION异常。

PEXCEPTION_RECORD pExceptionRecord

指向另一个未处理异常的EXCEPTION_RECORD结构。(即嵌套异常发生时,异常会形成异常链)

PVOID ExceptionAddress

导致异常的CPU指令的地址

DWORD NumberParameters

ExceptionInformation数组里元素的个数。对绝大部分的异常来说,这个值为0。

ULONG_PTR ExceptionInformation

[EXCEPTION_MAXIMUM_PARAMETERS]

描述异常的附加参数数组,对绝大部分的异常来说,这个数组元素都未定义。

24.5 软件异常——RaiseException函数

参数

说明

DWORD dwExceptionCode

要抛出异常的标识符,可参考《异常错误代码规则》来编写

DWORD dwExceptionFlags

必须下列两者之一

0:

EXCEPTION_NONCONTINUABLE:异常不可继续,即不能再异常过滤程序中返回EXCEPTION_CONTINUE_EXECUTE,否则重新执行那条导致错误的CPU指令会继续抛出一个新的EXCEPTION_NONCONTINUABLE_EXCEPTION异常。

DWORD nNumberOfArguments

用来传递有关抛出异常的附加信息。一般不需要。可将nNumberOfArgument设为0。pArguments设为NULL。

Const ULONG_PTR* pArguments

返回值

void

 【RaiseException程序】——演示自己抛出的软件异常

技术分享

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

DWORD FilterFunction(){
    _tprintf(_T("1")); //第1句被输出的语句
    return EXCEPTION_EXECUTE_HANDLER;
}

int main(){
    __try{
        __try{
            RaiseException(1, 0, 0, NULL);
        }
        __finally{
            _tprintf(_T("2")); //第2句被输出的语句
        }
    }
    __except (FilterFunction()){
        _tprintf(_T("3\n")); //第3句被输出的语句
    }
    _tsystem(_T("PAUSE"));
return 0; }

 

以上是关于第24章 SEH结构化异常处理—异常处理及软件异常的主要内容,如果未能解决你的问题,请参考以下文章

SEH结构化异常处理

解析结构化异常处理(SEH)(第二部分)

SEH结构化异常处理

SEH 结构化异常

结构化异常SEH处理机制详细介绍(二)

内存保护机制及绕过方案——从堆中绕过safeSEH