标 题 : SEH源码赏析之C篇
作 者 : xinlin
时 间 : 2007 - 10 - 24 , 12 : 12
链 接 : http : //bbs.pediy.com/showthread.php?t=53778
SEH结构化异常处理源码赏析 ( C篇 )
关键字 : C SEH 结构化异常处理 _try _finally _except _except_handler3 VC
工程使用工具 : VC7 .1.3088 IDA pro 4.9.0.863 cdb Windbg Editplus
1. 起因
C ++ 程序员对 try , catch , throw 都应该很熟悉 , 能知道VC怎么实现它的人就不多了 , 不过网络世界使很多人知道了它与SEH ( structured exception handling)有密切关系 , 我也不例外 , 也是在若干年前从网络知道了SEH , 并且大致也知道SEH的流程 . 但是和多数人一样在我的实践也很少直接使用 SEH , 对SEH也就仅限于网络上一些文章的介绍 . 曾经在用Windbg对某些软件作分析 , 我遇到了断点失效的情况 , 查找资料介绍是SEH中的 Handler清除了调试寄存器 , 在分析SEH代码中由于VC没有SEH的源码 , 于是我产生了一种想法 , 详细完整地翻译VC的SEH的代码 , 一劳永逸的解决问题 . C和C ++ 的SEH有所不同 , C ++ 的要复杂些 , 我在此介绍的仅为C的SEH代码 , 也就是__try , __finally , __except , __leave所产生的SEH代码 , C ++ 篇有时间的话我再作 . 我以前看过的资料大都以比较专业的语言介绍 , 在此我仅以我自己感觉比较通俗的语言介绍 , 希望能有更多的人能认识 , 认清SEH .
2.SEH 术语 : SEH中术语虽然不多 , 但由于没有SDK的明确定义有时很难区别 , 各家说的表述也不太统一 , 因此为此文特定义如下术语 , 这些术语可能与其它文献有点冲突或细微差别 , 有的也是我自己的定义 :
A . SEH ( structured exception handling ): 在C语言中关键字是__try ( _try ), __finally ( _finally ), _except ( __except ), 而C ++ 使用的关键字是 try , catch . 在以下的表述中所有的 try 均不再特别声明为C关键字 , 一律默认为C关键字 .
B . EH3_List : _EH3_EXCEPTION_REGISTRATION链表 , 表头位于FS :[ 0 ], 0xFFFFFFFF 为链表结束标志 . 编译器在编译一个函数时只要检测到含有_try或__try则为此函数生成一个_EH3_EXCEPTION_REGISTRATION节点 , 并插入到表头 . 因为每个函数编译只生成一个节点 , 因此在一个函数中C和C ++ 的SEH不能同时存在 , 如果代码中同时有 catch 和except则不能通过编译就是此原因 .
C . EH3_ScopeTable : 是一个由编译器在data section生成的一张表 ( 数组 ), 实质可看作是二叉树结构 ( 可能有多个二叉树顺序存放 ), 节点为_SCOPETABLE_ENTRY类型 , 其中 _SCOPETABLE_ENTRY . ParentLevel是父节点在数组中的位置 , EH3_ScopeTable [ 0 ] 是根节点 , _SCOPETABLE_ENTRY . ParentLevel = 0xFFFFFFFF. 由此可见ParentLevel很重要 , 是SEH判断 try 嵌套层次的唯一依据 . 编译器从函数入口点开始遍历 try , 每遇到一个 try 生成一个节点_SCOPETABLE_ENTRY , 并放在表最后 , 注意节点的先后与 try 的嵌套层次无关 .
D . filter handler : 是异常发生后让用户决定是否认识此异常 , 通过修改异常语句的上下文环境 ( CONTEXT ) 可使应用程序能继续正常运行 . 其返回值有三 .
EXCEPTION_EXECUTE_HANDLER ( 1 ): 去执行exception handler , 然后进程终止 , 且不显示出错提示框 .
EXCEPTION_CONTINUE_SEARCH ( 0 ): 不执行exception handler , 显示出错提示框 , 进程终止或者进入调试器进行调试 .
EXCEPTION_CONTINUE_EXECUTION (- 1 ): 不执行exception handler , 系统用CONTEXT重新设置CPU环境 , 进程继续执行 , 如果修改了EIP则从新的EIP开始执行 , 否则从原异常点开始执行 .
E . exception handler : 是异常发生后检测到该异常无法被处理 , 而进程终止前提醒应用程序执行的收尾工作
F . termination handler : 如果 try 语句被过早终止 , 不管是正常离开或者是非正常离开 , 包括 goto , leave及异常时均执行此handler , 具体执行过程可查MSDN .
G . 展开 ( unwind ): 这个名词很让人费解 , 翻译也确实不好命名 , 我也沿用此名 . 展开的目的是执行finally对应的termination handler , 对照在下面的源代码中我们就能很容易理解MSDN关于finally的解释了 . 展开分为本地局部展开 ( _local_unwind ) 和全局展开 ( _global_unwind ); 本地展开 : 展开此 try 所在函数 try 嵌套关系并分别执行其finally对应的termination handler ; 全局展开 : 当exception handler不在本 try 函数时 , 执行exception handler前需要先执行这之前的termination handler , 全局展开就是查找这些termination handler并执行它 . 需要说明的是全局展开不含本地展开 .
3.SEH 数据结构
typedef struct // (sizeof=0xC)
{
DWORD ParentLevel ; // 当前Handler父层TRY在EH3_ScopeTable中的位置,根没有上一层,故值=-1
// 形成TRY层次的二叉树结构,与_EH3_EXCEPTION_REGISTRATION.TryLevel物理意义一样
DWORD FilterFunc ; // 非NULL则HandlerFunc是exception handler,否则termination handler
DWORD HandlerFunc ; // exception handler or termination handler
} _SCOPETABLE_ENTRY ;
typedef struct // (sizeof=0x10)
{
_EH3_EXCEPTION_REGISTRATION * pPrev ; // 栈上一级EH3_List节点,=0xFFFFFFFF则为最后一个节点
EXCE_HANDLER ExceptionHandler ; // VC7.1中统一指向_except_handler3
_SCOPETABLE_ENTRY * pScopeTable ; // 指向一个_SCOPETABLE_ENTRY数组,函数有n个TRY,则数组有n个元素
// p[0]->Try0为根,p[1]->Try1,p[2]->Try2...
DWORD TryLevel ; // 指示当前指令在Try的层次级别,-1未进入TRY,进第一个TRY为0,第二个为1,...
// 但它与嵌套层次无关,在编译时确定,从函数代码开始处开始计数
} _EH3_EXCEPTION_REGISTRATION ;
typedef struct // (sizeof=0x10)还有待进一步分析其用处
{
DWORD unKnown ; // 未知:被编译器赋值
DWORD HandlerFunc ; // _SCOPETABLE_ENTRY.HandlerFunc
DWORD firstPara ; // Try所在函数第一个参数:crtMain!EBP+8
DWORD TryEBP ; // Try所在函数EBP
} _NLGDestination ;
// 以下在MSDN中均有定义,不再作解释
typedef struct _EXCEPTION_RECORD
{
DWORD ExceptionCode ;
DWORD ExceptionFlags ;
struct _EXCEPTION_RECORD * ExceptionRecord ;
PVOID ExceptionAddress ;
DWORD NumberParameters ;
ULONG_PTR ExceptionInformation [ EXCEPTION_MAXIMUM_PARAMETERS ];
} EXCEPTION_RECORD ,* PEXCEPTION_RECORD ;
typedef struct _CONTEXT
{
... // 依据CPU类型有不同定义,具体可见winnt.h
} CONTEXT ,* PCONTEXT ;
typedef struct _EXCEPTION_POINTERS
{
PEXCEPTION_RECORD ExceptionRecord ;
PCONTEXT ContextRecord ;
} EXCEPTION_POINTERS ,* PEXCEPTION_POINTERS ;
4.SEH 汇编要点
A . 函数中 try 的数据模型 : VC将会为有 try 函数在栈上首先建立三个变量 :
EBP - 18h : SaveESP // TRY前保存当时ESP,因此有:SaveESP<&Eh3Exception
EBP - 14h : pExceInfo // GetExceptionPointers(),在handler中调用filter前赋值
EBP - 10h : Eh3Exception // 一个_EH3_EXCEPTION_REGISTRATION,其大小刚好为10h哦
EBP + 00h : ebp // 上一frame的EBP
EBP + 04h : EIP // CALL返回地址
在这里我们应该注意到这个公式是成立的 :
EBP =& Eh3Exception + 10h
而在_except_handler3中参数 : pEh3Exce刚好为 & Eh3Exception , 所以每当在_except_handler3中要回调filter , exception , termination handler时 , 在这之前汇编中均有一句 :
lea ebp , [ ebx + 10h ] // ebx=_except_handler3!arg_pEh3Exception
明白了这点就不难明白如何在_except_handler3中访问pExceInfo及SaveESP了 !
B . try 块的终止 : 异常发生 , goto , return , leave , 正常终止 . goto 和 return 可能跨过其它 try , 所以必须要展开到目的地所在的 TryLevel , 但是leave关键字不会跨过其它 try , 只是跳出自己这层 try , 如果编译器检测到这层 try 有termination handler , 则用CALL xxx直接调用 . 当然也有例外 , 这也是VC聪明的地方 , 如果函数只有一个 try , 则根本不用展开而直接使用CALL了 , 这种情况有时可见 . 这样就不难理解如下代码 :
goto 004013fb 终止代码 :
// ---------------goto跳出try----------------------------------------
goto try0 ;
push 0 或者 ( 仅一个 try 时 ) CALL 00401070 // 直接调termination handler
lea eax , [ ebp + var_Eh3Exce ] jmp loc_4013FB
push eax
call __local_unwind2 // 展开到0(即第一个try内,goto目的地肯定在第一个try内)
add esp , 8
jmp loc_4013FB
return 终止代码 :( 无返回值 )
// ---------------return跳出try--------------------------------------
push 0FFFFFFFFh
lea eax ,[ ebp - 10h ]
push eax
call __local_unwind2 ( 401786h ) // 展开到-1(即所有try外)
add esp , 8
return ;
jmp $L19800 ( 401139h )
return var_i 终止代码 :( 有返回值 )
// ---------------return(带返回值)跳出try----------------------------
mov eax , dword ptr [ i ]
mov dword ptr [ ebp - 100h ], eax // 返回值先被保存
push 0FFFFFFFFh
lea ecx ,[ ebp - 10h ]
push ecx
call __local_unwind2 ( 4017D6h ) // 再展开,即使finally中改变了i,也不会改变返回值!!!
add esp , 8
return i ;
mov eax , dword ptr [ ebp - 100h ] // 取保存的返回值来保存
jmp $L19800 ( 40117Bh ) // 跳到复原先前SEH链处
leave和正常退出的代码 :
// ---------------leave跳出try---------------------------------------
if ( x > 18 )
004010FD cmp dword ptr [ x ], 12h
00401101 jle FunC + 55h ( 401105h )
__leave ;
00401103 jmp FunC + 66h ( 401116h ) // 直接调用termination handler
printf ( "%s Try!\n" , fun );
00401105 mov eax , dword ptr [ fun ]
00401108 push eax
00401109 push offset string "%s Try!\n" ( 410130h )
0040110E call printf ( 401650h )
00401113 add esp , 8
00401116 mov dword ptr [ ebp - 4 ], 0FFFFFFFFh // 退出try
// ---------------正常退出try----------------------------------------
0040111D call $L19798 ( 401124h ) // 直接调用termination handler
00401122 jmp $L19801 ( 40113Fh )
C . _except_handler3执行两次原因 , 实际上理解了展开就能理解它 , 举例解释如下 :
EH3_List如 : FS [ 0 ]-> E1 -> E2 -> E3 -> E4 , 在E4中发生异常 , 依次搜索E1 , E2 , E3 , 最后在E4中找到能处理此异常的filter handler , 这样E4 , E3 , E2 , E1的_except_handler3均执行了一次 , 这是第一次 .
第二次 : 在执行E4的exception handler前要先调用全局展开 ( _global_unwind2 ) 以运行E1 , E2 , E3的termination handler在全局展开 ( _global_unwind2 ) 中将为EXCEPTION_RECORD . ExceptionFlags增加标志 _EH_UNWINDING ( 2 ), 再依次调用它们的_except_handler3 , 这就是E1 , E2 , E3的_except_handler3的第二次调用 , 但E4只有一次 .
5. VC7 .1 下的SEH的C代码 :
// 源码基本以汇编为蓝本,没有进行优化,主要是为了方便大家与汇编对照阅读
#define MAX_PAGES 0x10
int ValidateEH3RN ( _EH3_EXCEPTION_REGISTRATION * pEh3Exce )
{
// 1.验证_EH3_EXCEPTION_REGISTRATION.pScopeTable的合法性:4字节对齐,且不在栈空间内,因为它是编译器生成的全局变量
_SCOPETABLE_ENTRY * pScopeTable = pEh3Exce -> pScopeTable ;
if ((( DWORD ) pScopeTable & 0x3 ) == 0 )
return 0 ;
NT_TIB * pTeb = ( NT_TIB *) FS [ 0x18 ];
DWORD nStackLimit = pTeb -> StackLimit ;
if ( pScopeTable >= pTeb -> StackLimit && pScopeTable < pTeb -> StackBase )
return 0 ; // pScopeTable在栈内肯定不合法
// 2.判断二叉树pScopeTable是否合法,并统计exception handler数量
if ( pEh3Exce -> TryLevel == - 1 )
return 1 ; // 表示语句不在try中
DWORD nFilters , count ;
nFilters = count = 0 ;
for (; nFilters <= pEh3Exce -> TryLevel ; nFilters ++)
{ // LOOP1:验证已进入的TRY的SCOPETABLE_ENTRY是否都合法
if ( pScopeTable [ nFilters ]. ParentLevel !=- 1 && pScopeTable [ nFilters ]. ParentLevel >= nFilters )
return 0 ; // EnclosingLevel在二叉树中不合法:不是根结点且ParentLevel>=nFilters
// ParentLevel>=nFilters不合法原因:二叉树存贮不可能儿子先存
if ( pScopeTable [ nFilters ]. FilterFunc == NULL )
continue ; // termination handler不统计
count ++;
}
if ( count != 0 )
{ // 有异常处理,验证saveESP
PVOID saveESP = 0 ; // 运行时真实值 = crtMain!saveESP=crtMain!(ebp-18h);
if ( saveESP < nStackLimit || saveESP >= pEh3Exce )
return 0 ; // 不在栈内,或不在TRY函数内,saveESP为TRY函数的栈顶
}
// 3.找g_rgValidPages中是否已经有pScopeTable的页基址
static int g_nValidPages = 0 ; // g_rgValidPages数组有效元素个数
static int g_rgValidPages [ MAX_PAGES ]={ 0 }; // MaxPages=0x10
DWORD nPageBase = ( DWORD ) pScopeTable & 0xfffff000 ; // 取pScopeTable所在页基址
int nFind = 0 ;
if ( g_nValidPages > 0 )
{
do
{ // LOOP2
if ( nPageBase == g_rgValidPages [ nFind ])
goto Finded ; // 找到
nFind ++;
}
while ( nFind < g_nValidPages );
}
// 4.没有过在g_rgValidPages找到nPageBase则判断pScopeTable是否在合法的EXE映像内
MEMORY_BASIC_INFORMATION memInfo ;
if ( VirtualQuery ( pScopeTable ,& memInfo , sizeof ( memInfo )) == 0 )
return - 1 ;
if ( memInfo . Type != 0x01000000 ) // MEM_IMAGE
return - 1 ;
DWORD protect = 0xCC ; // PAGE_READWRITE|PAGE_WRITECOPY|PAGE_EXECUTE_READWRITE|PAGE_EXECUTE_WRITECOPY;
if ( memInfo . Protect & protect )
{ // 有指定属性
IMAGE_DOS_HEADER * pDosHeader = ( IMAGE_DOS_HEADER *) memInfo . AllocationBase ;
if ( pDosHeader -> e_magic != 'ZM' )
return - 1 ; // 非法DOS头,pScopeTable不为编译器分配的空间
IMAGE_NT_HEADERS * pPeHeader = ( IMAGE_NT_HEADERS *)(( char *) pDosHeader + pDosHeader -> e_lfanew );
if ( pPeHeader -> Signature != 'ZM' )
return - 1 ; // 非法PE头,pScopeTable不为编译器分配的空间
IMAGE_OPTIONAL_HEADER32 * pOptHeader = & pPeHeader -> OptionalHeader ;
if ( pOptHeader -> Magic != 0x10b )
return - 1 ; // 非WIN32 EXE
DWORD rvaScope = ( DWORD ) pScopeTable -( DWORD ) pDosHeader ; // 计算pScopeTable的RAV
if ( pPeHeader -> FileHeader . NumberOfSections <= 0 )
return - 1 ;
IMAGE_SECTION_HEADER * pSection = ( IMAGE_SECTION_HEADER *)( pPeHeader + 1 );
if ( rvaScope >= pSection -> VirtualAddress && rvaScope < pSection -> VirtualAddress + pSection -> Misc . VirtualSize )
{ // rvaScope在代码节内
if ( pSection -> Characteristics & 0x80000000 ) // 0x80000000=IMG_SCN_MEM_WRITE
return 0 ;
}
}
// 5.对新验证的nPageBse插入到数组中
static int g_nLock = 0 ; // 1:上锁,0:解锁
if ( InterlockedExchange (& g_nLock , 1 )!= 0 ) // 写线程锁
return 1 ; // 其它线程已经进入,则不能再进入
int k = g_nValidPages ;
if ( k > 0 )
{
do
{ // LOOP4:判断其它线程是否写入这个页基址
if ( nPageBase == g_rgValidPages [ k - 1 ])
break ; // 找到,k>0
k --;
}
while ( k > 0 );
}
if ( k == 0 )
{ // 没有找到,nPageBase插入到g_rgValidPages头
int pages = 0x0F ;
if ( g_nValidPages <= pages )
pages = g_nValidPages ;
k = 0 ;
if ( pages >= 0 )
{
do
{ // LOOP5
int temp = g_rgValidPages [ k ];
g_rgValidPages [ k ]= nPageBase ;
nPageBase = temp ;
k ++;
}
while ( k <= pages );
}
if ( g_nValidPages < 0x10h )
g_nValidPages ++;
}
InterlockedExchange (& g_nLock , 0 ); // 解锁
return 1 ;
// 6.找到的nPageBase移到头
// 但前面找到的结果也可能被其它线程改变位置甚至推出数组,但无论如何这个nPageBase合法可不再验证
Finded :
if ( nFind <= 0 ) // 相当于if(nFind == 0)
return 1 ; // nPageBase已经在头,可直接返回
if ( InterlockedExchange (& g_nLock , 1 )!= 0 ) // 写线程锁
return 1 ; // 其它线程已经进入,则不能再进入
if ( g_rgValidPages [ nFind ] != nPageBase )
{ // 再次对找到的pos进行比较,因为其它线程可能又修改了这个元素的值
nFind = g_nValidPages - 1 ;
if ( nFind >= 0 )
{
while ( nFind >= 0 )
{ // LOOP3
if ( g_rgValidPages [ nFind ]== nPageBase )
break ;
nFind --;
}
if ( nFind >= 0 )
goto End1 ;
}
// 没找到,新增加
if ( g_nValidPages < 0x10 )
g_nValidPages ++;
nFind = g_nValidPages - 1 ;
goto end2 ;
}
else
goto end2 ;
end1 :
if ( nFind != 0 )
{
end2 :
if ( nFind >= 0 )
{
for ( int j = 0 ; j <= nFind ; j ++)
{ // LOOP6:g_rgValidPages中找到的nPageBase移到头或新nPageBase插入头,其余每个元素向后推
int temp = g_rgValidPages [ j ];
g_rgValidPages [ j ]= nPageBase ;
nPageBase = temp ;
}
}
}
InterlockedExchange (& g_nLock , 0 ); // 解线程锁
return 1 ;
}
void _global_unwind2 ( _EH3_EXCEPTION_REGISTRATION * pEh3Exce )
{ // 对调用RtlUnwind的封装
RtlUnwind ( pEh3Exce , offset exit , 0 , 0 );
exit :
return ;
}
int _UnwindHandler ( EXCEPTION_RECORD * pExceRec , _EH3_EXCEPTION_REGISTRATION * pEh3Exce ,
void *, _EH3_EXCEPTION_REGISTRATION ** ppEh3Exce )
{
if ( pExceRec -> ExceptionFlags && 6 == 0 )
return 1 ; // ExceptionContinueSearch
* ppEh3Exce = pEh3Exce ;
return 3 ; // ExceptionCollidedUnwind
}
// NLG == "non-local-goto"
_NLGDestination g_NLGDestination ; // 此全局变量我至始至终未见到任何其它EXE处或者DLL使用,通知是何意义暂无知!!!!!!!
void _NLG_Notify1 ( int x ) // --------------------------CallSettingFrame调用
{ // g_NLGDestination全局分配变量:0x19930520
g_NLGDestination . dwInCode = EBP + 8 ; // Try函数中第一个参数???
g_NLGDestination . HandlerFunc = EAX ; // 调用前通过EAX传入
g_NLGDestination . TryEBP = EBP ; // Try中的EBP???
}
void _NLG_Notify ( int x ) // --------------------------_except_handler3,_local_unwind2调用
{ // 传入参数未用
// g_NLGDestination.unKnown全局分配变量:0x19930520
g_NLGDestination . dwInCode = EBP + 8 ; // Try函数中第一个参数,如:FunB(i,j)!i,crtMain!ebp+8
g_NLGDestination . HandlerFunc = EAX ; // 调用前通过EAX传入:pScopeTable.HandlerFunc
g_NLGDestination . TryEBP = EBP ; // Try中的EBP
}
// nToLevel:展开到此为止(不含nToLevel),举例:在goto中nToLevel由目标所在try决定,因为系统规定跳入TRY是不合法的,因此
// goto不能跳入其它TRY(嵌套的上层TRY是合法的,很明显平级层是不合法的)层,这时编译是通不过的.
void _local_unwind2 ( _EH3_EXCEPTION_REGISTRATION * pEh3Exce , int nToLevel )
{ // 用ESP,未用EBP,之前的EBP传入内调的_NLG_Notify,__try中的goto将用此函数展开得到其handler
_EH3_EXCEPTION_REGISTRATION eh3Unwind ;
eh3Unwind . TryLevel = ( int ) pEh3Exce ;
eh3Unwind . pScopeTable = ( _SCOPETABLE_ENTRY *)- 2 ;
eh3Unwind . ExceptionHandler = ( EXCE_HANDLER ) _UnwindHandler ;
eh3Unwind . pPrev = ( _EH3_EXCEPTION_REGISTRATION *) FS [ 0 ];
FS [ 0 ]= ESP ;
int nLevel ; //
while ( 1 )
{
_SCOPETABLE_ENTRY * pScope = pEh3Exce -> pScopeTable ;
ESI = pEh3Exce -> TryLevel ;
if ( ESI == - 1 )
break ;
if ( ESI == nToLevel )
break ;
nLevel = pScope [ ESI ]. ParentLevel ;
pEh3Exce -> TryLevel = nLevel ;
if ( pScope [ ESI ]. FilterFunc != NULL ) // 有FilterFunc则是exception handler
continue ;
EAX = pScope [ ESI ]. HandlerFunc ; // 无FilterFunc则是termination handler
_NLG_Notify ( 0x101 ); // 无返回值
CALL ( EAX ); // 这就是展开所求得的Handler,可以是termination handler,exception Handler
}
FS [ 0 ]=( DWORD ) eh3Unwind . pPrev ;
}
// _except_handler3执行两次原因:
// 举例:EH3_List如 FS[0]->E1->E2->E3->E4,在E4中发生异常,依次搜索E1,E2,E3,最后在E4中找到能处理此异常的 filter handler,这样E4,E3,E2,E1的_except_handler3均执行了一次,这是第一次.
// 第二次:在执行E4的exception handler前要先调用全局展开(_global_unwind2)以运行E1,E2,E3的termination handler
// 在全局展开(_global_unwind2)中将为EXCEPTION_RECORD.ExceptionFlags增加标志_EH_UNWINDING(2),再依次调用它们的
// _except_handler3,这就是E1,E2,E3的_except_handler3的第二次调用,但E4只有一次.
int _except_handler3 ( EXCEPTION_RECORD * pExceRec , _EH3_EXCEPTION_REGISTRATION * pEh3Exce ,
CONTEXT * pContextRecord /*,void * DispatcherContext*/ )
{
if (( pExceRec -> ExceptionFlags & 6 ) == 0 )
{ // 无_EH_UNWINDING and _EH_EXIT_UNWIND
EXCEPTION_POINTERS ePointer ;
ePointer . ExceptionRecord = pExceRec ;
ePointer . ContextRecord = pContextRecord ;
// pEh3Exce=&EhRecord
// lea EAX, [EBP+var_ExcePointers]
// mov [ebx-4], EAX ;EBX=pEh3Exce
*(( DWORD *) pEh3Exce - 1 )=( DWORD )& ePointer ; // 给_XcptFilter的参数pExceptPointers赋值
if ( ValidateEH3RN ( pEh3Exce ))
{
_SCOPETABLE_ENTRY * pScopeTable = pEh3Exce -> pScopeTable ;
int i = pEh3Exce -> TryLevel ;
while ( 1 )
{
if ( i ==- 1 )
return 1 ; // handler拒绝处理这个异常
EAX = pScopeTable [ i ]. FilterFunc ;
if ( EAX != NULL )
{ // 属于exception handler
EBP =( DWORD ) pEh3Exce + 0x10 ; // 恢复原TRY基址pEh3Exce=&crtMain!EhRecord,sizeof(EhRecord)=0x10
CALL ( EAX ); // 没有参数,ePointer已传过去
if ( EAX != 0 )
{
if (( int ) EAX < 0 )
return 0 ; // 如果在filter中返回值<0,此返回后会引起代码为
// EXCEPTION_NONCONTINUABLE_EXCEPTION(0xC0000025)的异常,反复如此栈溢出
_global_unwind2 ( pEh3Exce ); // 调用handler前,展开前级函数中的termination handler
EBP =( DWORD ) pEh3Exce + 0x10 ; // 恢复原TRY基址pEh3Exce=&crtMain!EhRecord,sizeof(EhRecord)=0x10
_local_unwind2 ( pEh3Exce , i ); // 此函数内不直接用EBP,但暗传EBP给可能执行的_NLG_Notify
// 并展开本函数中的termination handler
EAX = pScopeTable [ i ]. HandlerFunc ;
_NLG_Notify ( 1 ); // 此函数内要用EAX
pEh3Exce -> TryLevel = pScopeTable [ i ]. ParentLevel ; // 修改节点所在层次使EIP在TRY中位置表示正确
EAX = pScopeTable [ i ]. HandlerFunc ;
CALL ( EAX ); // 进入异常处理,不再返回此函数(内部可接受异常并修改环境,继续执行)
}
}
i = pScopeTable [ i ]. ParentLevel ;
}
return 0 ;
}
else
{
pExceRec -> ExceptionFlags |= 8 ; // 置pEh3Exce无效标志,它不属于此handler
return 1 ; // handler拒绝处理这个异常,系统转调上一级
}
}
else
{ // 有_EH_UNWINDING or _EH_EXIT_UNWIND
EBP =( DWORD ) pEh3Exce + 0x10 ; // 恢复原TRY基址pEh3Exce=&crtMain!EhRecord,sizeof(EhRecord)=0x10
_local_unwind2 ( pEh3Exce ,- 1 );
}
return 1 ;
}
6. 结语
SEH代码还有很多 , 它们有许多包含在NTDLL . DLL中 , 比如RtlUnwind我曾经非常想翻译它 , 但至今我也还未完成 , 因此我也就没有粘出来 . 实际上从我以上粘出的初步代码 , 我们应该能看出SEH并不复杂 , 翻译DLL中的相关代码也并不困难 , 我估计我们唯一无法看见的代码就是中断处理的起始部分 , 把它想像成一个黑匣子就成了 . 附件有我的IDA注释说明 , 注释由于是一个动态过程 , 特别是起初的注释可能有错 , 我没有精力再去一一阅读更正 , 希望大家理解 , 一并奉献上 , 希望对大伙有用 .
在这里 , 我只是作了SEH源码的一部分翻译工作 , 在翻译过程中我也在网上查看了很多关于SEH方面的文章 , 在此我不再一一列出 , 一并致谢这些无私奉献的网友和同行们 ! 看了此文相信再看其它SEH专著就应该容易多了 !
今天就是我们的嫦娥飞天的日子 , 谨以此文预祝她能回宫成功 ! 顺便也希望此文是我下一个工作的起始点 !