基础
SEH(Structured Exception Handling)结构化异常处理
SEH实际包含两个主要功能:结束处理(termination handling)和异常处理(exceptionhandling)
结束处理(__finally)
代码语言:javascript复制 __try
{// todo
}
__finally
{// todo
}
__try和__finally关键字用来标出结束处理程序两段代码的轮廓。在上面的代码段中,操作系统和编译程序共同来确保结束处理程序中的__finally代码块能够被执行,不管保护体(try块)是如何退出的。不论你在保护体中使用return,还是goto,或者是long jump,结束处理程序(__finally块)都将被调用
示例1:(正常流程)
代码语言:javascript复制DWORD SEHTest()
{
// 第一步
DWORD dwTemp;
HANDLE hSem = CreateSemaphoreA(NULL,1,1,NULL);
__try
{// 第二步
WaitForSingleObject(hSem,INFINITE);
dwTemp = 5;
}
__finally
{// 第三步
ReleaseSemaphore(hSem,1,NULL);
}
// 第四步
return dwTemp;
}
示例2:(__try代码块中return,返回值为5)
代码语言:javascript复制DWORD SEHTest()
{
// 第一步
DWORD dwTemp;
HANDLE hSem = CreateSemaphoreA(NULL,1,1,NULL);
__try
{// 第二步
WaitForSingleObject(hSem,INFINITE);
dwTemp = 5;
// 在return之前会执行第三步
return dwTemp;
}
__finally
{// 第三步
ReleaseSemaphore(hSem,1,NULL);
}
// 此步不会被执行到
dwTemp = 9;
return dwTemp;
}
当编译程序检查源代码时,它看到在try块中有return语句,这样,编译程序就生成代码将返回值(本例中是5)保存在一个编译程序建立的临时变量中。编译程序然后再生成代码来执行finally块中包含的指令,这称为局部展开。更特殊的情况是,由于try块中存在过早退出的代码,从而产生局部展开,导致系统执行finally块中的内容。在finally块中的指令执行之后,编译程序临时变量的值被取出并从函数中返回。
可以用IDA看到:
代码语言:javascript复制call ds:WaitForSingleObject(x,x)
mov [ebp dwTemp], 5 ; 5保存到临时变量
mov ecx, [ebp dwTemp]
示例3:(__try代码块中goto,同样会执行到__finaly)
代码语言:javascript复制DWORD SEHTest()
{
// 第一步
DWORD dwTemp;
HANDLE hSem = CreateSemaphoreA(NULL,1,1,NULL);
__try
{// 第二步
WaitForSingleObject(hSem,INFINITE);
dwTemp = 5;
goto Go;
}
__finally
{// 第三步
ReleaseSemaphore(hSem,1,NULL);
}
Go:
// 第四步
return dwTemp;
}
当编译程序看到try块中的goto语句,它首先生成一个局部展开来执行finally块中的内容。这一次,在finally块中的代码执行之后,在Go标号之后的代码将执行,因为在try块和finally块中都没有返回发生。这里的代码使函数返回5。而且,由于中断了从try块到finally块的自然流程,可能要蒙受很大的性能损失(取决于运行程序的CPU)。
示例4:(异常退出)
代码语言:javascript复制DWORD Sub_SEHTest()
{
// 错误产生
int i=0;
return 10/i;
}
DWORD SEHTest()
{
// 第一步
DWORD dwTemp;
HANDLE hSem = CreateSemaphoreA(NULL,1,1,NULL);
__try
{// 第二步
WaitForSingleObject(hSem,INFINITE);
dwTemp = Sub_SEHTest();// 此处会产生错误,但仍会调到第三步
}
__finally
{// 第三步
ReleaseSemaphore(hSem,1,NULL);
}
return dwTemp;
}
再假想一下,try块中的Funcinator函数调用包含一个错误,会引起一个无效内存访问。如果没有SEH,在这种情况下,将会给用户显示一个很常见的ApplicationError对话框。当用户忽略这个错误对话框,该进程就结束了。当这个进程结束(由于一个无效内存访问),信标仍将被占用并且永远不会被释放,这时候,任何等待信标的其他进程中的线程将不会被分配CPU时间。但若将对ReleaseSemaphore的调用放在finally块中,就可以保证信标获得释放,即使某些其他函数会引起内存访问错误。
示例5:(跳转流程分析)
代码语言:javascript复制DWORD SEHTest()
{
DWORD dwTemp = 0;
while (dwTemp<10)
{//0
__try
{
if (2 == dwTemp)
{
continue;//1
}
if (3 == dwTemp)
{
break;//2
}
}
__finally
{
dwTemp ;//3
}
dwTemp ;//4
}
dwTemp = 10;//5
return dwTemp;
}
1.dwTemp = 0;执行正常的__finally中的dwTemp ,以及随后的dwTemp ,此时dwTemp=2(流程:0->3->4->0)
2.continue被执行,因为要跳出__try代码块,所以执行__finally的dwTemp ,dwTemp=3(接着流程:1->3->0)
3.dwTemp=3时,因为要跳出__try代码块,所以执行__finally的dwTemp ,dwTemp=4,dwTemp 10=14(接着流程:2->3->5)
示例6:(__try代码块的return值可以被__finally块的覆盖,下例返回103)
代码语言:javascript复制DWORD SEHTest()
{
// 第一步
DWORD dwTemp;
HANDLE hSem = CreateSemaphoreA(NULL,1,1,NULL);
__try
{// 第二步
WaitForSingleObject(hSem,INFINITE);
dwTemp = 5;
// 在return之前会执行第三步
return dwTemp;
}
__finally
{// 第三步
ReleaseSemaphore(hSem,1,NULL);
return 103;//103会把5覆盖
}
// 此步不会被执行到
dwTemp = 9;
return dwTemp;
}
示例7:(__leave可以减少局部展开的开销)
代码语言:javascript复制DWORD SEHTest(int dwTemp)
{
bool bRet = false;
__try
{
if (0 == dwTemp)
{
dwTemp ;
__leave;
}
else if (1 == dwTemp)
{
dwTemp --;
__leave;
}
bRet = true;//用于区分是否为正常跳转到finally
}
__finally
{
if (bRet)
{
dwTemp =10;
}
}
return dwTemp;
}
在try块中使用__leave关键字会引起跳转到try块的结尾。可以认为是跳转到try块的右大括号。由于控制流自然地从try块中退出并进入finally块,所以不产生系统开销。当然,需要引入一个新的Boolean型变量bRet,用来指示函数是成功或失败。这是比较小的代价。
关于finally块的说明
1.从try块进入finally块的正常控制流。
2.局部展开:从try块的过早退出(goto、longjump、continue、break、return等)强制控制转移到finally块。
3.全局展开(globalunwind),在发生的时候没有明显的标识,我们在示例4已经见到。在SEHTest的try块中,有一个对Sub_SEHTest函数的调用。如果Sub_SEHTest函数引起一个内存访问违规(memoryaccessviolation),一个全局展开会使SEHTest的finally块执行。
为了确定是哪一种情况引起finally块执行,可以调用内部函数
代码语言:javascript复制BOOL AbnormalTermination();
这个内部函数只在finally块中调用,返回一个Boolean值。指出与finally块相结合的try块是否过早退出。换句话说,如果控制流离开try块并自然进入finally块,AbnormalTermination将返回FALSE。如果控制流非正常退出try块—通常由于goto、return、break或continue语句引起的局部展开,或由于内存访问违规或其他异常引起的全局展开—对AbnormalTermination的调用将返回TRUE。没有办法区别finally块的执行是由于全局展开还是由于局部展开。但这通常不会成为问题,因为可以避免编写执行局部展开的代码。
异常处理(__except)
代码语言:javascript复制 __try
{}
__except(1)
{}
示例1:(不会执行的__except代码块)
代码语言:javascript复制DWORD SEHTest()
{
DWORD dwTemp;
__try
{
dwTemp = 0;
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
// 此处不会执行
}
return dwTemp;
}
try块中,只是把一个0赋给dwTemp变量。这个操作决不会造成异常的引发,所以except块中的代码永远不会执行
示例2:(正常引发__except异常处理)
代码语言:javascript复制DWORD SEHTest()
{
DWORD dwTemp = 0;
__try
{
dwTemp = 5/dwTemp;// 除以0!
dwTemp = 10;
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
MessageBeep(0);
}
dwTemp = 20;
return dwTemp;
}
try块中有一个指令试图以0来除5。CPU将捕捉这个事件,并引发一个硬件异常。当引发了这个异常时,系统将定位到except块的开头,并计算异常过滤器表达式的值,过滤器表达式的结果值只能是下面三个标识符之一,这些标识符定义在Windows的Excpt.h文件中
标识符 | 定义为 |
---|---|
E X C E P T I O N _ E X E C U T E _ H A N D L E R | 1 |
E X C E P T I O N _ C O N T I N U E _ S E A R C H | 0 |
E X C E P T I O N _ C O N T I N U E _ E X E C U T I O N | -1 |
流程图如下:
(注意:最里层try块是指包含了这个异常代码的最里层的try块,不包含的不算)
代码语言:javascript复制 DWORD dwTemp = 0;
__try
{
dwTemp = 5/dwTemp;//异常
__try
{
}
__except(1)// 这个未包含异常,所以执行的是外面那个except
{
int j= 0;
}
}
__except(1)// 这个被执行
{
int k = 0;
}
EXCEPTION_EXECUTE_HANDLER
这个值的意思是要告诉系统:“我认出了这个异常。即,我感觉这个异常可能在某个时候发生,我已编写了代码来处理这个问题,现在我想执行这个代码。”在这个时候,系统执行一个全局展开(本章后面将讨论),然后执行向except块中代码(异常处理程序代码)的跳转。在except块中代码执行完之后,系统考虑这个要被处理的异常并允许应用程序继续执行。这种机制使Windows应用程序可以抓住错误并处理错误,再使程序继续运行,不需要用户知道错误的发生。
但是,当except块执行后,代码将从何处恢复执行?稍加思索,我们就可以想到几种可能性
1.从产生异常的CPU指令之后恢复执行,即执行示例2中的dwTemp =10
2.是从产生异常的指令恢复执行,如果在except块中有这样的语句会怎么样呢,对应
代码语言:javascript复制dwTemp = 2;
可以从产生异常的指令恢复执行。这一次,将用2来除5,执行将继续,不会产生其他的异常,对应EXCEPTION_CONTINUE_EXECUTION
3.从except块之后的第一条指令开始恢复执行,即执行dwTemp =20;对应EXCEPTION_EXECUTE_HANDLER
当一个异常过滤器的值为EXCEPTION_EXECUTE_HANDLER时,系统必须执行一个全局展开(globalunwind)。这个全局展开使某些try-finally块恢复执行,某些try-finally块指在处理异常的try_except块之后开始执行但未完成的块
示例3:
代码语言:javascript复制void Sub_SEHTest()
{
DWORD dwTemp = 0;
HANDLE hSem = CreateSemaphoreA(NULL,1,1,NULL);
__try
{//2
WaitForSingleObject(hSem,INFINITE);
dwTemp = 5/dwTemp;// 异常!!!
}
__finally
{//3
ReleaseSemaphore(hSem,1,NULL);
}
}
void SEHTest()
{
__try
{//1
Sub_SEHTest();
}
__except(EXCEPTION_EXECUTE_HANDLER)
{//4
// TODO.
int j=0;
}
}
1.SEHTest开始执行,进入它的__try块并调用Sub_SEHTest
2.Sub_SEHTest开始执行,等到信标,然后除0产生异常
3.系统因此取得控制,开始搜索一个与except块相配的try块,因为Sub_SEHTest的__try对应的是__finally,所以它向上查找
4.系统在SEHTest中找到相配的_except
5.系统现在计算与SEHTest中except块相联的异常过滤器的值,并等待返回值。当系统看到返回值是EXCEPTION_EXECUTE_HANDLER的,系统就在Sub_SEHTest的finally块中开始一个全局展开
6.对于一个全局展开,系统回到所有未完成的try块的结尾,查找与finally块相配的try块。在这里,系统发现的finally块是Sub_SEHTest中所包含的finally块。从而执行finally块
7.在finally块中包含的代码执行完之后,系统继续上溯,查找需要执行的未完成finally块。在这个例子中已经没有这样的finally块了。系统到达要处理异常的try-except块就停止上溯。这时,全局展开结束,系统可以执行except块中所包含的代码。
为了更好地理解这个执行次序,我们再从不同的角度来看发生的事情。当一个过滤器返回EXCEPTION_EXECUTE_HANDLER时,过滤器是在告诉系统,线程的指令指针应该指向except块中的代码。但这个指令指针在Sub_SEHTest的try块里。回忆一下前面提到的,每当一个线程要从一个try-finally块离开时,必须保证执行finally块中的代码。在发生异常时,全局展开就是保证这条规则的机制。
流程如下:
暂停全局展开
通过在finally块里放入一个return语句,可以阻止系统去完成一个全局展开
示例4:
代码语言:javascript复制void Sub_SEHTest()
{
DWORD dwTemp = 0;
HANDLE hSem = CreateSemaphoreA(NULL,1,1,NULL);
__try
{
WaitForSingleObject(hSem,INFINITE);
dwTemp = 5/dwTemp;// 异常!!!
}
__finally
{
ReleaseSemaphore(hSem,1,NULL);
return;///直接返回!!
}
}
void SEHTest()
{
__try
{
Sub_SEHTest();
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
// TODO.
int j=0;// 这里永远也调不到了
}
}
当全局展开时,先执行Sub_SEHTest的finally中的代码,它的return使系统完全停止了展开,从而无法执行到except块
EXCEPTION_CONTINUE_EXECUTION
前面为简单起见,在过滤器里直接硬编码了标识符EXCEPTION_EXECUTE_HANDLER,但实际上可以让过滤器调用一个函数确定应该返回哪一个标识符
示例5:
代码语言:javascript复制char g_szBuf[100];
LONG SEHFilter(char** ppBuf)
{
if (*ppBuf == NULL)
{
*ppBuf = g_szBuf;
return (EXCEPTION_CONTINUE_EXECUTION);
}
return EXCEPTION_EXECUTE_HANDLER;
}
void SEHTest()
{
int x = 0;
char* pBuf = NULL;
__try
{
*pBuf = 'j';
x = 5/x;
}
__except(SEHFilter(&pBuf))
{
// todo
int j = 0;
}
}
理论上应该是这样的:
1.*pBuf='j'引发异常,从而跳转到SEHFilter中,从而让pBuf指向g_szBuf,并返回EXCEPTION_CONTINUE_EXECUTION
2.继续试着执行*pBuf='j'成功,即g_szBuf[0]='j'
3.继续向下执行x = 5/x;除数为0,所以异常,从而跳转到SEHFilter中,这次返回EXCEPTION_EXECUTE_HANDLER
4.所以执行到except块
但实际上g_szBuf[0]='j'不一定能成立,因为EXCEPTION_CONTINUE_EXECUTION是让thread回到发生exception的机器指令,不是回到发生exception的C/C 语句
如果*pBuf = 'j'的机器指令如下:
代码语言:javascript复制00EC367D mov eax,dword ptr [ebp-2Ch]
00EC3680 mov byte ptr [eax],6Ah
那么,第二条指令产生异常。异常过滤器可以捕获这个异常,修改pBuf的值,并告诉系统重新执行第二条CPU指令。但问题是,寄存器的值可能不改变,不能反映装入到pBuf的新值、
如果编译程序优化了代码,继续执行可能顺利;如果编译程序没有优化代码,继续执行就可能失败。
GetExceptionCode(异常处理)
一个异常过滤器在确定要返回什么值之前,必须分析具体情况。例如,异常处理程序可能知道发生了除以0引起的异常时该怎么做,但是不知道该如何处理一个内存存取异常。异常过滤器负责检查实际情况并返回适当的值。
代码语言:javascript复制__try
{
x = 0;
y = 4 / x;
}
__except((GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO) ?
EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
{
// Handle divide by zero exception.
}
内部函数GetExceptionCode返回一个值,该值指出所发生异常的种类:
内部函数GetExceptionCode只能在一个过滤器中调用(--except之后的括号里),或在一个异常处理程序中被调用
代码语言:javascript复制___try
{
y = 0;
x = 4 / y;
}
__except(
((GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION) ||
(GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO)) ?
EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
{
switch(GetExceptionCode())
{
case EXCEPTION_ACCESS_VIOLATION:
//Handle the access violation.
break;
case EXCEPTION_INT_DIVIDE_BY_ZERO:
//Handle the integer divide by?.
break;
}
}
但是,不能在一个异常过滤器函数里面调用GetExceptionCode。编译程序会捕捉这样的错误。当编译下面的代码时,将产生编译错误
代码语言:javascript复制__try
{
y = 0;
x = 4 / y;
}
__except(CoffeeFilter())
{
// Handle the exception.
}
LONG CoffeeFilter(void)
{
//Compilation error: illegal call to GetExceptionCode.
return((GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION) ?
EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH);
}
可以按下面的形式改写代码:
代码语言:javascript复制__try
{
y = 0;
x = 4 / y;
}
__except(CoffeeFilter(GetExceptionCode()))
{
//Handle the exception.
}
LONG CoffeeFilter(DWORD dwExceptionCode)
{
return((dwExceptionCode == EXCEPTION_ACCESS_VIOLATION) ?
EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH);
}
异常处理错误码
1.与内存有关的异常
EXCEPTION_ACCESS_VIOLATION:线程试图对一个虚地址进行读或写,但没有做适当的存取。这是最常见的异常。
EXCEPTION_DATATYPE_MISALIGNMENT:线程试图读或写不支持对齐(alignment)的硬件上的未对齐的数据。例如,16位数值必须对齐在2字节边界上,32位数值要对齐在4字节边界上。
EXCEPTION_ARRAY_BOUNDS_EXCEEDED:线程试图存取一个越界的数组元素,相应的硬件支持边界检查。
EXCEPTION_IN_PAGE_ERROR:由于文件系统或一个设备启动程序返回一个读错误,造成不能满足要求的页故障。
EXCEPTION_GUARD_PAGE:一个线程试图存取一个带有PAGE_GUARD保护属性的内存页。该页是可存取的,并引起一个EXCEPTION_GUARD_PAGE异常。
EXCEPTION_STACK_OVERFLOW:线程用完了分配给它的所有栈空间。
EXCEPTION_ILLEGAL_INSTRUCTION:线程执行了一个无效的指令。这个异常由特定的CPU结构来定义;在不同的CPU上,执行一个无效指令可引起一个陷井错误。
EXCEPTION_PRIV_INSTRUCTION:线程执行一个指令,其操作在当前机器模式中不允许。
2.与异常相关的异常
EXCEPTION_INVALID_DISPOSITION:一个异常过滤器返回一值,这个值不是EXCEPTION_EXECUTE_HANDLER、EXCEPTION_CONTINUE_SEARCH、EXCEPTION_CONTINUE_EXECUTION三者之一。
EXCEPTION_NONCONTINUABLE_EXCEPTION:一个异常过滤器对一个不能继续的异常返回EXCEPTION_CONTINUE_EXECUTION。
3.与调试有关的异常
EXCEPTION_BREAKPOINT:遇到一个断点。
EXCEPTION_SINGLE_STEP:一个跟踪陷井或其他单步指令机制告知一个指令已执行完毕。
EXCEPTION_INVALID_HANDLE:向一个函数传递了一个无效句柄。
4.与整数有关的异常
EXCEPTION_INT_DIVIDE_BY_ZERO:线程试图用整数0来除一个整数。
EXCEPTION_INT_OVERFLOW:一个整数操作的结果超过了整数值规定的范围。
5.与浮点数有关的异常
EXCEPTION_FLT_DENORMAL_OPERAND:浮点操作中的一个操作数不正常。不正常的值是一个太小的值,不能表示标准的浮点值。
EXCEPTION_FLT_DIVIDE_BY_ZERO:线程试图用浮点数0来除一个浮点。
EXCEPTION_FLT_INEXACT_RESULT:浮点操作的结果不能精确表示成十进制小数。
EXCEPTION_FLT_INVALID_OPERATION:表示任何没有在此列出的其他浮点数异常。
EXCEPTION_FLT_OVERFLOW:浮点操作的结果超过了允许的值。
EXCEPTION_FLT_STACK_CHECK:由于浮点操作造成栈溢出或下溢。
EXCEPTION_FLT_UNDERFLOW:浮点操作的结果小于允许的值。