前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >VMP处理SEH

VMP处理SEH

原创
作者头像
franket
修改2020-03-17 22:53:48
1.5K0
修改2020-03-17 22:53:48
举报
文章被收录于专栏:技术杂记技术杂记

基础

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

流程图如下:

1.png
1.png

(注意:最里层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块中的代码。在发生异常时,全局展开就是保证这条规则的机制。

流程如下:

1.jpg
1.jpg
暂停全局展开

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
作者已关闭评论
0 条评论
热度
最新
推荐阅读
目录
  • 基础
    • SEH(Structured Exception Handling)结构化异常处理
      • 结束处理(__finally)
      • 异常处理(__except)
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
http://www.vxiaotou.com