VMP处理SEH

2020-03-17 22:53:48 浏览数 (2)

基础

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:浮点操作的结果小于允许的值。

0 人点赞