一
理论篇
缘起
最近在分析转储文件时,遇到了一个由throw
抛出的异常。尽管在windbg
中使用!analyze -v
迅速知道了异常码是0xe06d7363
(对应的ASCII
码是.msc
),但是根据异常码并不能确定具体抛出来的是哪种异常。针对这种情况,确定具体的异常类型才有意义。
本篇文章会简单介绍与抛出异常相关的内容,包括关键的函数及结构体。下一篇文章会通过实例介绍几种典型情况(有调试符号 / 没有调试符号 /32
位程序 /64
位程序)下的定位方法。
说明:对源码不感兴趣的小伙伴而可以直接跳到【解析方法小结】查看结论。
突破口
throw
关键字编译后对应的函数是_CxxThrowException()
,该函数内部会通过RaiseException()
触发异常。_CxxThrowException()
是有源码可查的,我们可以从这个函数入手,先来熟悉下这个函数以及相关的结构体。
_CxxThrowException
该函数定义在vs
自带的throw.cpp
中,一般在crtsrcvcruntime
目录下。直接用everything
搜索throw.cpp
,然后打开即可。vs2019
中的实现代码如下,有删减:
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()
的原型如下:
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
。定义如下:
#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
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
,定义如下:
#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
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
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
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
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
成员是经过名字改编后的异常类型,它是一个以
评论