标 题
: 
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专著就应该容易多了
!
      
今天就是我们的嫦娥飞天的日子
,
谨以此文预祝她能回宫成功
!
顺便也希望此文是我下一个工作的起始点
!