首页
社区
课程
招聘
调试实战 | 从转储文件找出抛出的异常 —— 理论
2024-1-7 18:36 6105

调试实战 | 从转储文件找出抛出的异常 —— 理论

2024-1-7 18:36
6105

缘起

最近在分析转储文件时,遇到了一个由 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.paramsThisException 的类型是 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_TYPEINFO0,则包含 3 个成员,否则就会包含第 4 个成员 pThrowImageBase

_EH_RELATIVE_TYPEINFO32 位程序中是 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 的值是 3EHExceptionRecord::EHParameters 刚好有 3 个成员,在 64 位程序中 nNumberOfArguments 的值是 4EHExceptionRecord::EHParameters 刚好有 4 个成员。

EHExceptionRecord::EHParameters 中的 pExceptionObjectpThrowInfo 是查找异常类型的关键。

其中,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

所以,pForwardCompatpCatchableTypeArray32 位程序中是地址,在 64 位程序中是偏移。

还记得 EHExceptionRecord::EHParameters64 位程序中有 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 的值为 2arrayOfCatchableTypes[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 EHExceptionRecord
TestThrowException!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::EHParameters
TestThrowException!EHExceptionRecord::EHParameters
  +0x000 magicNumber      : Uint4B
  +0x004 pExceptionObject : Ptr32 Void
  +0x008 pThrowInfo       : Ptr32 _s_ThrowInfo //<----
 
0:000> dt _s_ThrowInfo
TestThrowException!_s_ThrowInfo
  +0x000 attributes       : Uint4B
  +0x004 pmfnUnwind       : Ptr32     void
  +0x008 pForwardCompat   : Ptr32     int
  +0x00c pCatchableTypeArray : Ptr32 _s_CatchableTypeArray //<----
 
0:000> dt _s_CatchableTypeArray
TestThrowException!_s_CatchableTypeArray
  +0x000 nCatchableTypes  : Int4B
  +0x004 arrayOfCatchableTypes : [0] Ptr32 _s_CatchableType //<----
 
0:000> dt _s_CatchableType
TestThrowException!_s_CatchableType
  +0x000 properties       : Uint4B
  +0x004 pType            : Ptr32 TypeDescriptor //<----
  +0x008 thisDisplacement : PMD
  +0x014 sizeOrOffset     : Int4B
  +0x018 copyFunction     : Ptr32     void
 
0:000> dt TypeDescriptor
TestThrowException!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 EHExceptionRecord
VCRUNTIME140!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::EHParameters
VCRUNTIME140!EHExceptionRecord::EHParameters
   +0x000 magicNumber      : Uint4B
   +0x008 pExceptionObject : Ptr64 Void
   +0x010 pThrowInfo       : Ptr64 _s_ThrowInfo //<----
   +0x018 pThrowImageBase  : Ptr64 Void
 
0:000> dt _s_ThrowInfo
VCRUNTIME140!_s_ThrowInfo
   +0x000 attributes       : Uint4B
   +0x004 pmfnUnwind       : Int4B
   +0x008 pForwardCompat   : Int4B
   +0x00c pCatchableTypeArray : Int4B //<----
 
0:000> dt CatchableTypeArray
VCRUNTIME140!CatchableTypeArray
   +0x000 nCatchableTypes  : Int4B
   +0x004 arrayOfCatchableTypes : [0] Int4B //<----
 
0:000> dt CatchableType
VCRUNTIME140!CatchableType
   +0x000 properties       : Uint4B
   +0x004 pType            : Int4B //<----
   +0x008 thisDisplacement : PMD
   +0x014 sizeOrOffset     : Int4B
   +0x018 copyFunction     : Int4B
 
0:000> dt TypeDescriptor
VCRUNTIME140!TypeDescriptor
   +0x000 pVFTable         : Ptr64 Void
   +0x008 spare            : Ptr64 Void
   +0x010 name             : [0] Char //<====

划重点: 务必记住以上结构体的定义,尤其是关键字段的偏移。这是解析的依据!

解析方法小结

  1. 先找到 EHParameters 类型的对象(可省略此步)

    可以通过 RaiseException() 的第四个参数查找。

    32 位程序中,定位方法非常简单,可以直接查看 RaiseException() 的第 4 个参数,ebp+0x14

    x64 位中可以通过 _CxxThrowException()rsp + 0x28 定位。因为在调用 RaiseException() 的时候, _CxxThrowException() 会把此参数存在自己栈帧中 rsp + 0x28 的位置。

  2. 再找到 ThrowInfo 类型的对象

    解析 EHParameters 中的第 3 个成员 pThrowInfo,在 32 位程序中偏移是 0x8,在 64 位程序中偏移是 0x10

    说明: 还有两种查看方法:

    1. 对于 32 位程序可以通过 _CxxThrowException() 对应栈帧的第 2 个参数(ebp+c)直接查看。

    2. 如果有 vcruntimexxx.dll 的调试符号,可以直接切到 _CxxThrowException() 对应的栈帧,windbg 会自动帮忙列出对应的值。

  3. 再找到 CatchableTypeArray 类型的对象

    解析 ThrowInfo 的第 4 个成员 pCatchableTypeArray,其偏移是 0xc3264 位通用)。

    需要注意的是,此成员在 32 位程序中是地址;在 64 位程序中是偏移,需要加上镜像基址得到最终的地址。

  4. 再找到 CatchableType 类型的对象

    解析 CatchableTypeArray 的第 2 个成员 arrayOfCatchableTypes ,偏移是 0x43264 位通用)。

    该成员记录了 CatchableType 数组的首地址或者偏移。

    需要注意的是,此成员在 32 位程序中是地址;在 64 位程序中是偏移,需要加上镜像基址得到最终的地址。

    说明:1 个成员 nCatchableTypes 记录了 CatchableType 数组的个数。

  5. 再找到 TypeDescriptor 类型的对象

    解析 CatchableType 数组中的每个对象(其实,只需要解析第一个即可)。重点关注第 2 个成员 pType,偏移是 0x43264 位通用)。

    需要注意的是,此成员在 32 位程序中是地址;在 64 位程序中是偏移,需要加上镜像基址得到最终的地址。

  6. 最后找到异常类型名

    解析 TypeDescriptor 对象,只需要关注第 3 个成员 name 成员即可,在 32 位程序中偏移是 0x8,在 64 位程序中偏移是 0x10

    它是一个以 \0 结尾的字符串,可以在 windbgda 显示其内容。

总结

  • throw 对应的实现函数是 _CxxThrowException() 函数,该函数定义在 throw.cpp 中,可以查看源码。
  • _CxxThrowException() 内部会调用 RaiseException(),调用时传递的错误码是 0xe06d7363(对应的字符是 .msc)。
  • 【关键结构】中的结构体是解析时的依据,务必要熟悉。
  • 【解析方法小结】中总结的方法是通用方法,适用于任何情况。在实际解析过程中还可以利用虚表等其它相关信息进行解析。
  • 在解析过程中,需要注意的是在 64 位程序中,很多成员变量都是相对于发生异常模块的偏移,而不是直接可用的地址,需要先把偏移转换成虚拟地址后再使用。

参考资料

vs 源码


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
点赞6
打赏
分享
最新回复 (3)
雪    币: 19349
活跃值: (28971)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
秋狝 2024-1-7 20:18
2
1
感谢分享
雪    币: 447
活跃值: (819)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
frendguo 2024-1-10 10:11
3
0
强啊
雪    币: 3350
活跃值: (3377)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
fengyunabc 1 2024-1-20 18:09
4
0
感谢分享
游客
登录 | 注册 方可回帖
返回