首页
论坛
课程
招聘
[原创]白话windows之四 异常处理机制(VEH、SEH、TopLevelEH...)
2013-6-19 01:41 32686

[原创]白话windows之四 异常处理机制(VEH、SEH、TopLevelEH...)

2013-6-19 01:41
32686
今天是我侄子的4岁生日,特此更新一篇,祝我小侄子在幼儿园多泡MM

我们都知道SEH异常处理机制,那VEH、TopLevelEH呢?他们执行的先后顺序是怎样的呢?当这些机制都不使用的情况下,会发生什么情况呢?异常处理器是怎么工作的?如果你对此感兴趣,那我们就一起来扒开异常处理机制的面纱吧

术语:
SEH: 结构化异常处理
VEH: 向量化异常处理
TopLevelEH:顶层异常处理

EXCEPTION_EXECUTE_HANDLER :该异常被处理。从异常处下一条指令继续执行
EXCEPTION_CONTINUE_SEARCH:不能处理该异常,让别人处理它吧
EXCEPTION_CONTINUE_EXECUTION:该异常被忽略。从异常处处继续执行

//调试器返回值:
DBG_CONTINUE : 等同于EXCEPTION_CONTINUE_EXECUTION
DBG_EXCEPTION_NOT_HANDLED :等同于EXCEPTION_CONTINUE_SEARCH


想想对我这等语文是体育老师教出来的童鞋来说,想把这个主题将透彻,还真是有点难度的~,我们还是按异常处理器执行顺序来吧,废话不多说,开始。。。

异常处理器其实包含 内核异常处理R3异常处理内核异常处理比较简单,我也对它没兴趣,所以这里就把它给忽略了。我们只讲R3程序产生异常时,异常处理器是怎么工作的。

异常处理器处理顺序流程
1. 交给调试器(进程必须被调试)
2. 执行VEH
3. 执行SEH
4. TopLevelEH(进程被调试时不会被执行)
5. 交给调试器(上面的异常处理都说处理不了,就再次交给调试器)
6. 调用异常端口通知csrss.exe


大致分上面几步把,下面咱就详细讨论一下各个步骤都干了哪些细活

1. 第一次交给调试器
如果该出现异常的程序正在被调试,则该异常首先交给调试器处理(通过DebugPort)。
调试器拿到这个异常后,需要判断是否要处理该异常,如果处理该异常返回DBG_CONTINUE,否则返回DBG_EXCEPTION_NOT_HANDLED
    while(!bExit) 
    {
        DWORD dwContinueStatus = DBG_EXCEPTION_NOT_HANDLED;
        DEBUG_EVENT debugEvent;
        WaitForDebugEvent(&debugEvent, INFINITE);
        switch ( debugEvent.dwDebugEventCode )
        {
        case EXCEPTION_DEBUG_EVENT:
            {
                EXCEPTION_DEBUG_INFO* pExcpInfo = &debugEvent.u.Exception;
                if ( MessageBox(0,_T("处理该异常?"), _T("我是调试器"),MB_YESNO)==IDYES )
                {
                    dwContinueStatus = DBG_CONTINUE;
                    //...
                }
            }
            break;
            //...
        }
        ContinueDebugEvent(debugEvent.dwProcessId,  debugEvent.dwThreadId, dwContinueStatus); 
    }
2. 执行VEH
这里就不讲Veh的概念了,有兴趣的去Google一下。
如果没有被调试,或者调试器返回DBG_EXCEPTION_NOT_HANDLED,则就会检查是否存在VEH。如果存在VEH,则把异常交给他们处理。
VEH是个链表,可以存在多个Veh。每个VEH按顺序被调用
一个VEH可以返回连个值:EXCEPTION_CONTINUE_SEARCHEXCEPTION_CONTINUE_EXECUTION。返回EXCEPTION_EXECUTE_HANDLER无效的,等同于EXCEPTION_CONTINUE_SEARCH。
当一个Veh返回EXCEPTION_CONTINUE_SEARCH,则把异常交给下一个VEH处理。
如果返回EXCEPTION_CONTINUE_EXECUTION,认为已经被处理,退出异常处理器在异常指令处继续执行
从执行顺序来看,VEH是在SEH之前执行的,并且不依赖某一线程,本进程中任何线程出现异常都可以被VEH处理,所以在有些时候是很有用处的。
怎么添加一个VEH呢?
LONG NTAPI FirstVectExcepHandler( PEXCEPTION_POINTERS pExcepInfo )
{
    if( ... )
    {
        return EXCEPTION_CONTINUE_EXECUTION;
    }
    return EXCEPTION_CONTINUE_SEARCH;
}
//参数1=1表示插入Veh链的头部,=0表示插入到VEH链的尾部
AddVectoredExceptionHandler( 1, &FirstVectExcepHandler );
3. 执行SEH
SEH应该是大家都比较熟悉的了。当所有的VEH都不处理该异常,该异常就会让SEH处理。
SEH是基于线程栈的异常处理机制,所以它只能处理自己线程的异常。
先看一个示例代码:
LONG FirstSEHer( PEXCEPTION_POINTERS pExcepInfo )
{
    TCHAR* pTitle = _T("第一个SEH处理器");
    _tprintf( _T("[EH.Exe] [SEH][1] in \n") );
    LONG nRet = ShowSelectMessageBox(pTitle);
    _tprintf( _T("[EH.Exe] [SEH][1] out \n") );
    return nRet;
}
LONG SecondSEHer( PEXCEPTION_POINTERS pExcepInfo )
{
    TCHAR* pTitle = _T("第二个SEH处理器");
    _tprintf( _T("[EH.Exe] [SEH][2] in \n") );
    LONG nRet = ShowSelectMessageBox(pTitle);
    _tprintf( _T("[EH.Exe] [SEH][2] out \n") );;
    return nRet;
}
void ExcepFunction()
{
	__try
	{
		__try
		{
			__try
			{

				_tprintf( _T("[EH.Exe] *[CALL] int 3\n") );
				__asm int 3;
			}
			__finally
			{
				printf( "[EH.Exe] *[SEH][0] finally call...\n" );
			}
		}
		__except( FirstSEHer(GetExceptionInformation()) )
		{
			_tprintf( _T("[EH.Exe] [SEH][1] 被俺处理了~(只有返回EXCEPTION_EXECUTE_HANDLER才会走到这里)\n"));
		}
	}
	__except( SecondSEHer(GetExceptionInformation()) )
	{
		_tprintf( _T("[EH.Exe] [SEH][2] 被俺处理了(只有返回EXCEPTION_EXECUTE_HANDLER才会走到这里)\n"));
	}
}

ExcepFunction函数有三个SEH,但是有两个Headler。当__asm int 3;被执行时就会被SEH捕获。捕获后,首先交给FirstSEHer处理,如果FirstSEHer返回EXCEPTION_CONTINUE_SEARCH则才会交给SecondSEHer处理。
FirstSEHer可以返回三个值:EXCEPTION_CONTINUE_SEARCHEXCEPTION_EXECUTE_HANDLEREXCEPTION_CONTINUE_EXECUTION
当返回EXCEPTION_CONTINUE_SEARCH,执行上一层SEH,这里执行SecondSEHer
返回EXCEPTION_EXECUTE_HANDLER时则表示异常被处理,会先把内部的__finally块执行完,再跳到自身的__except块中执行。
返回EXCEPTION_CONTINUE_EXECUTION时表示该异常被忽略,会再次执行__asm int 3处指令。如果该条汇编不被修正成其他指令(如nop),则会再次产生一个异常。

另外,如果想在 try catch的C++异常中捕获系统异常,必须让C++支持SEH异常处理。设置方法: Vc-〉项目属性-->配置属性-->c/C++-->代码生成-->启用C++异常,选中"是,但有SEH异常(/EHa)"。

4. TopLevelEH
顶层异常处理,这个其实是利用SEH实现的。在最顶层的SEH中,可以注册一个顶层异常处理器。虽然他是基于SEH实现的,但是它可以处理所有线程抛出的异常。
当SEH都处理不了该异常,在最顶层的SEH中就会检查是否注册了顶层异常处理,如果注册了,则执行顶层异常处理。
注意:如果该进程正在调试状态,顶层异常处理会被忽略,不会被执行
顶层异常处理函数也可以返回三个值:EXCEPTION_CONTINUE_SEARCHEXCEPTION_EXECUTE_HANDLEREXCEPTION_CONTINUE_EXECUTION
返回EXCEPTION_CONTINUE_EXECUTION时,和SEH一样。
返回EXCEPTION_EXECUTE_HANDLER时,则直接杀死该进程
返回EXCEPTION_CONTINUE_SEARCH时,会查注册表,检查是否存在实时调试器。注册表路径:KLM\software\microsoft\windows nt\currentvsrsion\aedebug。如果Auto==1,Debugger!=NULL则根据Debugger中指示的参数启动实时调试器,让调试器处理该异常(如果不存在顶层异常且进程没被调试,也会检查并启动实时调试器)
注册方法:
LONG NTAPI TopLevelExcepFilter( PEXCEPTION_POINTERS pExcepInfo )
{
    TCHAR* pTitle = _T("*顶级* 异常处理器");
    _tprintf( _T("[EH.Exe] [TOP] in \n") );
    LONG nRet = ShowSelectMessageBox(pTitle);
    _tprintf( _T("[EH.Exe] [TOP] out \n") );;
    return nRet;
}
//注册
SetUnhandledExceptionFilter( &TopLevelExcepFilter );
顶层异常处理通常用来生成程序Dump文件。供开发人员分析。

5. 再次交给调试器
如果上述的异常处理机制都没有处理该异常,则调试器会再次接收该异常。
调试器这个时候返回DBG_CONTINUE,则和第一次相同。
返回DBG_EXCEPTION_NOT_HANDLED,则直接杀死该进程

6. 调用异常端口通知csrss.exe
当上面提到的都没有处理该异常,则调用ExceptionPort通知csrss.exe。csrss.exe的做法是会弹出一个对话框:

这个时候还有一次修复异常并让程序继续运行的机会,就是点击“调试”按钮。其他按钮都很导致异常进程被终止。

终于写完了,为了完成该文,特意写了个小软件。里面包含一个异常产生程序(ExcepHandler)和一个简单调试器(MyDbg)源码在此:
TestException.rar
(调试器参考了超然兄的代码,特此感谢)


欢迎各位拍砖~                                                                           

第五届安全开发者峰会(SDC 2021)10月23日上海召开!

上传的附件:
收藏
点赞2
打赏
分享
最新回复 (16)
雪    币: 218
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
誓言剑 活跃值 2013-6-19 04:21
2
0
UnhandledExceptionFilter处理不了还会再一次交给VEH处理,但是这个句柄是用AddVectoredContinueHandler注册的
不过Win7下这个又有了一些变化,详见https://blogs.msdn.com/b/zhanli/archive/2010/06/25/c-tips-addvectoredexceptionhandler-addvectoredcontinuehandler-and-setunhandledexceptionfilter.aspx?Redirected=true
雪    币: 481
活跃值: 活跃值 (132)
能力值: ( LV12,RANK:250 )
在线值:
发帖
回帖
粉丝
ddlx 活跃值 5 2013-6-19 07:27
3
0
我看了一下AddVectoredContinueHandler,不支持我的Xp32
然后在我的Win8 64位上跑的时候,发现是不起效果的,使用AddVectoredContinueHandler注册的Handler不会被执行。
MSDN上也没说明一下蛋都碎了
雪    币: 819
活跃值: 活跃值 (31)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
zyicai 活跃值 2013-6-19 08:45
4
0
祝你侄子生日快乐
雪    币: 459
活跃值: 活跃值 (34)
能力值: ( LV13,RANK:400 )
在线值:
发帖
回帖
粉丝
tishion 活跃值 9 2013-6-19 09:37
5
0
AddVectoredContinueHandler是在Windows XP Professional x64 Edition以及之后的系统中才有。
雪    币: 5451
活跃值: 活跃值 (177)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Protected 活跃值 2013-6-19 09:44
6
0
学习了,谢谢了
雪    币: 459
活跃值: 活跃值 (34)
能力值: ( LV13,RANK:400 )
在线值:
发帖
回帖
粉丝
tishion 活跃值 9 2013-6-19 09:56
7
0
文中有一些基本知识的误区。

LONG FirstSEHer( PEXCEPTION_POINTERS pExcepInfo )
LONG SecondSEHer( PEXCEPTION_POINTERS pExcepInfo )

这些函数只是exception过滤函数,并不是真正的exception hanlder

真正的SEH exception handler 是每一个_try块后面跟随的_except块中的内容。

另外__ finally子句块的内容也是一个很有深度的内容,建议把这块内容也讲出来,最后__finally和_except结合在一起的时候,又会是更有趣的内容。
雪    币: 481
活跃值: 活跃值 (132)
能力值: ( LV12,RANK:250 )
在线值:
发帖
回帖
粉丝
ddlx 活跃值 5 2013-6-19 10:52
8
0
你说FirstSEHer只是exception过滤函数这个观点我不认同。其实FirstSEHer可以做任何事。反而__except内部可以做的事情就受到限制了,因为只有FirstSEHer返回EXCEPTION_EXECUTE_HANDLER时才会进入__except内。

__try _except __ finally是VC辅助实现的,SEH链的构造和分发过程确实有好多值得学习和借鉴的地方。过段时间打算写一个可以在非VC(gcc、clong等)中使用的SEH框架。

感谢关注
雪    币: 481
活跃值: 活跃值 (132)
能力值: ( LV12,RANK:250 )
在线值:
发帖
回帖
粉丝
ddlx 活跃值 5 2013-6-19 10:55
9
0
不知道为啥Win8上失效了,WIn7好像也不可以
雪    币: 346
活跃值: 活跃值 (27)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
学雄 活跃值 1 2013-6-19 11:55
10
0
先顶再看
雪    币: 255
活跃值: 活跃值 (13)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
AioliaSky 活跃值 1 2013-6-19 12:15
11
0
慢慢看,学习了
雪    币: 1040
活跃值: 活跃值 (288)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Rookietp 活跃值 2013-7-15 14:14
12
0
好文章,学习了~
雪    币: 108
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
jylaxp 活跃值 2013-7-15 23:49
13
0
不得不读
雪    币: 728
活跃值: 活跃值 (131)
能力值: ( LV9,RANK:200 )
在线值:
发帖
回帖
粉丝
房有亮 活跃值 3 2013-7-16 05:05
14
0
好文章,学习了~
雪    币: 43
活跃值: 活跃值 (10)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
AnChen 活跃值 2014-2-7 16:27
15
0
MSDN上有如下说明:
To compile an application that uses this function, define the _WIN32_WINNT macro as 0x0500 or later. For more information, see Using the Windows Headers.

我印象中,较老的编译器编译出来的EXEPE头中,OS Version和Subsystem Version两个成员值都是4.0,也就意味着在Win2K上都能跑。

AddVectoredContinueHandler是XP后才出现的,所以只能支持Win5.1及其以后版本,所以OS Version和Subsystem Version必须大于5.0。

我现在用着Win8 64位,弄弄看是不是这个问题
雪    币: 43
活跃值: 活跃值 (10)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
AnChen 活跃值 2014-2-7 16:31
16
0
我的Win8 64位AddVectoredContinueHandler注册的Handler有效,跑起了,用的是12.0的编译器,OS Version和Subsystem Version两个成员值是6.0。但我把他俩改成4.0,VEH还是有效。。。。纳闷了
雪    币: 218
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
誓言剑 活跃值 2014-2-7 22:08
17
0
PE文件头那两个值只在加载时有用,跟执行无关
游客
登录 | 注册 方可回帖
返回