2023年1月

根据异常来源,一般分硬件异常和软件异常,它们处理的流程大致一样,下面简单讲一下。

如果是硬件异常,CPU会根据中断类型号转而执行对应的中断处理程序。CPU会在IDT中查找对应的函数来处理,各个异常处理函数不仅仅处理异常还需要将异常信息封装,以便对后续处理,KiTrapXX例程在完成针对本异常的特别动作后,通常会调用CommonDispatchException函数,它会在栈中分配一个EXCEPTION_RECORD结构,并把异常信息存储到该结构中。在准备好这个结构后,它会调用内核中的KiDispatchExcption函数来分发异常。

如果是软甲异常,是通过直接或间接调用内核服务KiRaiseException而产生的。函数内部会把Context上下背景文复制到当前线程的内核栈,接下来调用KiDispatchExcption函数来进行分发。

综上所述,不管什么异常最后都会调用内核中的KiDispatchExcption函数进行分发,也就是说Windows用统一的方式来管理。异常封装完成后,系统会调用nt!KiDispatchException来处理异常,所以分析KiDispatchException函数就可以了解异常是如何被处理的。

首先来看看KiDisPatchException函数的函数原型

void KiDispatchException (
IN PEXCEPTION_RECORD ExceptionRecord,
IN PKEXCEPTION_FRAME ExceptionFrame,
IN PKTRAP_FRAME TrapFrame,
IN KPROCESSOR_MODE PreviousMode,
IN BOOLEAN FirstChance
)

ExceptionRecord也就是前面提的描述异常的结构,TrapFrame指向的结构用来描述发生异常时候的上下文,PreviousMode来说明异常来自Kernel还是User,最后的FirstChance用来表示异常是不是第一次被处理,实际上这些结构的集合就形成了一个虚拟的、完整的“异常”结构,再去进行下面的处理。下面先来看看KiDispatchException 的分发示意图:

从图中我们可以看到,KiDispatchException会先调用KeContextFromKframes函数,目的是根据TrapFrame参数指向的KTRAP_FRAME结构产生一个CONTEXT结构,以供向调试器和异常处理器函数报告异常时使用。右边是内核的异常,左边边是用户的异常。

内核异常处理分发流程:

但PreviousMode为0时,就会进入Kernel的异常分发,系统会维护一个KiDebugRoutine的函数,当内核的调试器启动时,它就帮我们把异常送往了内核调试器,而在未启动时,它只是一个“存根”函数(stub),返回一个False。这一步也就是图中的debug

当第一次debug返回False后会接着调用RtlDispatchException,试图寻找已经注册的结构化异常处理器(SEH),函数的原型:BOOLEAN RtlDispatchException(PEXCEPTION_RECORD ExceptionRecord,PCONTEXT ContextRecord)。如果没有处理的话就会进行第二轮调试,重复上面的debug内容,如果依然是没有启用调试器的话就那么就会把这个异常当作UnhandleException,也就是我们常说的未处理异常,在kernel下未处理异常可是个大问题,毕竟这可是操作系统最最重要的也最最完善的内核,这样的未处理异常一般都不是小问题,为了防止异常引发更大的问题,这时候系统就会调用KeBugCheckEx中止系统运行显示蓝屏,并将导致异常的地址打印在屏幕上。

具体步骤如下:

    ①系统会先检测是否有内核调试器,如果没有,就跳过这一步,如果有,就把异常处理的权限交给内核调试器,并且注明是第一次来执行的这个异常(FirstChance),内核调试器如果处理了该异常就继续回到原来异常地方继续执行,如果没有处理则发生中断,将控制权交给用户,用户决定是否继续处理

结构化异常处理(SEH)是Windows操作系统提供的强大异常处理功能。而Visual C++中的__try{}/__finally{}和__try{}/__except{}结构本质上是对Windows提供的SEH的封装。

一、分类

  • Per-Thread类型SEH(也称为线程异常处理),用来监视某线程代码是否发生异常。
  • Final类型SEH(也称为进程异常处理、筛选器或顶层异常处理),用于监视整个进程中所有线程是否发生异常。在整个进程中,该类型异常处理过程只有一个,可通过SetUnhandledExceptionFilter设置。

二、SEH相关的数据结构和API

2.1、相关结构

1) 线程信息块TIB(Thread Information Block或TEB)


复制代码

本文将全面阐述__try,__except,__finally,__leave异常模型机制,它也即是Windows系列操作系统平台上提供的SEH模型。SEH实际包含两个主要功能:结束处理(termination handling)和异常处理(exception handling)。每当你建立一个try块,它必须跟随一个finally块或一个except块。一个try 块之后不能既有finally块又有except块。但可以在try - except块中嵌套try - finally块,反过来也可以。__try  __finally关键字用来标出结束处理程序两段代码的轮廓。不管保护体(try块)是如何退出的。不论你在保护体中使用return,还是goto,或者是longjump,结束处理程序(finally块)都将被调用。在try使用__leave关键字会引起跳转到try块的结尾。

一、try/except框架

SEH的异常处理模型主要由try-except语句来完成,它与标准C++所定义的异常处理模型非常类似,也都是可以定义出受监控的代码模块,以及定义异常处理模块等。代码框架如下:

__try{

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

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

在一个函数中,可以有多个try-except语句。它们可以是一个平面的线性结构,也可以是分层的嵌套结构。为了与C++异常处理模型相区别,VC编译器对关键字做了少许变动。首先是在每个关键字加上两个下划线作为前缀,这样既保持了语义上的一致性,另外也尽最大可能来避免了关键字的有可能造成名字冲突而引起的麻烦等;其次,C++异常处理模型是使用catch关键字来定义异常处理模块,而SEH是采用__except关键字来定义。并且,catch关键字后面往往好像接受一个函数参数一样,可以是各种类型的异常数据对象;但是__except关键字则不同,它后面跟的却是一个表达式(可以是各种类型的表达式)。从这个框架我们可以看到由三块组成:__try块,__except过滤表达式和异常处理块。

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

②过滤程序可以是一个简单的表达式或一个函数(返回值应为EXCEPTION_CONTINUE_SEARCH、EXCEPT_CONTINUE_EXECUTE或EXCEPT_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语义的正确性。

③过滤表达式中可以调用GetExceptionCode和GetExceptionInformation函数取得正在处理的异常信息。但这两个函数不能在异常处理程序中使用。GetExceptionCode是个内联函数,其代码直接嵌入到被调用的地方(注意与函数调用的区别),它的返回值表明刚刚发生的异常的类型(定义在WinBase.h中,如EXCEPTION_ACCESS_VIOLATION)。GetExceptionInformation可获取异常发生时,系统向发生异常的线程栈中压入的EXCEPTION_RECORD、CONTEXT和EXCEPTION_POINTERS结构中的异常信息或CPU有关的信息

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

二、try/finally框架

SEH的异常结束处理模型主要由try-finally语句来完成。终止处理就是保证应用程序在一段被保护的代码发生中断后(无论是异常还是其他)还能够执行清理工作,清理工作包括关闭文件、清理内存等。代码框架如下:

__try{

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

//终止处理
}

__try/__finally的特点

①finally块总是保证,无论__try块中的代码有无异常,finally块总是被调用执行。

②try块后面只能跟一个finally块或except块,要跟多个时只能用嵌套,但__finally块不可以再嵌套SEH块,except块中可以嵌套SEH块。

③利用try/finally可以使代码的逻辑更清楚,在try块中完成正常的逻辑,finally块中完成清理工作,使代码可读性更强,更容易维护。

④当指令从__try块底部自然流出时,会执行finally块

⑤局部展开时:从try块中提前退出(由goto、longjump、continue、break、return等语句引发)将程序控制流强制转入finally块,这时就会进行局部展开(但ExitProcess、ExitThread、TerminateProcess、TerminateThread等原因导致的提前离开除外,因为这会直接终止线/进程,而不能展开)。说白了,局部展开就是将__finally块的代码提前到上述那几种语句之前执行。

⑥全局展开时也会引发finally块的执行。从Vista开始,须显示保护try/finally框架,以确保抛出异常时finally会被执行。即try/finally块外面的某层要使用try/except块保护且except中的过滤函数要返回EXCEPTION_EXECUTE_HANDLER。Vista以前的Windows,会在线程的入口点处用try/except加以保护,但Vista为了提高Windows错误报告(WER)记录的可靠性,将这个入口点的异常过滤程序返回为EXCEPTION_CONTINUE_SEARCH,最后进程会被终止,从而导致finally块没有机会被执行。(关于全局展开见第24章的相关的部分)。

⑦如果异常发生在异常过滤程序里,终止处理程序也不会被执行。

⑧finally块被执行的原因总是由以上三种情况之一引起。可以调用AbnormalTermination函数来查看原因。该函数是个内联函数,当正常流出时会返回FALSE,局部或全局展开时返回TRUE。

 三、__leave关键字

①该关键字只能用在try/finally框架中,它会导致代码执行控制流跳转到到try块的结尾,也可以认为是try后的闭花括号处。

②这种情况下,代码执行是正常从try块进入finally,所以不会进行局部展开。

③但一般需定义一个布尔变量,指令离开try块时,函数执行的结果是成功还是失败,然后在finally块中可根据这个(或这些)变量以决定资源是否需要释放。

参考:

https://www.cnblogs.com/5iedu/p/5228946.html

异常处理是C++的一项语言机制,用于在程序中处理异常事件。异常事件在C++中表示为异常对象。异常事件发生时,程序使用throw关键字抛出异常表达式,抛出点称为异常出现点,由操作系统为程序设置当前异常对象,然后执行程序的当前异常处理代码块,在包含了异常出现点的最内层的try块,依次匹配catch语句中的异常对象(只进行类型匹配,catch参数有时在catch语句中并不会使用到)。若匹配成功,则执行catch块内的异常处理语句,然后接着执行try…catch…块之后的代码。如果在当前的try…catch…块内找不到匹配该异常对象的catch语句,则由更外层的try…catch…块来处理该异常;如果当前函数内所有的try…catch…块都不能匹配该异常,则递归回退到调用栈的上一层去处理该异常。如果一直退到主函数main()都不能处理该异常,则调用系统函数terminate()终止程序。

一、抛异常---throw

throw是个关键字,与抛出表达式构成了throw语句。其语法为:throw 表达式;throw语句必须包含在try块中,也可以是被包含在调用栈的外层函数的try块中。执行throw语句时,throw表达式将作为对象被复制构造为一个新的对象,称为异常对象。异常对象放在内存的特殊位置,该位置既不是栈也不是堆,在window上是放在线程信息块TIB中。这个构造出来的新对象与本级的try所对应的catch语句进行类型匹配。如下:

在vc++里,throw关键字其实是对RaiseException函数的封装,代码如下:

__declspec(noreturn) extern "C" void __stdcall
#if !defined(_M_ARM) || defined(_M_ARM_NT)
_CxxThrowException(
#else
__CxxThrowException(
#endif
        void*           pExceptionObject,   // The object thrown
        _ThrowInfo*     pThrowInfo          // Everything we need to know about it
) {
        EHTRACE_ENTER_FMT1("Throwing object @ 0x%p", pExceptionObject);

        static const EHExceptionRecord ExceptionTemplate = { // A generic exception record
            EH_EXCEPTION_NUMBER,            // Exception number
            EXCEPTION_NONCONTINUABLE,       // Exception flags (we don't do resume)
            NULL,                           // Additional record (none)
            NULL,                           // Address of exception (OS fills in)
            EH_EXCEPTION_PARAMETERS,        // Number of parameters
            {   EH_MAGIC_NUMBER1,           // Our version control magic number
                NULL,                       // pExceptionObject
                NULL,
#if _EH_RELATIVE_OFFSETS
                NULL                        // Image base of thrown object
#endif
            }                      // pThrowInfo
        };
        EHExceptionRecord ThisException = ExceptionTemplate;    // This exception

        ThrowInfo* pTI = (ThrowInfo*)pThrowInfo;
        if (pTI && (THROW_ISWINRT( (*pTI) ) ) )
        {
            ULONG_PTR *exceptionInfoPointer = *reinterpret_cast<ULONG_PTR**>(pExceptionObject);
            exceptionInfoPointer--; // The pointer to the ExceptionInfo structure is stored sizeof(void*) infront of each WinRT Exception Info.

            WINRTEXCEPTIONINFO** ppWei = reinterpret_cast<WINRTEXCEPTIONINFO**>(exceptionInfoPointer);
			pTI = (*ppWei)->throwInfo;

			(*ppWei)->PrepareThrow( ppWei );
        }

        //
        // Fill in the blanks:
        //
        ThisException.params.pExceptionObject = pExceptionObject;
        ThisException.params.pThrowInfo = pTI;
#if _EH_RELATIVE_OFFSETS
        PVOID ThrowImageBase = RtlPcToFileHeader((PVOID)pTI, &ThrowImageBase);
        ThisException.params.pThrowImageBase = ThrowImageBase;
#endif

        //
        // If the throw info indicates this throw is from a pure region,
        // set the magic number to the Pure one, so only a pure-region
        // catch will see it.
        //
        // Also use the Pure magic number on Win64 if we were unable to
        // determine an image base, since that was the old way to determine
        // a pure throw, before the TI_IsPure bit was added to the FuncInfo
        // attributes field.
        //
        if (pTI != NULL)
        {
            if (THROW_ISPURE(*pTI))
            {
                ThisException.params.magicNumber = EH_PURE_MAGIC_NUMBER1;
            }
#if _EH_RELATIVE_OFFSETS
            else if (ThrowImageBase == NULL)
            {
                ThisException.params.magicNumber = EH_PURE_MAGIC_NUMBER1;
            }
#endif
        }

        //
        // Hand it off to the OS:
        //

        EHTRACE_EXIT;
#if defined(_M_X64) && defined(_NTSUBSET_)
        RtlRaiseException( (PEXCEPTION_RECORD) &ThisException );
#else
        RaiseException( ThisException.ExceptionCode,
                        ThisException.ExceptionFlags,
                        ThisException.NumberParameters,
                        (PULONG_PTR)&ThisException.params );
#endif
}

Windbg里的K*命令显示给定线程的堆栈帧以及相关信息,对于我们调试时,进行调用栈回溯有很大的帮助。

一、K*命令使用方式

在不同平台上,K*命令的使用组合如下

User-Mode, x86 Processor

[~Thread] k[b|p|P|v] [c] [n] [f] [L] [M] [FrameCount]
[~Thread] k[b|p|P|v] [c] [n] [f] [L] [M] =BasePtr[FrameCount]
[~Thread] k[b|p|P|v] [c] [n] [f] [L] [M] =BasePtrStackPtrInstructionPtr
[~Thread] kd [WordCount]

Kernel-Mode, x86 Processor

[Processor] k[b|p|P|v] [c] [n] [f] [L] [M] [FrameCount]
[Processor] k[b|p|P|v] [c] [n] [f] [L] [M] =StackPtrFrameCount
[Processor] k[b|p|P|v] [c] [n] [f] [L] [M] =BasePtrStackPtrInstructionPtr
[Processor] kd [WordCount]

User-Mode, x64 Processor

[~Thread] k[b|p|P|v] [c] [n] [f] [L] [M] [FrameCount]
[~Thread] k[b|p|P|v] [c] [n] [f] [L] [M] =StackPtrFrameCount
[~Thread] k[b|p|P|v] [c] [n] [f] [L] [M] =StackPtrInstructionPtrFrameCount
[~Thread] kd [WordCount]

Kernel-Mode, x64 Processor

[Processor] k[b|p|P|v] [c] [n] [f] [L] [M] [FrameCount]
[Processor] k[b|p|P|v] [c] [n] [f] [L] [M] =StackPtrFrameCount
[Processor] k[b|p|P|v] [c] [n] [f] [L] [M] =StackPtrInstructionPtrFrameCount
[Processor] kd [WordCount]

User-Mode, ARM Processor

[~Thread] k[b|p|P|v] [c] [n] [f] [L] [M] [FrameCount]
[~Thread] k[b|p|P|v] [c] [n] [f] [L] [M] =StackPtrFrameCount
[~Thread] k[b|p|P|v] [c] [n] [f] [L] [M] =StackPtrInstructionPtrFrameCount
[~Thread] kd [WordCount]

Kernel-Mode, ARM Processor

[Processor] k[b|p|P|v] [c] [n] [f] [L] [M] [FrameCount]
[Processor] k[b|p|P|v] [c] [n] [f] [L] [M] =StackPtrFrameCount
[Processor] k[b|p|P|v] [c] [n] [f] [L] [M] =StackPtrInstructionPtrFrameCount
[Processor] kd [WordCount]

二、参数说明

  • Thread
    指定要显示其堆栈的线程。如果省略此参数,将显示当前线程的堆栈。只能在用户模式下指定线程。
  • Processor
    指定要显示其堆栈的处理器。


  • b
    显示传递给堆栈跟踪中每个函数的前三个参数。