【VS开发】关于SEH的简单总结
但那真的只是皮毛,一直对Windows异常处理的原理似懂非懂, 看了下面的文章 ,一切都豁然开朗.
1997年文章,Windows技术的根一直没变:http://www.microsoft.com/msj/0197/exception/exception.aspx
Matt Pietrek 著
董岩 译
了。当你考虑Win32结构化异常处理时,也许会想到__try、__finally和__except等术语。可能你在任何一本讲解Win32的好书上 都能找到关于SEH较为详细的描述,甚至Win32 SDK文档也对使用__try、__finally和__except进行结构化异常处理进行了相当完整的描述。
层把原始的操作系统SEH的复杂性封装起来的时候,它同时也把原始的操作系统SEH的细节隐藏了起来。
应的源代码。)
__cdecl _except_handler( struct _EXCEPTION_RECORD *ExceptionRecord,
void * EstablisherFrame,
struct _CONTEXT *ContextRecord,
void * DispatcherContext);
DWORD ExceptionCode;
DWORD ExceptionFlags;
struct _EXCEPTION_RECORD *ExceptionRecord;
PVOID ExceptionAddress;
DWORD NumberParameters;
DWORD ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD;
构的指针。此结构在WINNT.H中定义,它代表某个特定线程的寄存器值。图1显示了CONTEXT结构的成员。当用于SEH时,CONTEXT结构表示 异常发生时寄存器的值。顺便说一下,这个CONTEXT结构就是GetThreadContext和SetThreadContext这两个API中使用 的那个CONTEXT结构。
{
DWORD ContextFlags;
DWORD Dr0;
DWORD Dr1;
DWORD Dr2;
DWORD Dr3;
DWORD Dr6;
DWORD Dr7;
FLOATING_SAVE_AREA FloatSave;
DWORD SegGs;
DWORD SegFs;
DWORD SegEs;
DWORD SegDs;
DWORD Edi;
DWORD Esi;
DWORD Ebx;
DWORD Edx;
DWORD Ecx;
DWORD Eax;
DWORD Ebp;
DWORD Eip;
DWORD SegCs;
DWORD EFlags;
DWORD Esp;
DWORD SegSs;
} CONTEXT;
C++运行时库源代码中的EXSUP.INC文件中:
prev dd ?
handler dd ?
_EXCEPTION_REGISTRATION ends
下图是在SoftICE中的结果:
译者注)
The Hood专栏中,我介绍了一个关键的Win32数据结构——线程信息块(Thread Information/Environment Block,TIB或TEB)。这个结构的某些域在Windows
NT、Windows 95、Win32s和OS/2上是相同的。TIB的 第一个DWORD是一个指向线程的EXCEPTION_REGISTARTION结构的指针。在基于Intel处理器的Win32平台上,FS寄存器总是 指向当前的TIB。因此在FS:[0]处你可以找到一个指向EXCEPTION_REGISTARTION结构的指针。
图2 _except_handler函数
handler”和“PUSH FS:[0]”)在堆栈上创建了一个EXCEPTION_REGISTRATION结构。PUSH FS:[0]这条指令保存了先前的FS:[0]中的值作为这个结构的一部分,但这在此刻并不重要。重要的是现在堆栈上有一个8字节的 EXCEPTION_REGISTRATION结构。紧接着的下一条指令(MOV FS:[0],ESP)使线程信息块中的第一个DWORD指向了新的EXCEPTION_REGISTRATION结构。(注意堆栈操作)
// MYSEH - Matt Pietrek 1997
// Microsoft Systems Journal, January 1997
// FILE: MYSEH.CPP
// 用命令行CL MYSEH.CPP编译
//==================================================
#include <stdio.h>
__cdecl
_except_handler( struct _EXCEPTION_RECORD *ExceptionRecord,
void * EstablisherFrame,
struct _CONTEXT *ContextRecord,
void * DispatcherContext )
{
unsigned i;
printf( "Hello from an exception handler\n" );
ContextRecord->Eax = (DWORD)&scratch;
return ExceptionContinueExecution;
{
DWORD handler = (DWORD)_except_handler;
{
// 创建EXCEPTION_REGISTRATION结构:
push handler // handler函数的地址
push FS:[0] // 前一个handler函数的地址
mov FS:[0],ESP // 安装新的EXECEPTION_REGISTRATION结构
}
{
mov eax,0 // 将EAX清零
mov [eax], 1 // 写EAX指向的内存从而故意引发一个错误
}
{
// 移去我们的EXECEPTION_REGISTRATION结构
mov eax,[ESP] // 获取前一个结构
mov FS:[0], EAX // 安装前一个结构
add esp, 8 // 将我们的EXECEPTION_REGISTRATION弹出堆栈
}
ExceptionContinueExecution这个值,它在EXCPT.H文件中定义。
CPU的机器上是FS:[0])指向这个链表的头部。
什么呢?原来,当异常发生时,系统遍历这个链表以查找一个(其异常处理程序)同意处理这个异常的EXCEPTION_REGISTRATION结构。在 MYSEH.CPP中,异常处理程序通过返回ExceptionContinueExecution表示它同意处理这个异常。异常回调函数也可以拒绝处理 这个异常。在这种情况下,系统移向链表的下一个EXCEPTION_REGISTRATION结构并询问它的异常回调函数,看它是否同意处理这个异常。图 4显示了这个过程。一旦系统找到一个处理这个异常的回调函数,它就停止遍历链表。
图4 查找一个处理异常的EXCEPTION_REGISTRATION结构
指针所指向的内存处写入数据而故意引发一个错误:
EXCEPTION_REGISTRATION结构。接下来安装的异常回调函数是针对main函数中的__try/__except块的。 __except块简单地打印出“Caught the exception in main()”。此时我们只是简单地忽略这个异常来表明我们已经处理了它。
// MYSEH2 - Matt Pietrek 1997
// Microsoft Systems Journal, January 1997
// FILE: MYSEH2.CPP
// 使用命令行CL MYSEH2.CPP编译
//=================================================
#include <stdio.h>
__cdecl _except_handler(
struct _EXCEPTION_RECORD *ExceptionRecord,
void * EstablisherFrame,
struct _CONTEXT *ContextRecord,
void * DispatcherContext )
{
ExceptionRecord->ExceptionCode, ExceptionRecord->ExceptionFlags );
printf( " EH_NONCONTINUABLE" );
printf( " EH_UNWINDING" );
printf( " EH_EXIT_UNWIND" );
printf( " EH_STACK_INVALID" );
printf( " EH_NESTED_CALL" );
return ExceptionContinueSearch;
{
{
// 创建EXCEPTION_REGISTRATION结构:
push handler // handler函数的地址
push FS:[0] // 前一个handler函数的地址
mov FS:[0],ESP // 安装新的EXECEPTION_REGISTRATION结构
}
{
// 移去我们的EXECEPTION_REGISTRATION结构
mov eax,[ESP] // 获取前一个结构
mov FS:[0], EAX // 安装前一个结构
add esp, 8 // 把我们EXECEPTION_REGISTRATION结构弹出堆栈
}
{
{
HomeGrownFrame();
}
{
printf( "Caught the exception in main()\n" );
}
Home Grown handler: Exception Code: C0000027 Exception Flags 2 EH_UNWINDING
Caught the Exception in main()
C++ 运行时库源代码文件EXCEPT.INC中,但Win32 SDK中并没有与之等价的定义。)
一个C++类的实例作为局部变量的函数。C++规范规定析构函数必须被调用。这带EH_UNWINDING标志的第二次回调就给这个函数一个机会去做一些 类似于调用析构函数和__finally块之类的清理工作。
EXCEPTION_REGISTRATION结构链表上移除的EXCEPTION_REGISTRATION结构高。图6显示了我说的情况。
图6 从异常展开
处理异常。它进行的操作与其它正常的异常处理回调函数有些不同,下面我会说明。
NT KERNEL32.DLL的一个内部例程。这个函数带一个参数——线程入口点函数的地址。BaseProcessStart运行在新进程的环境中,并且它调用这个进程的第一个线程的入口点函数。
{
DWORD currentESP;
DWORD exceptionCode;
{
ThreadQuerySetWin32StartAddress,
&lpfnEntryPoint,
sizeof(lpfnEntryPoint) );
__except( //过滤器表达式代码
exceptionCode = GetExceptionInformation(),
UnhandledExceptionFilter( GetExceptionInformation() ) )
{
ExitProcess( exceptionCode );
else // 服务
ExitThread( exceptionCode );
序链表上的最后一个异常处理程序。所有后来注册的异常处理程序都被安装在链表中这个结点的前面。如果lpfnEntryPoint函数返回,那么表明线程 一直运行到完成并且没有引发异常。这时BaseProcessStart调用ExitThread使线程退出。
的__except块开始执行。而__except块所做的只是调用ExitProcess函数去终止当前进程。稍微想一下你就会理解了。常识告诉我们, 如果一个进程引发了一个错误而没有异常处理程序去处理它,这个进程就会被系统终止。你在伪代码中看到的正是这些。
图8 未处理异常对话框
C++是如何在操作系统对SEH功能实现的基础上来创建它自己的结构化异常处理支持的。
// 这里是被保护的代码
}
__except (过滤器表达式) {
// 这里是异常处理程序代码
}
函数的入口处,这个新的EXCEPTION_REGISTRATION结构被放在异常处理程序链表的头部。在__try块结束后,相应的 EXCEPTION_REGISTRATION结构从这个链表的头部被移除。正如我前面所说,异常处理程序链表的头部被保存在FS:[0]处。因此,如果 你在调试器中单步跟踪时看到类似下面的指令时
或者
MOV DWORD PTR FS:[00000000],ECX
C++的运行时库中,它被称为__except_handler3。正是这个__except_handler3调用了你的过滤器表达式代码,我一会儿再接着说它。
__try/__except块,但是在堆栈上只创建一个EXCEPTION_REGISTRATION结构。同样,你可以在一个函数中嵌套使用 __try块,但Visual C++仍旧只是创建一个EXCEPTION_REGISTRATION结构。
是要深入探索结构化异常处理,那就让我们来看一看这些数据结构吧。
SEH实现并没有使用原始的EXCEPTION_REGISTRATION结构。它在这个结构的末尾添加了一些附加数据。这些附加数据正是允许单个函数 (__except_handler3)处理所有异常并将执行流程传递到相应的过滤器表达式和__except块的关键。我在Visual C++运行时库源代码中的EXSUP.INC文件中找到了有关Visual C++扩展的EXCEPTION_REGISTRATION结构格式的线索。在这个文件中,你会看到以下定义(已经被注释掉了):
; struct _EXCEPTION_REGISTRATION *prev;
; void (*handler)( PEXCEPTION_RECORD,
; PEXCEPTION_REGISTRATION,
; PCONTEXT,
; PEXCEPTION_RECORD);
; struct scopetable_entry *scopetable;
; int trylevel;
; int _ebp;
; PEXCEPTION_POINTERS xpointers;
;};
EBP。但是如果使用了SEH的话,那么无论你是否使用了FPO优化,编译器一定生成标准的堆栈帧)。 这条指令可以使EXCEPTION_REGISTRATION结构中所有其它的域都可以用一个相对于栈帧指针(EBP)的负偏移来访问。例如 trylevel域在[EBP-04]处,scopetable指针在[EBP-08]处,等等。(也就是说,这个结构是从[EBP-10H]处开始
的。)
结构)的指针所保留的空间。这个指针就是你调用GetExceptionInformation这个API时返回的指针。尽管SDK文档暗示GetExceptionInformation是一个标准的Win32 API,但事实上它是一个编译器内联函数。当你调用这个函数时,Visual C++生成以下代码:
数也是如此。此函数实际上只是返回GetExceptionInformation返回的数据结构(EXCEPTION_POINTERS)中的一个结构 (EXCEPTION_RECORD)中的一个域(ExceptionCode)的值。当Visual C++为GetExceptionCode函数生成下面的指令时,它到底是想干什么?我把这个问题留给读者。(现在就能理解为什么SDK文档提醒我们要注 意这两个函数的使用范围了。)
MOV EAX,DWORD PTR [EAX] ; 执行完毕,EAX指向EXCEPTION_RECORD结构
MOV EAX,DWORD PTR [EAX] ; 执行完毕,EAX中是ExceptionCode的值
码执行完毕之后的堆栈指针(ESP)的值(实际生成的指令为MOV DWORD PTR [EBP-18H],ESP)。这个DWORD中保存的值是函数执行时ESP寄存器的正常值(除了在准备调用其它函数时把参数压入堆栈这个过程会改变 ESP寄存器的值并在函数返回时恢复它的值外,函数在执行过程中一般不改变ESP寄存器的值)。
EBP-04 trylevel
EBP-08 scopetable数组指针
EBP-0C handler函数地址
EBP-10指向前一个EXCEPTION_REGISTRATION结构
EBP-14 GetExceptionInformation
EBP-18 栈帧中的标准ESP
C++的。把这个Visual C++生成的标准异常堆栈帧记到脑子里之后,让我们来看一下真正实现编译器层面SEH的这个Visual C++运行时库例程——__except_handler3。
struct _EXCEPTION_RECORD * pExceptionRecord,
struct EXCEPTION_REGISTRATION * pRegistrationFrame,
struct _CONTEXT *pContextRecord,
void * pDispatcherContext )
LONG trylevel;
EXCEPTION_POINTERS exceptPtrs;
PSCOPETABLE pScopeTable;
// 表明这是第一次调用这个处理程序(也就是说,并非处于异常展开阶段)
if ( ! (pExceptionRecord->ExceptionFlags
& (EXCEPTION_UNWINDING | EXCEPTION_EXIT_UNWIND)) )
{
// 在堆栈上创建一个EXCEPTION_POINTERS结构
exceptPtrs.ExceptionRecord = pExceptionRecord;
exceptPtrs.ContextRecord = pContextRecord;
// establisher栈帧低4个字节的位置上。参考前面我讲
// 的编译器为GetExceptionInformation生成的汇编代码*(PDWORD)((PBYTE)pRegistrationFrame - 4) = &exceptPtrs;
trylevel = pRegistrationFrame->trylevel;
scopeTable = pRegistrationFrame->scopetable;
if ( pRegistrationFrame->trylevel != TRYLEVEL_NONE )
{
if ( pRegistrationFrame->scopetable[trylevel].lpfnFilter )
{
PUSH EBP // 保存这个栈帧指针
// 栈帧上的所有局部变量能够在异常发生后仍然保持它的值不变。
EBP = &pRegistrationFrame->_ebp;
filterFuncRet = scopetable[trylevel].lpfnFilter();
{
if ( filterFuncRet < 0 ) // EXCEPTION_CONTINUE_EXECUTION
return ExceptionContinueExecution;
scopetable = pRegistrationFrame->scopetable;
__global_unwind2( pRegistrationFrame );
// 被清理完毕,流程要从最后一个栈帧继续执行
EBP = &pRegistrationFrame->_ebp;
__NLG_Notify( 1 ); // EAX = scopetable->lpfnHandler
// SCOPETABLE中当前正在被使用的那一个元素的内容
pRegistrationFrame->trylevel = scopetable->previousTryLevel;
pRegistrationFrame->scopetable[trylevel].lpfnHandler();
}
trylevel = scopeTable->previousTryLevel;
goto search_for_handler;
else // trylevel == TRYLEVEL_NONE
{
return ExceptionContinueSearch;
}
else // 设置了EXCEPTION_UNWINDING标志或EXCEPTION_EXIT_UNWIND标志
{
PUSH EBP // 保存EBP
EBP = &pRegistrationFrame->_ebp; // 为调用__local_unwind2设置EBP
__local_unwind2( pRegistrationFrame, TRYLEVEL_NONE )
POP EBP // 恢复EBP
return ExceptionContinueSearch;
}
数可以在两种情况下被调用,一次是正常调用,另一次是在展开阶段。其中大部分是在非展开阶段的回调。
exceptPrts,它的地址被放在[EBP-14h]处。你回忆一下前面我讲的编译器为GetExceptionInformation和 GetExceptionCode函数生成的汇编代码就会意识到,这实际上初始化了这两个函数使用的指针。
EXCEPTION_REGISTRATION结构。每个scopetable元素结构如下:
{
DWORD previousTryLevel;
DWORD lpfnFilter;
DWORD lpfnHandler;
} SCOPETABLE, *PSCOPETABLE;
问,__except_handler3是如何知道SCOPETABLE数组的哪个元素相应于外层的__try块的呢?答案是:外层__try块的索引由 SCOPETABLE结构的previousTryLevel域给出。利用这种机制,你可以嵌套任意层的__try块。previousTryLevel 域就好像是一个函数中所有可能的异常处理程序构成的线性链表中的结点一样。如果trylevel的值为0xFFFFFFFF(实际上就是-1,这个值在 EXSUP.INC中被定义为TRYLEVEL_NONE),标志着这个链表结束。
移向SCOPETABLE数组中的下一个元素,这个元素的索引由previousTryLevel域给出。如果遍历完整个线性链表(还记得吗?这个链表是 由于在一个函数内部嵌套使用__try块而形成的)都没有找到处理这个异常的代码,__except_handler3返回DISPOSITION_CONTINUE_SEARCH(原文如此,但根据_except_handler函数的定义,这个返回值应该为ExceptionContinueSearch。实际上这两个常量的值是一样的。我在伪代码中已经将其改正过来了),这导致系统移向下一个EXCEPTION_REGISTRATION帧(这个链表是由于函数嵌套调用而形成的)。
这意味着异常应该由相应的__except块处理。它同时也意味着所有前面的EXCEPTION_REGISTRATION帧都应该从链表中移除,并且相 应的__except块都应该被执行。第一个任务通过调用__global_unwind2来完成的,后面我会讲到这个函数。跳过这中间的一些清理代码, 流程离开__except_handler3转向__except块。令人奇怪的是,流程并不从__except块中返回,虽然是 __except_handler3使用CALL指令调用了它。
这个位于[EBP-04h]处的trylevel域的值的代码。
栈。如果你检查一下编译器为__except块生成的代码,你会发现它做的第一件事就是将EXCEPTION_REGISTRATION结构下面8个字节 处(即[EBP-18H]处)的一个DWORD值加载到ESP寄存器中(实际代码为MOV ESP,DWORD PTR [EBP-18H]),这个值是在函数的prolog代码中被保存在这个位置的(实际代码为MOV DWORD PTR [EBP-18H],ESP)。
么意义。当面对大堆的理论时,我最自然的做法就是写一些应用我学到的理论方面的程序。如果它能够按照预料的那样工作,我就知道我的理解(通常)是正确的。
C++生成多个EXCEPTION_REGISTRATION帧以及相应的scopetable。
// ShowSEHFrames - Matt Pietrek 1997
// Microsoft Systems Journal, February 1997
// FILE: ShowSEHFrames.CPP
// 使用命令行CL ShowSehFrames.CPP进行编译//=========================================================
#include <stdio.h>
// 本程序仅适用于Visual C++,它使用的数据结构是特定于Visual C++的
//-------------------------------------------------------------------
#error Visual C++ Required (Visual C++ specific information is displayed)
#endif
// 结构定义
//-------------------------------------------------------------------
struct EXCEPTION_REGISTRATION
{
EXCEPTION_REGISTRATION* prev;
FARPROC handler;
};
struct scopetable_entry
{
DWORD previousTryLevel;
FARPROC lpfnFilter;
FARPROC lpfnHandler;
};
struct VC_EXCEPTION_REGISTRATION : EXCEPTION_REGISTRATION
{
scopetable_entry * scopetable;
int trylevel;
int _ebp;
};
// 原型声明
//----------------------------------------------------------------
// 但是它的原型并没有出现在任何头文件中,所以我们需要自己声明它。
extern "C" int _except_handler3(PEXCEPTION_RECORD,
EXCEPTION_REGISTRATION *,
PCONTEXT,
PEXCEPTION_RECORD);
// 代码
//-------------------------------------------------------------
// 显示一个异常帧及其相应的scopetable的信息
//
void ShowSEHFrame( VC_EXCEPTION_REGISTRATION * pVCExcRec )
{
printf( "Frame: %08X Handler: %08X Prev: %08X Scopetable: %08X\n",
pVCExcRec, pVCExcRec->handler, pVCExcRec->prev,
pVCExcRec->scopetable );
{
printf( " scopetable[%u] PrevTryLevel: %08X "
"filter: %08X __except: %08X\n", i,
pScopeTableEntry->previousTryLevel,
pScopeTableEntry->lpfnFilter,
pScopeTableEntry->lpfnHandler );
}
// 遍历异常帧的链表,按顺序显示它们的信息
//
void WalkSEHFrames( void )
{
VC_EXCEPTION_REGISTRATION * pVCExcRec;
printf( "_except_handler3 is at address: %08X\n", _except_handler3 );
printf( "\n" );
__asm mov eax, FS:[0]
__asm mov [pVCExcRec], EAX
while ( 0xFFFFFFFF != (unsigned)pVCExcRec )
{
ShowSEHFrame( pVCExcRec );
pVCExcRec = (VC_EXCEPTION_REGISTRATION *)(pVCExcRec->prev);
}
{
// 嵌套3层__try块以便强制为scopetable数组产生3个元素
__try
{
__try
{
__try
{
WalkSEHFrames(); // 现在显示所有的异常帧的信息
} __except( EXCEPTION_CONTINUE_SEARCH )
{}
} __except( EXCEPTION_CONTINUE_SEARCH )
{}
} __except( EXCEPTION_CONTINUE_SEARCH )
{}
int i;
__try
{
i = 0x1234;
} __except( EXCEPTION_CONTINUE_SEARCH )
{
i = 0x4321;
}
{
Function1(); // 调用一个设置更多异常帧的函数
} __except( EXCEPTION_EXECUTE_HANDLER )
{
// 应该永远不会执行到这里,因为我们并没有打算产生任何异常
printf( "Caught Exception in main\n" );
}
__except_handler3的地址,打印它的原因很快就清楚了。接着,它从FS:[0]处获取异常链表的头指针,然后遍历该链表。此链表中每个结 点都是一个VC_EXCEPTION_REGISTRATION类型的结构,它是我自己定义的,用于描述Visual C++的异常处理帧。对于这个链表中的每个结点,WalkSEHFrames都把指向这个结点的指针传递给ShowSEHFrame函数。
道scopetable数组中有多少个元素的呢?其实我并不知道。但是我假定VC_EXCEPTION_REGISTRATION结构中的当前trylevel域的值比scopetable数组中的元素总数少1。
Visual C++运行时库函数__except_handler3的地址。这证明了我前面所说的单个回调函数处理所有异常这一点。
图11 ShowSEHFrames运行结果
块的过滤器表达式代码可以在WINXFLTR.C文件中找到。
异常处理程序之前的所有异常处理程序。
SDK文档。)
{
_RtlUnwind( pRegistFrame, &__ret_label, 0, 0 );
__ret_label:
}
PVOID returnAddr, // 并未使用!(至少是在i386机器上)
PEXCEPTION_RECORD pExcptRec,
DWORD _eax_value)
DWORD stackUserTop;
PEXCEPTION_RECORD pExcptRec;
EXCEPTION_RECORD exceptRec;
CONTEXT context;
RtlpGetStackLimits( &stackUserBase, &stackUserTop );
{
pExcptRec = &excptRec;
pExcptRec->ExceptionFlags = 0;
pExcptRec->ExceptionCode = STATUS_UNWIND;
pExcptRec->ExceptionRecord = 0;
pExcptRec->ExceptionAddress = [ebp+4]; // RtlpGetReturnAddress()—获取返回地址
pExcptRec->ExceptionInformation[0] = 0;
}
pExcptRec->ExceptionFlags |= EXCEPTION_UNWINDING;
else // 这两个标志合起来被定义为EXCEPTION_UNWIND_CONTEXT
pExcptRec->ExceptionFlags|=(EXCEPTION_UNWINDING|EXCEPTION_EXIT_UNWIND);
CONTEXT_INTEGER | CONTEXT_SEGMENTS);
context.Eax = _eax_value;
pExcptRegHead = RtlpGetRegistrationHead(); // 返回FS:[0]的值
while ( -1 != pExcptRegHead )
{
EXCEPTION_RECORD excptRec2;
{
NtContinue( &context, 0 );
}
else
{
// 如果存在某个异常帧在堆栈上的位置比异常链表的头部还低
// 说明一定出现了错误
if ( pRegistrationFrame && (pRegistrationFrame <= pExcptRegHead) )
{
// 生成一个异常
excptRec2.ExceptionRecord = pExcptRec;
excptRec2.NumberParameters = 0;
excptRec2.ExceptionCode = STATUS_INVALID_UNWIND_TARGET;
excptRec2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
RtlRaiseException( &exceptRec2 );
}
}
if ( (stackUserBase <= pExcptRegHead )
&& (stackUserTop >= pStack )
&& (0 == (pExcptRegHead & 3)) )
{
DWORD pNewRegistHead;
DWORD retValue;
&pNewRegistHead, pExceptRegHead->handler );
{
if ( retValue != DISPOSITION_COLLIDED_UNWIND )
{
excptRec2.ExceptionRecord = pExcptRec;
excptRec2.NumberParameters = 0;
excptRec2.ExceptionCode = STATUS_INVALID_DISPOSITION;
excptRec2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
RtlRaiseException( &excptRec2 );
}
else
pExcptRegHead = pNewRegistHead;
}
pExcptRegHead = pExcptRegHead->prev;
RtlpUnlinkHandler( pCurrExcptReg );
else // 堆栈已经被破坏!生成一个异常
{
excptRec2.ExceptionRecord = pExcptRec;
excptRec2.NumberParameters = 0;
excptRec2.ExceptionCode = STATUS_BAD_STACK;
excptRec2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
RtlRaiseException( &excptRec2 );
}
// 结构链表的末尾,正常情况下不应该发生这种情况。
//(因为正常情况下异常应该被处理,这样就不会到链表末尾)
if ( -1 == pRegistrationFrame )
NtContinue( &context, 0 );
else
NtRaiseException( pExcptRec, &context, 0 );
{
return FS:[0];
}
{
FS:[0] = pRegistrationFrame->prev;
}
{
pContext->Eax = 0;
pContext->Ecx = 0;
pContext->Edx = 0;
pContext->Ebx = 0;
pContext->Esi = 0;
pContext->Edi = 0;
pContext->SegCs = CS;
pContext->SegDs = DS;
pContext->SegEs = ES;
pContext->SegFs = FS;
pContext->SegGs = GS;
pContext->SegSs = SS;
pContext->EFlags = flags; // 它对应的汇编代码为__asm{ PUSHFD / pop [xxxxxxxx] }
pContext->Eip = 此函数的调用者的调用者的返回地址 // 读者看一下这个函数的
pContext->Ebp = 此函数的调用者的调用者的EBP // 汇编代码就会清楚这一点
pContext->Esp = pContext->Ebp + 8;
}
后,这个函数调用RtlCaptureContext函数来创建一个空的CONTEXT结构,这个结构也变成了在展开阶段调用每个异常回调函数时传递给它 们的一个参数。
异常处理回调函数。每次回调之后,它调用RtlpUnlinkHandler移除相应的异常帧。
何问题,RtlUnwind就引发一个异常,指示出了什么问题,并且这个异常带有EXCEPTION_NONCONTINUABLE标志。当一个进程被设 置了这个标志时,它就不允许再运行,必须终止。
数的伪代码,但可以简要描述一下。如果是因为要对EXE或DLL的资源节(.rsrc)进行写操作而导致的异 常,_BasepCurrentTopLevelFilter就改变出错页面正常的只读属性,以便允许进行写操作。如果是这种特殊的情 况,UnhandledExceptionFilter返回EXCEPTION_CONTINUE_EXECUTION,使系统重新执行出错指令。
{
DWORD currentESP;
DWORD retValue;
DWORD DEBUGPORT;
DWORD dwTemp2;
DWORD dwUseJustInTimeDebugger;
CHAR szDbgCmdFmt[256]; // 从AeDebug这个注册表键值返回的字符串
CHAR szDbgCmdLine[256]; // 实际的调试器命令行参数(已填入进程ID和事件ID)
STARTUPINFO startupinfo;
PROCESS_INFORMATION pi;
HARDERR_STRUCT harderr; // ???
BOOL fAeDebugAuto;
TIB * pTib; // 线程信息块
&& (pExcptRec->ExceptionInformation[0]) )
{
retValue=BasepCheckForReadOnlyResource(pExcptRec->ExceptionInformation[1]);
return EXCEPTION_CONTINUE_EXECUTION;
retValue = NtQueryInformationProcess(GetCurrentProcess(), ProcessDebugPort,
&debugPort, sizeof(debugPort), 0 );
return EXCEPTION_CONTINUE_SEARCH;
// 如果调用了,那现在就调用他安装的异常处理程序
if ( _BasepCurrentTopLevelFilter )
{
retValue = _BasepCurrentTopLevelFilter( pExceptionPtrs );
if ( EXCEPTION_EXECUTE_HANDLER == retValue )
return EXCEPTION_EXECUTE_HANDLER;
if ( EXCEPTION_CONTINUE_EXECUTION == retValue )
return EXCEPTION_CONTINUE_EXECUTION;
}
{
harderr.elem0 = pExcptRec->ExceptionCode;
harderr.elem1 = pExcptRec->ExceptionAddress;
harderr.elem2 = pExcptRec->ExceptionInformation[2];
harderr.elem2 = pExcptRec->ExceptionInformation[0];
fAeDebugAuto = FALSE;
harderr.elem3 = pExcptRec->ExceptionInformation[1];
pTib = FS:[18h];
DWORD someVal = pTib->pProcess->0xC;
{
__try
{
char szDbgCmdFmt[256];
retValue = GetProfileStringA( "AeDebug", "Debugger", 0,
szDbgCmdFmt, sizeof(szDbgCmdFmt)-1 );
dwTemp2 = 2;
retValue = GetProfileStringA( "AeDebug", "Auto", "0",
szAuto, sizeof(szAuto)-1 );
if ( 0 == strcmp( szAuto, "1" ) )
if ( 2 == dwTemp2 )
fAeDebugAuto = TRUE;
__except( EXCEPTION_EXECUTE_HANDLER )
{
ESP = currentESP;
dwTemp2 = 1;
fAeDebugAuto = FALSE;
}
{
retValue=NtRaiseHardError(STATUS_UNHANDLED_EXCEPTION | 0x10000000,
4, 0, &harderr,_BasepAlreadyHadHardError ? 1 : dwTemp2,
&dwUseJustInTimeDebugger );
else
{
dwUseJustInTimeDebugger = 3;
retValue = 0;
}
&& (!_BasepAlreadyHadHardError)&&(!_BaseRunningInServerProcess))
_BasepAlreadyHadHardError = 1;
SECURITY_ATTRIBUTES secAttr = { sizeof(secAttr), 0, TRUE };
HANDLE hEvent = CreateEventA( &secAttr, TRUE, 0, 0 );
memset( &startupinfo, 0, sizeof(startupinfo) );
sprintf(szDbgCmdLine, szDbgCmdFmt, GetCurrentProcessId(), hEvent);
startupinfo.cb = sizeof(startupinfo);
startupinfo.lpDesktop = "Winsta0\Default"
szDbgCmdLine, // 命令行
0, 0, // 进程和线程安全属性
1, // bInheritHandles
0, 0, // 创建标志、环境
0, // 当前目录
&statupinfo, // STARTUPINFO
&pi); // PROCESS_INFORMATION
{
NtWaitForSingleObject( hEvent, 1, 0 );
return EXCEPTION_CONTINUE_SEARCH;
}
NtTerminateProcess(GetCurrentProcess(), pExcptRec->ExceptionCode);
SetUnhandledExceptionFilter(
LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter )
{
// _BasepCurrentTopLevelFilter是KERNEL32.DLL中的一个全局变量
LPTOP_LEVEL_EXCEPTION_FILTER previous= _BasepCurrentTopLevelFilter;
_BasepCurrentTopLevelFilter = lpTopLevelExceptionFilter;
它使用NtQueryInformationProcess函数来确定进程是否正在被调试,我在本月的Under the Hood专栏中讲解了这个函数。如果正在被调试,UnhandledExceptionFilter就返回 EXCEPTION_CONTINUE_SEARCH,这告诉系统去唤醒调试器并告诉它在被调试程序(debuggee)中产生了一个异常。
SetUnhandledExceptionFilter这个API来安装。上面我也提供了这个API的伪代码。这个函数只是简单地用用户安装的回调函数 的地址来替换一个全局变量,并返回替换前的值。
法是将AeDebug子键下的Auto的值设为1。此时UnhandledExceptionFilter跳过应用程序错误对话框直接启动AeDebug 子键下的Debugger的值所指定的调试器。如果你熟悉“即时调试(Just In Time Debugging,JIT)”的话,这就是操作系统支持它的地方。接下来我会详细讲。
的进程通常通过终止自身来作为响应(正像你在BaseProcessStart的伪代码中看到的那样)。这就产生了一个有趣的问题——大多数人都认为是系 统终止了产生未处理异常的进程,而实际上更准确的说法应该是,系统进行了一些设置使得产生未处理异常的进程将自身终止掉了。
CreateEvent来创建一个事件内核对象,调试器成功附加到出错进程之后会将此事件对象变成有信号状态。这个事件句柄以及出错进程的ID都被传到 sprintf函数,由它将其格式化成一个命令行,用来启动调试器。一切就绪之后,UnhandledExceptionFilter就调用 CreateProcess来启动调试器。如果CreateProcess成功,它就调用NtWaitForSingleObject来等待前面创建的那 个事件对象。此时这个调用被阻塞,直到调试器进程将此事件变成有信号状态,以表明它已经成功附加到出错进程上。
UnhandledExceptionFilter函数中还有一些其它的代码,我在这里只讲重要的。
常回调过程最初是从哪里开始的这个问题了。好吧,让我们深入系统内部来看一下结构化异常处理的开始阶段吧。
0(0特权级,即内核模式)的一个处理程序上。这个处理程序由中断描述符表(Interrupt Descriptor Table,IDT)中的一个元素定义,它是专门用来处理相应异常的。我跳过所有的内核模式代码,假设当异常发生时CPU直接将控制权转到了 KiUserExceptionDispatcher函数。
{
DWORD retValue;
if ( RtlDispatchException( pExceptRec, pContext ) )
retValue = NtContinue( pContext, 0 );
else
retValue = NtRaiseException( pExceptRec, pContext, 0 );
excptRec2.ExceptionCode = retValue;
excptRec2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
excptRec2.ExceptionRecord = pExcptRec;
excptRec2.NumberParameters = 0;
}
{
DWORD stackUserBase;
DWORD stackUserTop;
PEXCEPTION_REGISTRATION pRegistrationFrame;
DWORD hLog;
RtlpGetStackLimits( &stackUserBase, &stackUserTop );
while ( -1 != pRegistrationFrame )
{
PVOID justPastRegistrationFrame = &pRegistrationFrame + 8;
if ( stackUserBase > justPastRegistrationFrame )
{
pExcptRec->ExceptionFlags |= EH_STACK_INVALID;
return DISPOSITION_DISMISS; // 0
}
{
pExcptRec->ExceptionFlags |= EH_STACK_INVALID;
return DISPOSITION_DISMISS; // 0
}
{
pExcptRec->ExceptionFlags |= EH_STACK_INVALID;
return DISPOSITION_DISMISS; // 0
}
{
hLog = RtlpLogExceptionHandler( pExcptRec, pContext, 0,
pRegistrationFrame, 0x10 );
}
pContext, &dispatcherContext,
pRegistrationFrame->handler );
RtlpLogLastExceptionDisposition( hLog, retValue );
{
pExcptRec->ExceptionFlags &= ~EH_NESTED_CALL; // 关闭标志
}
DWORD yetAnotherValue = 0;
{
if ( pExcptRec->ExceptionFlags & EH_NONCONTINUABLE )
{
excptRec2.ExceptionRecord = pExcptRec;
excptRec2.ExceptionNumber = STATUS_NONCONTINUABLE_EXCEPTION;
excptRec2.ExceptionFlags = EH_NONCONTINUABLE;
excptRec2.NumberParameters = 0;
RtlRaiseException( &excptRec2 );
}
else
return DISPOSITION_CONTINUE_SEARCH;
}
else if ( DISPOSITION_CONTINUE_SEARCH == retValue )
{}
else if ( DISPOSITION_NESTED_EXCEPTION == retValue )
{
pExcptRec->ExceptionFlags |= EH_EXIT_UNWIND;
if ( dispatcherContext > yetAnotherValue )
yetAnotherValue = dispatcherContext;
}
else // DISPOSITION_COLLIDED_UNWIND
{
excptRec2.ExceptionRecord = pExcptRec;
excptRec2.ExceptionNumber = STATUS_INVALID_DISPOSITION;
excptRec2.ExceptionFlags = EH_NONCONTINUABLE;
excptRec2.NumberParameters = 0;
RtlRaiseException( &excptRec2 );
}
}
}
JMP ExecuteHandler
PEXCEPTION_REGISTRATION pExcptReg,
CONTEXT * pContext,
PVOID pDispatcherContext,
FARPROC handler ) // 实际上是指向_except_handler()的指针
{
// 安装一个EXCEPTION_REGISTRATION帧,EDX指向相应的handler代码
PUSH EDX
PUSH FS:[0]
MOV FS:[0],ESP
EAX = handler( pExcptRec, pExcptReg, pContext, pDispatcherContext );
MOV ESP,DWORD PTR FS:[00000000]
POP DWORD PTR FS:[00000000]
}
{
// 如果设置了展开标志,返回DISPOSITION_CONTINUE_SEARCH
// 否则,给pDispatcherContext赋值并返回DISPOSITION_NESTED_EXCEPTION
DISPOSITION_CONTINUE_SEARC : ( *pDispatcherContext =
pRegistrationFrame->scopetable,
DISPOSITION_NESTED_EXCEPTION );
}
{
// 如果设置了展开标志,返回DISPOSITION_CONTINUE_SEARCH
// 否则,给pDispatcherContext赋值并返回DISPOSITION_COLLIDED_UNWIND
DISPOSITION_CONTINUE_SEARCH : ( *pDispatcherContext =
pRegistrationFrame->scopetable,
DISPOSITION_COLLIDED_UNWIND );
}
RtlDispatchException的调用就不会返回。如果它返回了,只有两种可能:或者调用了NtContinue以便让进程继续执行,或者产生 了新的异常。如果是这样,那异常就不能再继续处理了,必须终止进程。
数的代码,这就是我通篇提到的遍历异常帧的代码。这个函数获取一个指向EXCEPTION_REGISTRATION结构链表的指针,然后遍历此链表以寻 找一个异常处理程序。由于堆栈可能已经被破坏了,所以这个例程非常谨慎。在调用每个EXCEPTION_REGISTRATION结构中指定的异常处理程 序之前,它确保这个结构是按DWORD对齐的,并且是在线程的堆栈之中,同时在堆栈中比前一个EXCEPTION_REGISTRATION结构高。
RtlpExecuteHandlerForException来完成这个工作。根据RtlpExecuteHandlerForException的执 行情况,RtlDispatchException或者继续遍历异常帧,或者引发另一个异常。这第二次的异常表明异常处理程序内部出现了错误,这样就不能 继续执行下去了。
地给EDX寄存器加载一个不同的值然后就调用ExecuteHandler函数。也就是 说,RtlpExecuteHandlerForException和RtlpExecuteHandlerForUnwind都是 ExecuteHanlder这个公共函数的前端。
理程序封装着。在SEH自身中使用SEH看起来有点奇怪,但你思索一会儿就会理解其中的含义。如果在异常回调过程中引发了另外一个异常,操作系统需要知道 这个情况。根据异常发生在最初的回调阶段还是展开回调阶段,ExecuteHandler或者返回DISPOSITION_NESTED_EXCEPTION,或者返回DISPOSITION_COLLIDED_UNWIND。这两者都是“红色警报!现在把一切都关掉!”类型的代码。
EXCEPTION_REGISTRATION结构的handler域。这基本上与我在MYSEH和MYSEH2中对原始的结构化异常处理的使用情况一 样。
图15 在SEH中是谁调用了谁
MOV EBP, ESP
SUB ESP, XXX
了被调函数的prolog代码。此时[ESP]中是返回地址,[ESP+4]中是函数的第一个参数。本来可以就这样使用ESP寄存器来访问参数,但由于 PUSH和POP指令会隐含修改ESP寄存器的值,这样同一个参数在不同时刻可能需要通过不同的指令形式来访问(例如,如果现在向堆栈中压入一个值的话, 那访问第一个参数就需要使用[ESP+8]了)。为了解决这个问题,所以使用EBP寄存器。EBP寄存器被称为栈帧(frame)指针,它正是用于此目 的。当上述prolog指令中的前两条指令执行后,就可以使用EBP来访问参数了,并且在整个函数中都不会改变此寄存器的值。在前面的例子中,
[EBP+8]处就是第一个参数的值,[EBP+0Ch]处是第二个参数的值,依次类推。
个选项(在Microsoft C/C++编译器中为/Oy),它导致函数使用ESP来访问参数,从而可以空闲出一个寄存器(EBP)用于其它目的,并且由于不需要设置堆栈帧,从而会稍 微提高运行速度。但是在某些情况下必须使用堆栈帧。作者在前面也提到过,Microsoft已经在其MSDN文档中指明:结构化异常处理是基于帧的异常处理。也就是说,它必须使用堆栈帧。当你查看编译器为使用SEH的函数生成的汇编代码时就会清楚这一点。无论你是否使用/Oy选项,它都设置堆栈帧。
C/C++编译器被称为优化编译器,它怎么可能生成这么一条除了占用空间之外别无它用的指令呢?实际上,如果你比较细心的话,会发现以这条指令开头的函数 的前面有5条NOP指令(它们一共占5个字节),如下图所示。
EDI, EDI”处用一个近跳转指令覆盖,它跳转到5个NOP指令所在的位置。使用“MOV EDI, EDI”而不是直接使用两个NOP指令是出于性能考虑。
POP EBP
RET XXX
参数个数(每个参数均为4字节)。
是:prolog代码就是前面所说的那样,但epilog代码使用了LEAVE指令。
【VS开发】关于SEH的简单总结的更多相关文章
- Bootstrap 简洁、直观、强悍的前端开发框架,让web开发更迅速、简单。
Bootstrap 简洁.直观.强悍的前端开发框架,让web开发更迅速.简单.
- iOS开发多线程篇—多线程简单介绍
iOS开发多线程篇—多线程简单介绍 一.进程和线程 1.什么是进程 进程是指在系统中正在运行的一个应用程序 每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内 比如同时打开QQ.Xcod ...
- iOS开发UI篇—UITabBarController简单介绍
iOS开发UI篇—UITabBarController简单介绍 一.简单介绍 UITabBarController和UINavigationController类似,UITabBarControlle ...
- Asp.net Mvc模块化开发之“开启模块开发、调试的简单愉快之旅”
整个世界林林种种,把所有的事情都划分为对立的两个面. 每个人都渴望的财富划分为富有和贫穷,身高被划分为高和矮,身材被划分为胖和瘦,等等. 我们总是感叹,有钱人的生活我不懂;有钱人又何尝能懂我们每天起早 ...
- iOS开发UI篇—Modal简单介绍
iOS开发UI篇—Modal简单介绍 一.简单介绍 除了push之外,还有另外一种控制器的切换方式,那就是Modal 任何控制器都能通过Modal的形式展⽰出来 Modal的默认效果:新控制器从屏幕的 ...
- iOS开发数据库篇—SQLite简单介绍
iOS开发数据库篇—SQLite简单介绍 一.离线缓存 在项目开发中,通常都需要对数据进行离线缓存的处理,如新闻数据的离线缓存等. 说明:离线缓存一般都是把数据保存到项目的沙盒中.有以下几种方式 (1 ...
- iOS开发UI篇—Kvc简单介绍
ios开发UI篇—Kvc简单介绍 一.KVC简单介绍 KVC key valued coding 键值编码 KVC通过键值间接编码 补充: 与KVC相对的时KVO,即key valued observ ...
- iOS开发UI篇—iOS开发中三种简单的动画设置
iOS开发UI篇—iOS开发中三种简单的动画设置 [在ios开发中,动画是廉价的] 一.首尾式动画 代码示例: // beginAnimations表示此后的代码要“参与到”动画中 [UIView b ...
- iOS开发UI篇—UIWindow简单介绍
iOS开发UI篇—UIWindow简单介绍 一.简单介绍 UIWindow是一种特殊的UIView,通常在一个app中只会有一个UIWindow iOS程序启动完毕后,创建的第一个视图控件就是UIWi ...
随机推荐
- django 模型层(orm)05
目录 配置测试脚本 django ORM基本操作 增删改查 Django 终端打印SQL语句 13条基本查询操作 双下滑线查询 表查询 建表 一对多字段数据的增删改查 多对多字段数据的增删改查 基于对 ...
- Servlet中关于路径的小结
URL(统一 资源定位符).URI(统一资源标识符)都是由资源路径和资源名称组成. 访问路径按照路径是否可以独立完成资源准确定位的判别标准,可以将访问路径分为绝对路径与相对路径. 关系:绝对路径 ...
- java常见问题 ——编辑报错1
报错1 The method add(CatNode) in the type List<CatNode> is not applicable for the arguments (Str ...
- H5 设计尺寸
750*1218 微信下 兼容 7plus 内容高度 居中 1000px 内 750*1448 微信下 兼容 iphoneX 微信导航栏高度 64px 64px = 导航栏44+状态栏20 但是现在 ...
- sh_17_字符串的查找和替换
sh_17_字符串的查找和替换 hello_str = "hello world" # 1. 判断是否以指定字符串开始 print(hello_str.startswith(&qu ...
- 推荐系统系列(四):PNN理论与实践
背景 上一篇文章介绍了FNN [2],在FM的基础上引入了DNN对特征进行高阶组合提高模型表现.但FNN并不是完美的,针对FNN的缺点上交与UCL于2016年联合提出一种新的改进模型PNN(Produ ...
- Jmeter(十二)常用插件
一.下载及安装 下载地址:https://jmeter-plugins.org/install/Install/ 下载后文件为一个jar包,将其放入jmeter安装目录下的lib/ext目录,然后重启 ...
- 网络yum源
1,进入yum源配置目录cd /etc/yum.repos.d 2,备份系统自带的yum源mv CentOS-Base.repo CentOS-Base.repo.bk下载163网易的yum源:wge ...
- MS14-068提权
• Ms14- • 库 • https://github.com/bidord/pykek • ms14-.py -u user@lab.com -s userSID -d dc.lab.com • ...
- 标准库函数gets()和puts()
问题: 用标准库函数gets()和puts()实现字符串的输入输出.函数gets()用于从键盘读入一个字符串(包括空格符).它仅以回车符作为分隔符.函数gets()中的参数应是一个已存储字符串的字符数 ...