缘起
最近在分析转储文件时,遇到了一个由 throw 抛出的异常。尽管在 windbg 中使用 !analyze -v 迅速知道了异常码是 0xe06d7363(对应的 ASCII 码是 .msc),但是根据异常码并不能确定具体抛出来的是哪种异常。针对这种情况,确定具体的异常类型才有意义。
本篇文章会简单介绍与抛出异常相关的内容,包括关键的函数及结构体。下一篇文章会通过实例介绍几种典型情况(有调试符号 / 没有调试符号 / 32 位程序 / 64 位程序)下的定位方法。
说明: 对源码不感兴趣的小伙伴而可以直接跳到【解析方法小结】查看结论。
突破口
throw 关键字编译后对应的函数是 _CxxThrowException(),该函数内部会通过 RaiseException() 触发异常。_CxxThrowException() 是有源码可查的,我们可以从这个函数入手,先来熟悉下这个函数以及相关的结构体。
_CxxThrowException
该函数定义在 vs 自带的 throw.cpp 中,一般在 crt\src\vcruntime\ 目录下。直接用 everything 搜索 throw.cpp,然后打开即可。vs2019 中的实现代码如下,有删减:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | extern "C" __declspec(noreturn) void __stdcall_CxxThrowException( 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) nullptr, // Additional record (none) nullptr, // Address of exception (OS fills in) EH_EXCEPTION_PARAMETERS, // Number of parameters { EH_MAGIC_NUMBER1, // Our version control magic number nullptr, // pExceptionObject nullptr,#if EH_EXCEPTION_PARAMETERS == 4 nullptr // Image base of thrown object#endif } // pThrowInfo }; EHExceptionRecord ThisException = ExceptionTemplate; // This exception ThrowInfo* pTI = (ThrowInfo*)pThrowInfo; // deleted ... ThisException.params.pExceptionObject = pExceptionObject; ThisException.params.pThrowInfo = pTI;#if _EH_RELATIVE_TYPEINFO PVOID ThrowImageBase = RtlPcToFileHeader((PVOID)pTI, &ThrowImageBase); ThisException.params.pThrowImageBase = ThrowImageBase;#endif // deleted ... EHTRACE_EXIT; RaiseException( ThisException.ExceptionCode, ThisException.ExceptionFlags, ThisException.NumberParameters, (PULONG_PTR)&ThisException.params );} |
根据源码可知, _CxxThrowException() 内部会调用 RaiseException(),RaiseException() 的原型如下:
1 2 3 4 5 6 | VOID WINAPI RaiseException( _In_ DWORD dwExceptionCode, _In_ DWORD dwExceptionFlags, _In_ DWORD nNumberOfArguments, _In_reads_opt_(nNumberOfArguments) CONST ULONG_PTR* lpArguments); |
_CxxThrowException 调用 RaiseException() 时传递的各个参数值如下:
dwExceptionCode的值是EH_EXCEPTION_NUMBER,对应的十六进制值是0xe06d7363,也就是.msc。dwExceptionFlags的值是EXCEPTION_NONCONTINUABLE,对应的十六进制值是0x1。nNumberOfArguments的值是EH_EXCEPTION_PARAMETERS,在32位程序中是3,在64位程序中是4。定义如下:
1 2 3 4 5 | #if (defined(_M_AMD64) || defined(_M_ARM) || defined(_M_ARM64)) && !defined(_CHPE_X86_ARM64_EH_)#define EH_EXCEPTION_PARAMETERS 4 // Number of parameters in exception record#else#define EH_EXCEPTION_PARAMETERS 3 // Number of parameters in exception record#endif |
lpArguments指向具体的参数,来自ThisException.params。ThisException的类型是EHExceptionRecord,其定义如下:
EHExceptionRecord
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | typedef struct EHExceptionRecord { unsigned long ExceptionCode; // The code of this exception. (= EH_EXCEPTION_NUMBER) unsigned long ExceptionFlags; // Flags determined by NT struct _EXCEPTION_RECORD* ExceptionRecord; // An extra exception record (not used) void* ExceptionAddress; // Address at which exception occurred unsigned long NumberParameters; // Number of extended parameters. (= EH_EXCEPTION_PARAMETERS) struct EHParameters { unsigned long magicNumber; // = EH_MAGIC_NUMBER1 void * pExceptionObject; // Pointer to the actual object thrown ThrowInfo* pThrowInfo; // Description of thrown object#if _EH_RELATIVE_TYPEINFO void * pThrowImageBase; // Image base of thrown object#endif } params; // <-----} EHExceptionRecord; |
根据定义可知,ThisException.params 的类型是 EHExceptionRecord::EHParameters,如果 _EH_RELATIVE_TYPEINFO 为 0,则包含 3 个成员,否则就会包含第 4 个成员 pThrowImageBase。
而 _EH_RELATIVE_TYPEINFO 在 32 位程序中是 0,在 64 位程序中是 1,定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | #if defined(_M_CEE_PURE) || defined(BUILDING_C1XX_FORCEINCLUDE)#define _EH_RELATIVE_TYPEINFO 0 // <-----#define _EH_RELATIVE_FUNCINFO 0#define _RTTI_RELATIVE_TYPEINFO 0#elif defined(_CHPE_X86_ARM64_EH_)#define _EH_RELATIVE_TYPEINFO 0 // <-----#define _EH_RELATIVE_FUNCINFO 1#define _RTTI_RELATIVE_TYPEINFO 0#elif defined(_M_ARM)#define _EH_RELATIVE_TYPEINFO 1 // <-----#define _EH_RELATIVE_FUNCINFO 1#define _RTTI_RELATIVE_TYPEINFO 0#elif defined(_M_AMD64) || defined(_M_ARM64)#define _EH_RELATIVE_TYPEINFO 1 // <-----#define _EH_RELATIVE_FUNCINFO 1#define _RTTI_RELATIVE_TYPEINFO 1#else#define _EH_RELATIVE_TYPEINFO 0 // <-----#define _EH_RELATIVE_FUNCINFO 0#define _RTTI_RELATIVE_TYPEINFO 0#endif |
EHExceptionRecord::EHParameters 结构体的成员数量与调用 RaiseException() 时的 nNumberOfArguments 参数值是对应的。
在 32 位程序中,nNumberOfArguments 的值是 3,EHExceptionRecord::EHParameters 刚好有 3 个成员,在 64 位程序中 nNumberOfArguments 的值是 4,EHExceptionRecord::EHParameters 刚好有 4 个成员。
EHExceptionRecord::EHParameters 中的 pExceptionObject 和 pThrowInfo 是查找异常类型的关键。
其中,pExceptionObject 是异常对象的地址,pThrowInfo 的类型是 ThrowInfo,用来描述异常对象的类型信息。一起来看看 ThrowInfo 的定义。
ThrowInfo
1 2 3 4 5 6 7 8 9 10 11 | typedef const struct _s_ThrowInfo { unsigned int attributes; // Throw Info attributes (Bit field) PMFN pmfnUnwind; // Destructor to call when exception has been handled or aborted#if _EH_RELATIVE_TYPEINFO && !defined(BUILDING_C1XX_FORCEINCLUDE) int pForwardCompat; // Image relative offset of Forward compatibility frame handler int pCatchableTypeArray; // Image relative offset of CatchableTypeArray#else int (__cdecl * pForwardCompat)(...); // Forward compatibility frame handler CatchableTypeArray* pCatchableTypeArray; // Pointer to list of pointers to types#endif} ThrowInfo; |
pmfnUnwind是处理异常时会调用的回卷函数,一般是析构函数,可以根据此值判断异常对象的类型!pForwardCompat一般情况下都是0,不用太关心pCatchableTypeArray非常重要,记录了类型信息
_EH_RELATIVE_TYPEINFO 在上面已经贴出来了,在 32 位程序中被定义为 0,在 64 位程序中被定义为 1。
所以,pForwardCompat 和 pCatchableTypeArray 在 32 位程序中是地址,在 64 位程序中是偏移。
还记得 EHExceptionRecord::EHParameters 在 64 位程序中有 4 个成员吗?第 4 个成员就是抛出异常对应的模块基址,用这个基址加上这里的偏移就得到了对应成员在内存中的位置。一定要记住这个结论,在分析 64 位程序的异常对象类型时会用到!
接下来看看关键的 CatchableTypeArray 类型的定义,摘录如下:
CatchableTypeArray
1 2 3 4 5 6 7 8 | typedef const struct _s_CatchableTypeArray { int nCatchableTypes;#if _EH_RELATIVE_TYPEINFO int arrayOfCatchableTypes[]; // Image relative offset of Catchable Types#else CatchableType* arrayOfCatchableTypes[];#endif} CatchableTypeArray; |
nCatchableTypes记录了数组arrayOfCatchableTypes的数量。arrayOfCatchableTypes记录了异常类型信息。同样的,在32位程序中是地址,在64位程序中是偏移。
说明: 这里为什么使用数组呢?因为抛出的异常可能继承自某个基类。
arrayOfCatchableTypes会把继承链上的所有类型信息按照从子类到基类的顺序记录下来。拿std::bad_alloc举例,它继承自std::exception。所以,nCatchableTypes的值为2,arrayOfCatchableTypes[0]记录了std::bad_alloc的类型信息,arrayOfCatchableTypes[1]记录了std::exception的类型信息。
再来看看结构体 CatchableType 的定义,摘录如下:
CatchableType
1 2 3 4 5 6 7 8 9 10 11 12 | typedef const struct _s_CatchableType { unsigned int properties; // Catchable Type properties (Bit field)#if _EH_RELATIVE_TYPEINFO int pType; // Image relative offset of TypeDescriptor#else TypeDescriptor* pType; // Pointer to the type descriptor for this type#endif PMD thisDisplacement; // Pointer to instance of catch type within thrown object. int sizeOrOffset; // Size of simple-type object or offset into // buffer of 'this' pointer for catch object PMFN copyFunction; // Copy constructor or CC-closure} CatchableType; |
我们只需要关注 pType 成员即可。同样的,在 32 位程序中是地址,在 64 位程序中是偏移。 pType 对应的类型是 TypeDescriptor,接下来看看 TypeDescriptor 的定义。
TypeDescriptor
1 2 3 4 5 6 7 8 9 10 | typedef struct TypeDescriptor{#if defined(_WIN64) || defined(_RTTI) || defined(BUILDING_C1XX_FORCEINCLUDE) const void* pVFTable; // Field overloaded by RTTI#else unsigned long hash; // Hash value computed from type's decorated name#endif void* spare; // reserved, possible for RTTI char name[]; // The decorated name of the type; 0 terminated.} TypeDescriptor; |
其中,name 成员是经过名字改编后的异常类型,它是一个以 \0 结尾的字符串,可以在 windbg 中通过 da 查看。
源码有点乱,还是在 windbg 中看的直观舒服,还可以看到偏移。以下是 32 位和 64 位程序中对应的结构体定义:
关键结构
32 位关键结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | 0:000> dt EHExceptionRecordTestThrowException!EHExceptionRecord +0x000 ExceptionCode : Uint4B +0x004 ExceptionFlags : Uint4B +0x008 ExceptionRecord : Ptr32 _EXCEPTION_RECORD +0x00c ExceptionAddress : Ptr32 Void +0x010 NumberParameters : Uint4B +0x014 params : EHExceptionRecord::EHParameters //<----0:000> dt EHExceptionRecord::EHParametersTestThrowException!EHExceptionRecord::EHParameters +0x000 magicNumber : Uint4B +0x004 pExceptionObject : Ptr32 Void +0x008 pThrowInfo : Ptr32 _s_ThrowInfo //<----0:000> dt _s_ThrowInfoTestThrowException!_s_ThrowInfo +0x000 attributes : Uint4B +0x004 pmfnUnwind : Ptr32 void +0x008 pForwardCompat : Ptr32 int +0x00c pCatchableTypeArray : Ptr32 _s_CatchableTypeArray //<----0:000> dt _s_CatchableTypeArrayTestThrowException!_s_CatchableTypeArray +0x000 nCatchableTypes : Int4B +0x004 arrayOfCatchableTypes : [0] Ptr32 _s_CatchableType //<----0:000> dt _s_CatchableTypeTestThrowException!_s_CatchableType +0x000 properties : Uint4B +0x004 pType : Ptr32 TypeDescriptor //<---- +0x008 thisDisplacement : PMD +0x014 sizeOrOffset : Int4B +0x018 copyFunction : Ptr32 void0:000> dt TypeDescriptorTestThrowException!TypeDescriptor +0x000 hash : Uint4B +0x004 spare : Ptr32 Void +0x008 name : [0] Char //<==== |
64 位关键结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | 0:000> dt EHExceptionRecordVCRUNTIME140!EHExceptionRecord +0x000 ExceptionCode : Uint4B +0x004 ExceptionFlags : Uint4B +0x008 ExceptionRecord : Ptr64 _EXCEPTION_RECORD +0x010 ExceptionAddress : Ptr64 Void +0x018 NumberParameters : Uint4B +0x020 params : EHExceptionRecord::EHParameters //<----0:000> dt EHExceptionRecord::EHParametersVCRUNTIME140!EHExceptionRecord::EHParameters +0x000 magicNumber : Uint4B +0x008 pExceptionObject : Ptr64 Void +0x010 pThrowInfo : Ptr64 _s_ThrowInfo //<---- +0x018 pThrowImageBase : Ptr64 Void0:000> dt _s_ThrowInfoVCRUNTIME140!_s_ThrowInfo +0x000 attributes : Uint4B +0x004 pmfnUnwind : Int4B +0x008 pForwardCompat : Int4B +0x00c pCatchableTypeArray : Int4B //<----0:000> dt CatchableTypeArrayVCRUNTIME140!CatchableTypeArray +0x000 nCatchableTypes : Int4B +0x004 arrayOfCatchableTypes : [0] Int4B //<----0:000> dt CatchableTypeVCRUNTIME140!CatchableType +0x000 properties : Uint4B +0x004 pType : Int4B //<---- +0x008 thisDisplacement : PMD +0x014 sizeOrOffset : Int4B +0x018 copyFunction : Int4B0:000> dt TypeDescriptorVCRUNTIME140!TypeDescriptor +0x000 pVFTable : Ptr64 Void +0x008 spare : Ptr64 Void +0x010 name : [0] Char //<==== |
划重点: 务必记住以上结构体的定义,尤其是关键字段的偏移。这是解析的依据!
解析方法小结
先找到
EHParameters类型的对象(可省略此步)可以通过
RaiseException()的第四个参数查找。在
32位程序中,定位方法非常简单,可以直接查看RaiseException()的第4个参数,ebp+0x14。在
x64位中可以通过_CxxThrowException()的rsp + 0x28定位。因为在调用RaiseException()的时候,_CxxThrowException()会把此参数存在自己栈帧中rsp + 0x28的位置。再找到
ThrowInfo类型的对象解析
EHParameters中的第3个成员pThrowInfo,在32位程序中偏移是0x8,在64位程序中偏移是0x10。说明: 还有两种查看方法:
对于
32位程序可以通过_CxxThrowException()对应栈帧的第2个参数(ebp+c)直接查看。如果有
vcruntimexxx.dll的调试符号,可以直接切到_CxxThrowException()对应的栈帧,windbg会自动帮忙列出对应的值。
再找到
CatchableTypeArray类型的对象解析
ThrowInfo的第4个成员pCatchableTypeArray,其偏移是0xc(32位64位通用)。需要注意的是,此成员在
32位程序中是地址;在64位程序中是偏移,需要加上镜像基址得到最终的地址。再找到
CatchableType类型的对象解析
CatchableTypeArray的第2个成员arrayOfCatchableTypes,偏移是0x4(32位64位通用)。该成员记录了
CatchableType数组的首地址或者偏移。需要注意的是,此成员在
32位程序中是地址;在64位程序中是偏移,需要加上镜像基址得到最终的地址。说明: 第
1个成员nCatchableTypes记录了CatchableType数组的个数。再找到
TypeDescriptor类型的对象解析
CatchableType数组中的每个对象(其实,只需要解析第一个即可)。重点关注第2个成员pType,偏移是0x4(32位64位通用)。需要注意的是,此成员在
32位程序中是地址;在64位程序中是偏移,需要加上镜像基址得到最终的地址。最后找到异常类型名
解析
TypeDescriptor对象,只需要关注第3个成员name成员即可,在32位程序中偏移是0x8,在64位程序中偏移是0x10。它是一个以
\0结尾的字符串,可以在windbg用da显示其内容。
总结
throw对应的实现函数是_CxxThrowException()函数,该函数定义在throw.cpp中,可以查看源码。_CxxThrowException()内部会调用RaiseException(),调用时传递的错误码是0xe06d7363(对应的字符是.msc)。- 【关键结构】中的结构体是解析时的依据,务必要熟悉。
- 【解析方法小结】中总结的方法是通用方法,适用于任何情况。在实际解析过程中还可以利用虚表等其它相关信息进行解析。
- 在解析过程中,需要注意的是在
64位程序中,很多成员变量都是相对于发生异常模块的偏移,而不是直接可用的地址,需要先把偏移转换成虚拟地址后再使用。
参考资料
vs 源码