七.Windows平台上的SEH模拟
在前面的文章里我们实现了Windows(VC编译器)/Linux/OSX这3种操作系统上的统一的模拟SEH异常处理架构。万万没想到的是MINGW作为Windows编译器,居然不支持SEH, MINGW32完全不支持SEH,MINGW64要新版本才支持。只能为MINGW再写一个模拟SEH支持。
MINGW环境编译出来的应用程序是在Windows运行的,而SEH架构的一部分服务是由操作系统提供的,因此就不要浪费了,手动实现__try/__except部分代码配合操作系统吧。
先研究SEH内部实现,关于SEH内部实现在网上找到这篇文章
深入解析结构化异常处理(SEH) - by Matt Pietrek-CSDN博客
本文只对模拟SEH需要用到的部分作简单介绍,想深入研究SEH的读者请仔细阅读上面链接中的文章。
同时由于SEH本身跟CPU关联紧密,因此本文只讨论x86/32位架构下的SEH模拟。
首先,我们要注意SEH使用堆栈和FS:[0]来维护异常处理链,它天生就是线程相关的,因此既不需要我们建立异常管理块也不需要考虑定义线程相关的变量。
Windows 32位SEH架构
异常块结构定义如下
typedef struct _EXCEPTION_REGISTRATION
{
struct _EXCEPTION_REGISTRATION Prev;
EXCEPTION_DISPOSITION __stdcall __welees_ExceptionPreHandler(PEXCEPTION_RECORD pER, PEXCEPTION_HANDLER_NODE pNode,IN PCONTEXT pContext,IN PVOID pDC);
}
Prev指向上一层异常块,如果Prev=0xffffffff则表示当前块是最上层异常处理块。
__weLees_ExceptionPreHandler是缺省的异常过滤函数,在发生异常之后,OS会调用这个函数去取处理异常信息。在VC的runtime中,它就是__except_handler3。__except_handler3的内部实现比较复杂,我们只是实现转换参数并调用异常过滤例程,所以使用以下代码实现:
EXCEPTION_DISPOSITION __stdcall __welees_ExceptionPreHandler(IN PEXCEPTION_RECORD pER,IN PEXCEPTION_HANDLER_NODE pNode,IN PCONTEXT pContext,IN PVOID pDC)
{
int i;
EXCEPTION_POINTERS Param;
EXCEPTION_DISPOSITION edResult;
printf("Current exception node %p\n",pNode);
Param.ContextRecord=pContext;
Param.ExceptionRecord=pER;//为异常过滤函数准备参数
i=pNode->CustomHandler(&Param);//调用异常过滤函数
printf("Exception filter result %d\n",i);
switch(i)
{
case EXCEPTION_CONTINUE_EXECUTION: //异常修复,恢复到发生异常的代码继续运行
printf("%s %d\n",__FILE__,__LINE__);
return ExceptionContinueExecution;
case EXCEPTION_EXECUTE_HANDLER://异常无法修复,执行善后清理工作
printf("%s %d\n",__FILE__,__LINE__);
asm volatile("mov %0, %%fs:0" : : "r" (pNode->Next));//注销当前异常处理块
longjmp(pNode->SectionEntry,1);
case EXCEPTION_CONTINUE_SEARCH://异常无法修复,尝试寻找上一层异常处理块
printf("%s %d\n",__FILE__,__LINE__);
return ExceptionContinueSearch;
}
}
这里要注意的有三点:
1. 在使用longjmp跳转到异常善后清理例程之前,需要先注销当前异常处理块,不然在跳转后会重复触发异常。
2. SEH机制一部分是由Windows实现的,所以代码里要做的工作比非Windows平台少,也不需要注册系统异常处理函数。
3. 在SEH架构中,如果异常过滤函数返回EXCEPTION_EXECUTE_HANDLER,异常善后清理例程会被调用两次以处理unwind事务。这一点跟在我们的解决方案里跟SEH是一样的。
程序员可以定义自己的异常处理块结构用于保存额外的信息,只要自定义结构继承于_EXCEPTION_REGISTRATION结构或者开头就是一个_EXCEPTION_REGISTRATION结构就行。
另外异常过滤函数的声明如下
int (*CustomHandler)(IN PEXCEPTION_POINTERS pContext);
由于SEH服务提供的CPU细节比Linux/OSX多得多,因此程序员可以比较轻松地修复异常。在本文中我们将会写一个异常修复成功继续运行的例子。
这里我们自定义的异常处理块结构定义如下
typedef struct _EXCEPTION_HANDLER_NODE:public _EXCEPTION_REGISTRATION
{
int (*CustomHandler)(IN PEXCEPTION_POINTERS pContext);//异常过滤函数
jmp_buf SectionEntry;//异常善后清理例程入口
}EXCEPTION_HANDLER_NODE,*PEXCEPTION_HANDLER_NODE;
缺省异常处理函数组织参数之后,调用当前异常处理块的异常过滤函数,然后通过返回值确定如何执行下一步操作,这里的逻辑跟非Windows平台的相同,就不赘述了。
同样的,因为异常处理块链的维护方式跟非Windows平台不同了,所以注册和注销的代码也变了:
#define TRY_START \
{ \
EXCEPTION_HANDLER_NODE __weLees__ExceptNode; \
/*Save local exception node*/ \
\
__weLees__ExceptNode.HandlerEntry=(FARPROC)__weLees_ExceptionPreHandler; \
__weLees__ExceptNode.RunStatus=setjmp(__weLees__ExceptNode.SectionEntry); \
\
if(2==__weLees__ExceptNode.RunStatus) \
{/*Init exception node*/ \
SEH_LOG(("%s %d Register exception Node %pH\n",__FILE__,__LINE__,&__weLees__ExceptNode)); \
asm volatile \
( \
"mov %%fs:0, %%eax\n\t" \
"mov %%eax, %[except_node_next]\n\t" \
"lea %[except_node], %%eax\n\t" \
"mov %%eax, %%fs:0" \
: /* no outputs */ \
: [except_node_next] "m" (__weLees__ExceptNode.Prev), \
[except_node] "m" (__weLees__ExceptNode) \
: "%eax", "memory" \
); \
/*Run custom code*/
#define TRY_EXCEPT(filter) \
SEH_LOG(("%s %d Unregister exception Node %pH\n",__FILE__, __LINE__,&__weLees__ExceptNode)); \
asm volatile \
( \
"mov %[except_node_next], %%eax\n\t" \
"mov %%eax, %%fs:0" \
: /* no outputs */ \
: [except_node_next] "m" (__weLees__ExceptNode.Prev) \
: "%eax", "memory" \
); \
} \
else if(!__weLees__ExceptNode.RunStatus) \
{/*Store the filter into exception node*/ \
__weLees__ExceptNode.CustomHandler=filter; \
longjmp(__weLees__ExceptNode.SectionEntry,2); \
} \
else
对于TRY_START和TRY_EXCEPT就不做逐句说明了,注册异常处理块就是把FS:[0]中的一个双字(旧异常处理块的地址)取出保存到异常处理块的Prev成员,然后把异常处理块地址保存到FS:[0]中。
注销异常处理块则相反,从FS:[0]中取得当前异常处理块指针,然后把当前异常处理块中Prev成员中保存的上一级异常处理块的地址写到FS:[0]中。
其他的基本逻辑跟非Windows平台相同。
八.彩蛋:Windows平台上的SEH模拟二-64位
一部分MINGW64编译器不支持SEH(带posix-seh/win32-seh特性的才支持)。此外已经写了MINGW32上的模拟了,再写一个MINGW64的SEH模拟作为彩蛋吧。
64位Windows平台上的SEH实现不同于32位平台。异常过滤函数的注册和清理回收块入口的定位不是在运行时实现的,而是在编译时统一记录到可执行文件的专用区中。因此,在Windows 64位平台上没有现在的SEH服务可以借用。于是,又回到了类似于非Windows平台的情况:注册系统异常处理函数,然后使用setjmp/longjmp管理和处理异常。所以让我们直接写代码吧:
#include <windows.h>
#include <stdio.h>
#include <setjmp.h>
#include <crtdbg.h>
typedef struct _EXCEPTION_NODE
{
struct _EXCEPTION_NODE *Prev;
int (*ExceptionFilter)(PEXCEPTION_POINTERS pExceptionInfo);
jmp_buf Entry;
UINT RunStatus;
}EXCEPTION_NODE,*PEXCEPTION_NODE;
__thread struct _SEH_SET
{
PEXCEPTION_NODE Chain;
int Initialized;
}s_Maintain={0};
#define TRY_START \
{ \
EXCEPTION_NODE __weLees_ExceptionNode; \
if(!s_Maintain.Initialized) \
{/*注册 VEH(优先级高于未处理过滤器)*/ \
AddVectoredExceptionHandler(1,DefVEHHandler); \
s_Maintain.Initialized=1; \
} \
__weLees_ExceptionNode.Prev=s_Maintain.Chain; \
s_Maintain.Chain=&__weLees_ExceptionNode; \
__weLees_ExceptionNode.RunStatus=setjmp(__weLees_ExceptionNode.Entry); \
if(2==__weLees_ExceptionNode.RunStatus) \
{
#define TRY_EXCEPT(filter) \
s_Maintain.Chain=__weLees_ExceptionNode.Prev; \
} \
else if(!__weLees_ExceptionNode.RunStatus) \
{ \
__weLees_ExceptionNode.ExceptionFilter=filter; \
longjmp(__weLees_ExceptionNode.Entry, 2); \
} \
else
#define TRY_END }
LONG WINAPI DefVEHHandler(PEXCEPTION_POINTERS pInfo)
{
int iResult;
PEXCEPTION_NODE pNode;
printf("VEH caught exception: 0x%08X\n",pInfo->ExceptionRecord->ExceptionCode);
while(s_Maintain.Chain)
{
pNode=s_Maintain.Chain;
iResult=s_Maintain.Chain->ExceptionFilter(pInfo);
switch(iResult)
{
case EXCEPTION_EXECUTE_HANDLER:
s_Maintain.Chain=s_Maintain.Chain->Prev;
longjmp(pNode->Entry,1);
case EXCEPTION_CONTINUE_EXECUTION:
return iResult;
case EXCEPTION_CONTINUE_SEARCH:
s_Maintain.Chain=s_Maintain.Chain->Prev;
break;
default:
_ASSERT(FALSE);
break;
}
}
return -1;
}
int g_iTest;
int MyFilter(PEXCEPTION_POINTERS pExceptionInfo)
{
//pExceptionInfo->ContextRecord->Rax=(UINT64)&g_iTest;
printf(" Inner exception clean handler, we cannot handle it, try outer exception handler\n");
return EXCEPTION_CONTINUE_SEARCH;
}
int MyFilter2(PEXCEPTION_POINTERS pExceptionInfo)
{
//pExceptionInfo->ContextRecord->Rax=(UINT64)&g_iTest;
printf("Outer exception clean handler, do clean work\n");
return EXCEPTION_EXECUTE_HANDLER;
}
int main(void)
{
TRY_START
{
printf("Enter outer exception monitor section\n");
TRY_START
{
printf(" Enter inner exception monitor section\n");
// 触发异常
int* ptr=NULL;
printf(" Let's do bad thing.\n");
*ptr=42;
s_Maintain.Chain=__weLees_ExceptionNode.Prev;
}
TRY_EXCEPT(MyFilter)
{
printf("WTF\n");
}
TRY_END
}
TRY_EXCEPT(MyFilter2)
{
printf("WTF2\n");
}
TRY_END
return 0;
}
至此,整个任务完成!
本文章的源代码在github中
welees/pattern_seh: Approximate Implementation of Windows SEH which works on MAC/Linux Platform