首页 > 其他分享 >try-except异常

try-except异常

时间:2022-09-30 19:11:33浏览次数:59  
标签:EXCEPTION 00000000 代码 except try finally 异常

在Windows中使用SEH异常

Windows SEH异常处理,可以用结构体

typedef struct _EXCEPTION_REGISTRATION_RECORD {
struct _EXCEPTION_REGISTRATION_RECORD *Next;  下一个异常处理结构体的地址
PEXCEPTION_ROUTINE Handler;  当前异常处理函数的首地址
} EXCEPTION_REGISTRATION_RECORD;

来表示,当我们使用汇编编写代码时,x86下,不论是在用户态还是在内核态,都使用fs:[0]来指向当前的异常处理结构体地址。  x64下应该使用gs段寄存器,此处只讨论x86即使用fs段寄存器。

fs寄存器指向了 NT_TIB

在windbg中使用 dt _NT_TIB

0:000> dt _NT_TIB
+0x000 ExceptionList : Ptr32 _EXCEPTION_REGISTRATION_RECORD
+0x004 StackBase : Ptr32 Void
+0x008 StackLimit : Ptr32 Void
+0x00c SubSystemTib : Ptr32 Void
+0x010 FiberData : Ptr32 Void
+0x010 Version : Uint4B
+0x014 ArbitraryUserPointer : Ptr32 Void
+0x018 Self : Ptr32 _NT_TIB

偏移0x18指向 NT_TIB首地址,可以看到NT_TIB首地址是EXCEPTION_REGISTRATION_RECORD数据。

在x86 windbg中查看

0:000> dd fs:[0]
0053:00000000 0039f8b8 003a0000 0039c000 00000000
0053:00000010 00001e00 00000000 00418000 00000000
0053:00000020 00000b18 00003364 00000000 006377b8
0053:00000030 00415000 00000000 00000000 00000000
0053:00000040 00000000 00000000 00000000 00000000
0053:00000050 00000000 00000000 00000000 00000000
0053:00000060 00000000 00000000 00000000 00000000
0053:00000070 00000000 00000000 00000000 00000000
0:000> dt _NT_TIB
RaiseInt3!_NT_TIB
+0x000 ExceptionList : Ptr32 _EXCEPTION_REGISTRATION_RECORD
+0x004 StackBase : Ptr32 Void
+0x008 StackLimit : Ptr32 Void
+0x00c SubSystemTib : Ptr32 Void
+0x010 FiberData : Ptr32 Void
+0x010 Version : Uint4B
+0x014 ArbitraryUserPointer : Ptr32 Void
+0x018 Self : Ptr32 _NT_TIB
0:000> dd 00418000
00418000 0039f8b8 003a0000 0039c000 00000000
00418010 00001e00 00000000 00418000 00000000
00418020 00000b18 00003364 00000000 006377b8
00418030 00415000 00000000 00000000 00000000
00418040 00000000 00000000 00000000 00000000
00418050 00000000 00000000 00000000 00000000
00418060 00000000 00000000 00000000 00000000
00418070 00000000 00000000 00000000 00000000

可以看到NT_TIB地址为00418000

在汇编中使用SEH时 需要通过如下形式:

push offset  sehHandle   注释:sehHandle为异常处理函数

push dword ptr fs:[0]     注释:next

mov fs:[0],esp     注释:让fs:[0]指向当前栈顶,当前栈顶是_EXCEPTION_REGISTRATION_RECORD结构体。

通过这几条语句可以在栈上建立一个新的SEH结构体,并让FS:[0]指向这个新的SEH结构体。

在栈上去掉这个SEH框架时,通过如下语句:

mov eax, dword ptr fs:[0] 取出结构体

mov eax,[eax]

mov fs:[0],eax

此处没有考虑平衡堆栈,通过这几条语句从SEH链表中去掉栈上的当前SEH框架。

 

这是在汇编中可以使用此种方式,但是在VC++中如果每一个要插入异常处理的地方都需要程序员手动插入SEH框架,那太过繁琐且容易出错。

VC++编译器为了在编程中支持使用SEH,方便程序员编写代码,使用try except finally关键字来支持程序员编写代码。

__try
{
  //此处编写可能会抛出异常的代码
}
__except (EXCEPTION_CONTINUE_EXECUTION)  
{
  //此处为异常处理函数 

}

上面代码中__except 括号中可以为常量或者函数,或者多个函数,含义为异常过滤函数

__except 括号中的数据可以如下:

#define EXCEPTION_EXECUTE_HANDLER 1   表明执行except下面中括号中的代码 ,即执行异常处理函数,含义为 异常过滤函数返回EXCEPTION_EXECUTE_HANDLER ,那么就执行我下面的异常处理函数
#define EXCEPTION_CONTINUE_SEARCH 0   表明不执行except下面括号中的代码,此次发生的异常当前的异常处理函数处理不了,需要继续往后面搜寻,即使用EXCEPTION_REGISTRATION_RECORD中的next变量继续往后面搜寻。
#define EXCEPTION_CONTINUE_EXECUTION -1  表明不执行except下面括号中的代码,此次发生的异常当前的异常过滤函数已经处理了即修复了异常,可以从异常触发的地方继续往后面执行。

一般情况下异常过滤函数都是一个函数,动态的根据异常类型来选择返回上面三个变量中的某一个。

需要说明的是只有发生了异常才可能执行异常过滤函数,只有异常过滤函数返回EXCEPTION_EXECUTE_HANDLER ,才会执行下面中括号中的异常处理函数。

类似于try except还有一种形式为 try finally,如下

PVOID vv = NULL;
__try
{
  vv = malloc(10);

  __leave;

  printf("Hello");
}
__finally
{
  if (NULL != vv)
  {
    free(vv);
    vv = NULL;
  }
}

finally被称为异常终止处理函数,try except和try fianlly是可以相互嵌套的。

finally的含义是finally中括号中的代码必定会被执行,不论是否发生异常,所以finally更适用于对于那些需要释放资源,释放锁等需求,可以编写更优雅的代码,不需要在每个return前面检查已经申请的资源是否需要释放。

在try中还可以使用__leave可以更优雅的退出try代码块,进入finally前置的一个代码块,在前置代码块中会对某个局部变量进行赋值,用来后续判断是异常进入了finally代码块还是自己主动退出try块进入异常代码块。

在finally中可以使用AbnormalTermination()来判断是由于异常进入了终止处理代码中还是使用了__leave进入了终止处理代码,AbnormalTermination就是使用局部变量来判断的。

 

try的用法介绍完毕,下面介绍为了支持try这些关键字,编译器做了哪些事情?

VC++编译器为了支持try except finally这些关键字,扩展了EXCEPTION_REGISTRATION_RECORD这个结构体的内容。当使用try块嵌套时,一个函数中使用的所有嵌套try只会在栈上保存一个扩展的SEH结构。

包含如下内容:

EXCEPTION_REGISTRATION_RECORD

TryArray  这是自己定义的名字,实际不是这样的

tryLevel   初始为0FFFFFFFEh,表明不在try块中

ebp

在栈上的框架中,sehhandle指向_except_handler4(或者其他的函数,根据当前编译器版本),在vs中是个类似的固定函数,TryArray指向一个数据结构,前面16字节的数据(这是在vs2013中看到的)

下面是数组,数组中包含多项内容,每一项如下:

pre   默认为0xFFFFFFE

filterFunc  异常过滤函数

executeFunction

可以使用VS编写代码来查看更加详细的内容,此处介绍笔者通过学习其他的书籍、视频等理解的内容。

在汇编代码执行时,每一次进入不同的try时,会给tryLevel这个变量赋予一个新的值,作为索引,每一次离开一个try时,又会重新修改这个值。FFFFFFFF

在刚进入此函数前,会将代码中所有的异常过滤、异常处理、try的嵌套内容都保存在一个数组中

在触发异常时,会进入内核态,内核态再进入用户态中的nt!KiUserExceptionDispatcher函数,在调用异常处理函数_except_handler4时,会根据tryLevel来判断当前是哪个try块中的代码出了问题。

根据索引在上面提到的数组中找到对应的项,

如果filterFunc为NULL,那么代表这是try finally块,会继续往上寻找。如果pre为0xFFFFFFE,那么代表不需要再往上寻找了,在本函数中,这已经是最外层的try了,这时继续往上寻找异常过滤函数的话,已经不在此函数范围内了。如果pre不是0xFFFFFFE(如果有一层嵌套,那么pre为0,如果两层嵌套,那么最里面的嵌套pre为1,这是通过VS2013调试出来的),那么代表在当前函数中还有其它的try包含了当前这个try,那么可以让索引-1,找到前面那个索引,然后查看异常过滤函数、异常处理函数等,进行处理。

如果filterFunc为不NULL,那么表明有异常过滤函数,那么执行异常过滤函数,根据返回值判断是否执行异常处理函数,以及是否继续往下执行、继续往上搜索等。

需要注意有如下几点:

1、except括号中哪怕是一个常量,如EXCEPTION_EXECUTE_HANDLER,也会编译成函数形式,如xor eax,eax ret   因为在异常处理函数中会以函数调用的方式调用异常过滤函数

2、finally代码块执行前有个前置代码模块,会设置局部变量tryLevel,表明已经离开了当前try块,如果修改为0FFFFFFFEh表明不在任何try块中,其余为0 1 2等都表明在try块中。

3、如果在finally代码块中调用AbnormalTermination()函数,那么finally前置代码块还会修改一个局部变量,这个局部变量用来表明是正常运行到finally(包括try正常执行完毕和使用__leave关键字),还是通过异常(展开)调用的finally函数。

4、finally代码块中代码为函数形式,finally前置代码块以call的方式调用finally代码块,TryArray中保存的是finally代码块的地址,不是前置块的地址,_except_handler4函数也是以call的方式调用finally块代码的方式。

5、finally的执行时机有两种:第一种为try调用完成或者try中使用了__leave关键字,如果是try代码调用完成,那么顺序执行finally前置代码执行块,在这个块中call调用finally代码块。如果是__leave关键字,那么会使用jmp等跳转指令跳转到finally前置代码块起始处。

第二种为触发了异常,因为当前是finally代码块,所以只能往上搜寻是否有过滤函数可以处理此异常,假如有过滤函数返回了可以处理,那么在调用异常处理函数前,会有一个叫做unwind展开的过程,分为局部展开和全局展开,不懂,暂时理解为展开,这个展开过程会再次搜寻之前的异常SEH框架,对于那些没有异常过滤函数即finally代码块,会去调用finally代码块来做一些释放资源等程序员希望做的事情。而调用finally时会去访问finally块所在的函数栈帧中的局部变量,如果在执行finally代码时查看寄存器的值,会发现此时esp会出于比较高的地址,即虚拟地址比较小,而ebp与原来执行try块代码时一致,访问局部变量时是使用ebp来进行访问,而操作系统提供的异常处理框架会在unwind以及调用过滤处理函数时还原ebp的值。

 

问题:

1、在调用异常过滤函数以及unwind时如何还原ebp的值?

2、默认的异常过滤函数返回,即继续搜寻,可以调用到finally函数,应该是因为顶层异常了,所以unwind。使用自定义函数返回EXCEPTION_CONTINUE_SEARCH或者EXCEPTION_EXECUTE_HANDLER都可以unwind。使用EXCEPTION_CONTINUE_EXECUTION无法unwind这也正常,从异常处继续执行当然不需要继续unwind了。

 

在14.30.30705\crt\src\i386\chandler4.c代码中找到如下数据结构体,下次再继续学习。

typedef struct _EH4_EXCEPTION_REGISTRATION_RECORD
{
PVOID SavedESP;
PEXCEPTION_POINTERS ExceptionPointers;
EXCEPTION_REGISTRATION_RECORD SubRecord;
UINT_PTR EncodedScopeTable;
ULONG TryLevel;
} EH4_EXCEPTION_REGISTRATION_RECORD, *PEH4_EXCEPTION_REGISTRATION_RECORD;
typedef LONG(__cdecl *PEXCEPTION_FILTER_X86)(void);   异常过滤函数类型
typedef void(__cdecl *PEXCEPTION_HANDLER_X86)(void);   异常处理函数类型
typedef void(__fastcall *PTERMINATION_HANDLER_X86)(BOOL);   这个BOOL是代表什么含义,第一次第二次还是 局部全局  终止处理函数类型
typedef struct _EH4_SCOPETABLE_RECORD
{
ULONG EnclosingLevel;
PEXCEPTION_FILTER_X86 FilterFunc;
union
{
PEXCEPTION_HANDLER_X86 HandlerAddress;
PTERMINATION_HANDLER_X86 FinallyFunc;
} u;
} EH4_SCOPETABLE_RECORD, *PEH4_SCOPETABLE_RECORD;

 

标签:EXCEPTION,00000000,代码,except,try,finally,异常
From: https://www.cnblogs.com/ps12345678/p/16745884.html

相关文章