C++ EH Exception是Windows系统VC++里对c++语言的throw的分类和定义,它的代码就是0xe06d7363。在VC++里其本质也是SEH结构化异常机制。在我们分析用户崩溃的例子中经常会遇到它。一般情况下,遇到它,是我们代码里用throw抛出异常后没有处理导致程序崩溃。下面分析一下它的原理。
我们借助一段代码来跟踪和分析
class MyException { public: int nErr; char *szMessage; public: MyException(void) :nErr(0) , szMessage(NULL) { } MyException(int nerr,char *szMess) :nErr(nerr) , szMessage(szMess) { } ~MyException(void) { } }; int _tmain(int argc, _TCHAR* argv[]) { try { MyException me(1, "test exception"); throw me; } catch (MyException me1) { printf("err=%s\n",me1.szMessage); } }
将上述代码在VS2013里编译调试,转到汇编
91: try 92: { 01361797 mov dword ptr [ebp-4],0 93: MyException me(1, "test exception"); 0136179E push 1367858h 013617A3 push 1 013617A5 lea ecx,[ebp-1Ch] 013617A8 call MyException::MyException (013610D2h) 013617AD mov dword ptr [ebp-104h],eax 013617B3 mov byte ptr [ebp-4],1 94: throw me; 013617B7 mov eax,dword ptr [ebp-1Ch] 013617BA mov dword ptr [ebp-0FCh],eax 013617C0 mov ecx,dword ptr [ebp-18h] 013617C3 mov dword ptr [ebp-0F8h],ecx 013617C9 push 1369084h 013617CE lea edx,[ebp-0FCh] 013617D4 push edx 013617D5 call __CxxThrowException@8 (0136111Dh) 95: }
我们可以看到,throw 首先通过下面的代码
013617B7 mov eax,dword ptr [ebp-1Ch] 013617BA mov dword ptr [ebp-0FCh],eax 013617C0 mov ecx,dword ptr [ebp-18h] 013617C3 mov dword ptr [ebp-0F8h],ecx
复制了一份异常对象MyException me,然后取了这份拷贝的地址作为参数传给了__CxxThrowException函数,同时将异常类型信息也传递了过去
013617CE lea edx,[ebp-0FCh] 013617D4 push edx
接着调用了__CxxThrowException函数,我们进入看看
下面是改函数的代码
///////////////////////////////////////////////////////////////////////////// // // _CxxThrowException - implementation of 'throw' // // Description: // Builds the NT Exception record, and calls the NT runtime to initiate // exception processing. // // Why is pThrowInfo defined as _ThrowInfo? Because _ThrowInfo is secretly // snuck into the compiler, as is the prototype for _CxxThrowException, so // we have to use the same type to keep the compiler happy. // // Another result of this is that _CRTIMP can't be used here. Instead, we // synthesisze the -export directive below. // // Returns: // NEVER. (until we implement resumable exceptions, that is) // // We want double underscore for CxxThrowException for ARM CE only __declspec(noreturn) extern "C" void __stdcall #if !defined(_M_ARM) || defined(_M_ARM_NT) _CxxThrowException( #else __CxxThrowException( #endif void* pExceptionObject, // The object thrown _ThrowInfo* pThrowInfo // Everything we need to know about it ) { EHTRACE_ENTER_FMT1("Throwing object @ 0x%p", pExceptionObject); static const EHExceptionRecord ExceptionTemplate = { // A generic exception record EH_EXCEPTION_NUMBER, // Exception number EXCEPTION_NONCONTINUABLE, // Exception flags (we don't do resume) NULL, // Additional record (none) NULL, // Address of exception (OS fills in) EH_EXCEPTION_PARAMETERS, // Number of parameters { EH_MAGIC_NUMBER1, // Our version control magic number NULL, // pExceptionObject NULL, #if _EH_RELATIVE_OFFSETS NULL // Image base of thrown object #endif } // pThrowInfo }; EHExceptionRecord ThisException = ExceptionTemplate; // This exception ThrowInfo* pTI = (ThrowInfo*)pThrowInfo; if (pTI && (THROW_ISWINRT( (*pTI) ) ) ) { ULONG_PTR *exceptionInfoPointer = *reinterpret_cast<ULONG_PTR**>(pExceptionObject); exceptionInfoPointer--; // The pointer to the ExceptionInfo structure is stored sizeof(void*) infront of each WinRT Exception Info. WINRTEXCEPTIONINFO** ppWei = reinterpret_cast<WINRTEXCEPTIONINFO**>(exceptionInfoPointer); pTI = (*ppWei)->throwInfo; (*ppWei)->PrepareThrow( ppWei ); } // // Fill in the blanks: // ThisException.params.pExceptionObject = pExceptionObject; ThisException.params.pThrowInfo = pTI; #if _EH_RELATIVE_OFFSETS PVOID ThrowImageBase = RtlPcToFileHeader((PVOID)pTI, &ThrowImageBase); ThisException.params.pThrowImageBase = ThrowImageBase; #endif // // If the throw info indicates this throw is from a pure region, // set the magic number to the Pure one, so only a pure-region // catch will see it. // // Also use the Pure magic number on Win64 if we were unable to // determine an image base, since that was the old way to determine // a pure throw, before the TI_IsPure bit was added to the FuncInfo // attributes field. // if (pTI != NULL) { if (THROW_ISPURE(*pTI)) { ThisException.params.magicNumber = EH_PURE_MAGIC_NUMBER1; } #if _EH_RELATIVE_OFFSETS else if (ThrowImageBase == NULL) { ThisException.params.magicNumber = EH_PURE_MAGIC_NUMBER1; } #endif } // // Hand it off to the OS: // EHTRACE_EXIT; #if defined(_M_X64) && defined(_NTSUBSET_) RtlRaiseException( (PEXCEPTION_RECORD) &ThisException ); #else RaiseException( ThisException.ExceptionCode, ThisException.ExceptionFlags, ThisException.NumberParameters, (PULONG_PTR)&ThisException.params ); #endif }
可以看到,这个函数首先是创建了一个EHExceptionRecord 对象,其实对应的就是 SEH里的结构EXCEPTION_RECORD,并且给这个结构成员赋值。
在这里通过如下代码,0xe06d7363就赋值给ThisException.ExceptionCode
还有就是将ThrowInfo赋值给EHExceptionRecordThisException.params.pThrowInfo。_ThrowInfo 结构体定义如下:
typedef const struct _s__ThrowInfo { unsigned int attributes; _PMFN pmfnUnwind; int (__cdecl*pForwardCompat)(...); _CatchableTypeArray *pCatachableTypeArray; } _ThrowInfo;
结构体中重要的成员是_CatchableTypeArray。它包含了程序运行时抛出对象的类新信息(RTTI).
如果你的程序运行时抛出一个my_exception类型的对象,那么抛出的数据参数pCatchableTypeArray包含了两个重要子数据信息。一个是typeid(my_exception),另外一个是typeid(std::exception)。
在我们的例子里
紧接着就调用RaiseException函数进入了异常的分发过程。
综上,在C++ EH Exception 的异常里,EXCEPTION_RECORD结构填充如下:
ExceptionAddress: 异常地址 ExceptionCode: 异常代码 e06d7363 (C++ EH exception) ExceptionFlags: 标志 00000001 NumberParameters: 3 or 4 64位时是4 Parameter[0]: 0000000019930520 //魔数 Parameter[1]: 00000000015def30 // 抛出的异常对象指针 Parameter[2]: 00000000100cefa8 // 异常对象类型信息 Parameter[3]: 0000000010000000 // 64位下模块句柄HINSTANCE