kr 第三阶段(六)C++ 逆向

成员函数

普通成员函数

调用约定是 __thiscall,即 ecx 寄存器存 this 指针,内平栈。

    a.f(1, 2, 3);
008A1050  push        3  
008A1052  push        2  
008A1054  push        1  
008A1056  lea         ecx,[a]  
008A1059  call        Struct::f (08A1010h)  

在不开优化的前提下如果使用 __stdcall 修饰成员函数则 this 指针作为第一个参数栈传参。

    a.f(1, 2, 3);
000C1050  push        3  
000C1052  push        2  
000C1054  push        1  
000C1056  lea         eax,[a]  
000C1059  push        eax  
000C105A  call        Struct::f (0C1010h) 

同理,使用 __fastcall__cdecl 修饰成员函数后成员函数满足对应的调用约定,只不过 this 指针当做函数的第一个参数。

构造函数

栈对象

栈对象即 Class 在栈上实例化出的 Object 。

栈对象的构造函数是对应类的作用域中第一个被调用的成员函数,调用约定是 __thiscall

    Class a = Class();
007C1128  lea         ecx,[a]  
007C112B  call        Class::Class (07C10C0h)

构造函数的返回值是 this 指针。

    Class() {
007C10C0  push        ebp  
007C10C1  mov         ebp,esp  
007C10C3  push        ecx  
007C10C4  mov         dword ptr [this],ecx  
        puts("Construct");
007C10C7  push        offset string "Construct" (07C2120h)  
007C10CC  call        dword ptr [__imp__puts (07C20BCh)]  
007C10D2  add         esp,4  
    }
007C10D5  mov         eax,dword ptr [this]		; 返回 this 指针  
007C10D8  mov         esp,ebp  
007C10DA  pop         ebp  
007C10DB  ret  

因此构造函数反编译代码如下:

Class *__thiscall Class::Class(Class *this)
{
  _puts("Construct");
  return this;
}

堆对象

堆对象即 Class 在堆上实例化出的 Object 。

由于堆对象没有作用域概念,因此构造函数是在 new 之后(编译器自动添加)调用的,同理析构函数是在 delete 前调用的。

    Class* a=new Class();
00EC1065  push        30h  
00EC1067  call        operator new (0EC111Ch)		; new 一块 0x30 大小的内存
00EC106C  add         esp,4  
00EC106F  mov         dword ptr [ebp-10h],eax		;new 到的指针给一个临时变量 temp  
00EC1072  mov         dword ptr [ebp-4],0			; [ebp-4] 是 TryLevel,实际上调用构造函数的代码外面包着一层异常处理。  
00EC1079  cmp         dword ptr [ebp-10h],0			; 判断内存是否分配成功,如果分配失败则跳过构造函数。  
00EC107D  je          main+4Ch (0EC108Ch)  
00EC107F  mov         ecx,dword ptr [ebp-10h]		; 取出 [ebp-10h] 存放的 Object 地址作为 this 指针 temp   
00EC1082  call        Class::Class (0EC1000h)		; 调用构造函数    
00EC1087  mov         dword ptr [ebp-14h],eax  
00EC108A  jmp         main+53h (0EC1093h)  
00EC108C  mov         dword ptr [ebp-14h],0			; 如果 new 分配内存失败会将存放构造函数返回值的栈上局部变量置 0 表示没有调用构造函数  
00EC1093  mov         eax,dword ptr [ebp-14h]  
00EC1096  mov         dword ptr [ebp-1Ch],eax  
00EC1099  mov         dword ptr [ebp-4],0FFFFFFFFh	; TryLevel 置为 -1 表示已经不在 try...catch... 范围了。  
00EC10A0  mov         ecx,dword ptr [ebp-1Ch]  
00EC10A3  mov         dword ptr [a],ecx				; [ebp-14h] -> eax -> [ebp-1Ch] -> ecx -> [a]

不考虑异常处理可反编译成如下 C++ 代码:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  Class *a; // [esp+14h] [ebp-14h]
  Class *temp; // [esp+18h] [ebp-10h]

  temp = (Class *)operator new(0x30u);
  if ( temp )
    a = Class::Class(temp);
  else
    a = 0;
  if ( a )
    Class::`scalar deleting destructor'(a, 1u);
  return 0;
}

全局对象(静态对象)

以动态链接程序为例,在程序启动后有如下调用链:mainCRTStartup -> _scrt_common_main_seh

_scrt_common_main_seh 函数中有如 _initterm_e_initterm 函数,这个两个函数分别是 C 和 C++ 的初始化函数。以 _initterm 函数为例,这个函数会依次调用 __xc_a__xc_z 之间的函数指针。

    __scrt_current_native_startup_state = initializing;
    if ( _initterm_e(__xi_a, __xi_z) )
      return 255;
    _initterm(__xc_a, __xc_z);
    __scrt_current_native_startup_state = initialized;

我们看到在这些函数指针中有一个 _dynamic_initializer_for__a__ 函数,这个函数主要做了两件事:

  • 调用全局对象的构造函数
  • 调用 _atexit 注册 _dynamic_atexit_destructor_for__a__ 函数以便在程序结束的时候调用该函数析构全局对象。
; int dynamic_initializer_for__a__()
_dynamic_initializer_for__a__ proc near
push    ebp
mov     ebp, esp
mov     ecx, offset ?a@@3VClass@@A ; this
call    ??0Class@@QAE@XZ ; Class::Class(void)
push    offset _dynamic_atexit_destructor_for__a__ ; function
call    _atexit
add     esp, 4
pop     ebp
retn
_dynamic_initializer_for__a__ endp

反编译代码如下:

int dynamic_initializer_for__a__()
{
  Class::Class(&a);
  return atexit(dynamic_atexit_destructor_for__a__);
}

对象数组

如果定义一个栈上对象数组则程序会依次调用数组中每个对象的构造函数,而 Visual C++ 编译器会将这一过程定义为一个函数 eh vector constructor iterator

push    offset ??1Class@@QAE@XZ ; destructor
push    offset ??0Class@@QAE@XZ ; constructor
push    0Ah             ; count
push    30h ; '0'       ; size
lea     eax, [ebp+a]
push    eax             ; ptr
call    ??_L@YGXPAXIIP6EX0@Z1@Z ; `eh vector constructor iterator'(void *,uint,uint,void (*)(void *),void (*)(void *))

反编译代码如下:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  Class a[10]; // [esp+4h] [ebp-1E4h] BYREF

  `eh vector constructor iterator'(
    a,
    0x30u,
    0xAu,
    (void (__thiscall *)(void *))Class::Class,
    (void (__thiscall *)(void *))Class::~Class);
  `eh vector destructor iterator'(a, 0x30u, 0xAu, (void (__thiscall *)(void *))Class::~Class);
  return 0;
}

eh vector constructor iterator 的参数分别是:

  • 对象数组的首地址
  • 对象的大小
  • 对象的数量
  • 构造函数地址
  • 析构函数地址

该函数的会依次为每个对象调用构造函数。

void __stdcall `eh vector constructor iterator'(
        char *ptr,
        unsigned int size,
        unsigned int count,
        void (__thiscall *constructor)(void *),
        void (__thiscall *destructor)(void *))
{
  int i; // ebx

  for ( i = 0; i != count; ++i )
  {
    constructor(ptr);
    ptr += size;
  }
}

从 IDA 反编译结果来看 destructor 函数指针没有用到,但实际上这里有一个异常处理,即构造出现异常时会调用 __ArrayUnwind 函数将已初始化的对象析构。

.text:004011C3 ; void __stdcall `eh vector constructor iterator'(char *ptr, unsigned int size, unsigned int count, void (__thiscall *constructor)(void *), void (__thiscall *destructor)(void *))
.text:004011C3 ??_L@YGXPAXIIP6EX0@Z1@Z proc near       ; CODE XREF: _main+28↑p
.text:004011C3
.text:004011C3 i= dword ptr -20h
.text:004011C3 success= byte ptr -19h
.text:004011C3 ms_exc= CPPEH_RECORD ptr -18h
.text:004011C3 ptr= dword ptr  8
.text:004011C3 size= dword ptr  0Ch
.text:004011C3 count= dword ptr  10h
.text:004011C3 constructor= dword ptr  14h
.text:004011C3 destructor= dword ptr  18h
.text:004011C3
.text:004011C3 ; __unwind { // __SEH_prolog4
.text:004011C3 push    10h
.text:004011C5 push    offset ScopeTable
.text:004011CA call    __SEH_prolog4
.text:004011CA
.text:004011CF xor     ebx, ebx
.text:004011D1 mov     [ebp+i], ebx
.text:004011D4 mov     [ebp+success], bl               ; 初始化局部变量 success 为 false
.text:004011D7 ;   __try { // __finally(HandlerFunc)
.text:004011D7 mov     [ebp+ms_exc.registration.TryLevel], ebx
.text:004011D7
.text:004011DA
.text:004011DA LOOP:                                   ; CODE XREF: `eh vector constructor iterator'(void *,uint,uint,void (*)(void *),void (*)(void *))+35↓j
.text:004011DA cmp     ebx, [ebp+count]
.text:004011DD jz      short SUCCESS
.text:004011DD
.text:004011DF mov     ecx, [ebp+constructor]          ; Target
.text:004011E2 call    ds:___guard_check_icall_fptr    ; _guard_check_icall_nop(x)
.text:004011E2
.text:004011E8 mov     ecx, [ebp+ptr]                  ; void *
.text:004011EB call    [ebp+constructor]
.text:004011EB
.text:004011EE mov     eax, [ebp+size]
.text:004011F1 add     [ebp+ptr], eax
.text:004011F4 inc     ebx
.text:004011F5 mov     [ebp+i], ebx
.text:004011F8 jmp     short LOOP
.text:004011F8
.text:004011FA ; ---------------------------------------------------------------------------
.text:004011FA
.text:004011FA SUCCESS:                                ; CODE XREF: `eh vector constructor iterator'(void *,uint,uint,void (*)(void *),void (*)(void *))+1A↑j
.text:004011FA mov     al, 1                           ; 更新 al 寄存器和局部变量 success 为 true
.text:004011FC mov     [ebp+success], al
.text:004011FC ;   } // starts at 4011D7
.text:004011FF mov     [ebp+ms_exc.registration.TryLevel], 0FFFFFFFEh
.text:00401206 call    HandleIfFailure                 ; 调用
.text:00401206
.text:0040120B ; ---------------------------------------------------------------------------
.text:0040120B
.text:0040120B END:                                    ; CODE XREF: `eh vector constructor iterator'(void *,uint,uint,void (*)(void *),void (*)(void *)):RETN↓j
.text:0040120B mov     ecx, [ebp+ms_exc.registration.Next]
.text:0040120E mov     large fs:0, ecx
.text:00401215 pop     ecx
.text:00401216 pop     edi
.text:00401217 pop     esi
.text:00401218 pop     ebx
.text:00401219 leave
.text:0040121A retn    14h
.text:0040121A
.text:0040121D ; ---------------------------------------------------------------------------
.text:0040121D
.text:0040121D HandlerFunc:                            ; DATA XREF: .rdata:ScopeTable↓o
.text:0040121D ;   __finally // owned by 4011D7        ; 设置 ebp 为 i,这是需要调用析构函数的对象的数量。
.text:0040121D mov     ebx, [ebp+i]
.text:00401220 mov     al, [ebp+success]               ; 设置 al 为 局部变量 success 即 false
.text:00401220
.text:00401223
.text:00401223 HandleIfFailure:                        ; CODE XREF: `eh vector constructor iterator'(void *,uint,uint,void (*)(void *),void (*)(void *))+43↑j
.text:00401223 test    al, al
.text:00401225 jnz     short RETN                      ; 如果 al 为 true 则直接返回
.text:00401225
.text:00401227 push    [ebp+destructor]                ; destructor
.text:0040122A push    ebx                             ; count
.text:0040122B push    [ebp+size]                      ; size
.text:0040122E push    [ebp+ptr]                       ; ptr
.text:00401231 call    ?__ArrayUnwind@@YGXPAXIIP6EX0@Z@Z ; 否则调用 __ArrayUnwind 函数将已初始化的对象析构
.text:00401231
.text:00401236
.text:00401236 RETN:                                   ; CODE XREF: `eh vector constructor iterator'(void *,uint,uint,void (*)(void *),void (*)(void *))+62↑j
.text:00401236 retn
.text:00401236 ; } // starts at 4011C3
.text:00401236
.text:00401236 ??_L@YGXPAXIIP6EX0@Z1@Z endp

对于堆上对象数组也是调用 eh vector constructor iterator 函数构造,大致逻辑如下,因为是数组,所以申请的内存的前 4 字节用来记录数组中成员的个数。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  Class *v4; // [esp+14h] [ebp-14h]
  _DWORD *block; // [esp+18h] [ebp-10h]

  block = operator new[](0x1E4u);
  if ( block )
  {
    *block = 10;
    `eh vector constructor iterator'(
      block + 1,
      0x30u,
      0xAu,
      (void (__thiscall *)(void *))Class::Class,
      (void (__thiscall *)(void *))Class::~Class);
    v4 = (Class *)(block + 1);
  }
  else
  {
    v4 = 0;
  }
  if ( v4 )
    Class::`vector deleting destructor'(v4, 3u);
  return 0;
}

对于全局对象数组,则是在 dynamic_initializer_for__a__ 函数中调用 eh vector constructor iterator 函数。

int dynamic_initializer_for__a__()
{
  `eh vector constructor iterator'(
    a,
    0x30u,
    0xAu,
    (void (__thiscall *)(void *))Class::Class,
    (void (__thiscall *)(void *))Class::~Class);
  return atexit(dynamic_atexit_destructor_for__a__);
}

析构函数

栈对象

栈对象有如下特点:

  • 对应类的作用域中最后一个被调用的成员函数。(通常用来判断类的作用于结束位置)
  • 只有一个参数 this 指针,且用 ecx 传参。(实际上也是 __thiscall 调用约定)
  • 析构函数没有返回值。(或者说返回值为 0)
  • 如果没有重载析构函数那么一般析构函数都会被优化掉。

因此声明一个局部变量对象的反编译代码如下:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  Class a; // [esp+4h] [ebp-34h] BYREF

  Class::Class(&a);
  Class::~Class(&a);
  return 0;
}

显式调用析构函数不会直接调用类的析构函数而是调用析构代理函数 scalar deleting destructor 。这个函数会根据参数是否为 1 决定是否调用 delete 函数释放 Object 。由于是栈上的对象不需要释放,因此传入的参数为 0 。

    a.~Class(); // Class::`scalar deleting destructor'(&a, 0);
007C1147  push        0  
007C1149  lea         ecx,[a]  
007C114C  call        Class::`scalar deleting destructor' (07C1190h)  
    return 0;
007C1151  mov         dword ptr [ebp-44h],0  
007C1158  mov         dword ptr [ebp-4],0FFFFFFFFh  
007C115F  lea         ecx,[a]  
007C1162  call        Class::~Class (07C10E0h)  
007C1167  mov         eax,dword ptr [ebp-44h]  

在析构函数外面包裹的一层析构代理函数 scalar deleting destructor 会根据传入的参数是否为 1 决定是否调用 delete 函数释放对象,不过真正的析构函数一定会被调用。

ConsoleApplication2.exe!Class::`scalar deleting destructor'(unsigned int):
008211C0  push        ebp  
008211C1  mov         ebp,esp  
008211C3  push        ecx  
008211C4  mov         dword ptr [this],ecx  
008211C7  mov         ecx,dword ptr [this]  
008211CA  call        Class::~Class (08210E0h)								; 调用 Object 真正的析构函数,同样也是 thiscall
008211CF  mov         eax,dword ptr [ebp+8]									; 获取析构函数传入的参数
008211D2  and         eax,1  
008211D5  je          Class::`scalar deleting destructor'+25h (08211E5h)	; 如果传入的参数为 0 则为显式调用析构函数,因此直接跳过 delete008211D7  push        30h  
008211D9  mov         ecx,dword ptr [this]  
008211DC  push        ecx  
008211DD  call        operator delete (082122Ch)							; 调用 delete 函数释放 Object,采用 thiscall 调用约定。
008211E2  add         esp,8  
008211E5  mov         eax,dword ptr [this]									; 返回值为 this 指针。  
008211E8  mov         esp,ebp  
008211EA  pop         ebp  
008211EB  ret         4  

该函数反编译代码如下:

Class *__thiscall Class::`scalar deleting destructor'(Class *this, bool need_free)
{
  Class::~Class(this);
  if ( need_free )
    operator delete(this, 0x30u);
  return this;
}

堆对象

由于析构函数可以被显式调用,因此析构函数还会在栈上传一个参数。如果是显式调用则会传一个 1 ,否则传一个 0 。

例如这段代码:

int main() {
    Class* a=new Class();
    a->~Class();
    delete a;
    return 0;
}

对应汇编分析如下:

    a->~Class();
002F10A6  push        0													; 参数为 0 表示显式调用析构函数。  
002F10A8  mov         ecx,dword ptr [a]									; this 指针  
002F10AB  call        Class::`scalar deleting destructor' (02F10F0h)	; 调用析构代理函数  
    delete a;
002F10B0  mov         edx,dword ptr [a]  
002F10B3  mov         dword ptr [ebp-1Ch],edx  
002F10B6  cmp         dword ptr [ebp-1Ch],0								; 判断 this 指针是否为空,如果为空则跳过析构和 delete002F10BA  je          main+8Bh (02F10CBh)  
002F10BC  push        1													; 参数为 1 表示隐式调用析构函数。  
002F10BE  mov         ecx,dword ptr [ebp-1Ch]							; this 指针  
002F10C1  call        Class::`scalar deleting destructor' (02F10F0h)	; 调用析构代理函数  
002F10C6  mov         dword ptr [ebp-24h],eax							; 析构函数返回值保存在 [ebp-24h]002F10C9  jmp         main+92h (02F10D2h)								; 直接跳转到函数返回  
002F10CB  mov         dword ptr [ebp-24h],0								; 因为 this 指针为空,因此将析构函数执行结果置为 0return 0;
002F10D2  xor         eax,eax  

因此反编译代码如下:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  Class *a; // [esp+14h] [ebp-14h]
  Class *temp; // [esp+18h] [ebp-10h]

  temp = (Class *)operator new(0x30u);
  if ( temp )
    a = Class::Class(temp);
  else
    a = 0;
  Class::`scalar deleting destructor'(a, 0);
  if ( a )
    Class::`scalar deleting destructor'(a, 1u);
  return 0;
}

如果没有重写类的析构函数,那么编译器会优化掉所有显式调用析构函数的代码,而隐式调用析构函数会被优化成直接调用 delete 函数释放 Object 。

    a->~Class();
    delete a;
001B1156  mov         eax,dword ptr [a]  
001B1159  mov         dword ptr [ebp-1Ch],eax  
001B115C  push        30h  
001B115E  mov         ecx,dword ptr [ebp-1Ch]  
001B1161  push        ecx  
001B1162  call        operator delete (01B11D5h)  
001B1167  add         esp,8  
001B116A  cmp         dword ptr [ebp-1Ch],0  
001B116E  jne         main+99h (01B1179h)  
001B1170  mov         dword ptr [ebp-24h],0  
001B1177  jmp         main+0A6h (01B1186h)  
001B1179  mov         dword ptr [a],8123h  
001B1180  mov         edx,dword ptr [a]  
001B1183  mov         dword ptr [ebp-24h],edx  

反编译代码如下:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  Class *a; 
  Class *temp; 

  temp = (Class *)operator new(0x30u);
  if ( temp )
    a = Class::Class(temp);
  else
    a = 0;
  operator delete(a, 0x30u);
  return 0;
}

全局对象(静态对象)

dynamic_initializer_for__a__ 函数调用 _atexit 注册的 _dynamic_atexit_destructor_for__a__ 函数会直接调用析构函数。

.text:00401DB0                               ; void __cdecl dynamic_atexit_destructor_for__a__()
.text:00401DB0                               _dynamic_atexit_destructor_for__a__ proc near
.text:00401DB0                                                                       ; DATA XREF: _dynamic_initializer_for__a__+D↑o
.text:00401DB0 55                            push    ebp
.text:00401DB1 8B EC                         mov     ebp, esp
.text:00401DB3 B9 78 33 40 00                mov     ecx, offset ?a@@3VClass@@A      ; this
.text:00401DB8 E8 33 F3 FF FF                call    ??1Class@@QAE@XZ                ; Class::~Class(void)
.text:00401DB8
.text:00401DBD 5D                            pop     ebp
.text:00401DBE C3                            retn

反编译代码如下:

void __cdecl dynamic_atexit_destructor_for__a__()
{
  Class::~Class(&a);
}

dynamic_initializer_for__a__ 之所以调用 _atexit 不直接注册析构函数是因为析构函数需要传入 this 指针,即全局对象地址,而 _atexit 注册的函数不能有参数。

对象数组

与构造相似,如果一个栈上对象数组作用域结束则程序会依次调用数组中每个对象的析构函数,而 Visual C++ 编译器会将这一过程定义为一个函数 eh vector destructor iterator

push    offset ??1Class@@QAE@XZ ; destructor	; `eh vector destructor iterator'(a, 0x30u, 0xAu, (void (__thiscall *)(void *))Class::~Class);
push    0Ah             ; count
push    30h ; '0'       ; size
lea     ecx, [ebp+a]
push    ecx             ; ptr
call    ??_M@YGXPAXIIP6EX0@Z@Z ; `eh vector destructor iterator'(void *,uint,uint,void (*)(void *))

该函数的参数分别是:

  • 数组首地址
  • 对象大小
  • 对象数量
  • 析构函数地址

eh vector destructor iterator 函数会依次为对象数组中的每个对象调用析构函数。异常处理过程就不具体分析了。

void __stdcall `eh vector destructor iterator'(
        char *ptr,
        unsigned int size,
        unsigned int count,
        void (__thiscall *destructor)(void *))
{
  unsigned int v4; // edi
  char *i; // esi

  v4 = count;
  for ( i = &ptr[count * size]; v4--; destructor(i) )
    i -= size;
}

对于全局对象数组则是在 dynamic_atexit_destructor_for__a__ 函数中调用 eh vector destructor iterator 函数。

void __cdecl dynamic_atexit_destructor_for__a__()
{
  `eh vector destructor iterator'(a, 0x30u, 0xAu, (void (__thiscall *)(void *))Class::~Class);
}

而对于堆上对象数组如果指向该数组的指针不为空则会调用 vector deleting destructor 函数。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  Class *v4; // [esp+14h] [ebp-14h]
  _DWORD *block; // [esp+18h] [ebp-10h]

  block = operator new[](0x1E4u);
  if ( block )
  {
    *block = 10;
    `eh vector constructor iterator'(
      block + 1,
      0x30u,
      0xAu,
      (void (__thiscall *)(void *))Class::Class,
      (void (__thiscall *)(void *))Class::~Class);
    v4 = (Class *)(block + 1);
  }
  else
  {
    v4 = 0;
  }
  if ( v4 )
    Class::`vector deleting destructor'(v4, 3u);
  return 0;
}

该函数会先调用 eh vector destructor iterator 析构对象数组中的每个成员,之后调用 delete 函数释放内存。

Class *__thiscall Class::`vector deleting destructor'(Class *this, char a2)
{
  if ( (a2 & 2) != 0 )
  {
    `eh vector destructor iterator'(this, 0x30u, this[-1].z[9], (void (__thiscall *)(void *))Class::~Class);
    if ( (a2 & 1) != 0 )
      operator delete[](&this[-1].z[9], 48 * this[-1].z[9] + 4);
    return (Class *)((char *)this - 4);
  }
  else
  {
    Class::~Class(this);
    if ( (a2 & 1) != 0 )
      operator delete(this, 0x30u);
    return this;
  }
}

对象的传递

对象作为参数

指针\引用对象传参

无论是指针还是引用传参,都是直接把对象地址作为参数传入。

    foo(a);
00B71117  lea         eax,[a]  
00B7111A  push        eax  
00B7111B  call        foo (0B710C0h)  
00B71120  add         esp,4 

对象传参

浅拷贝

如果类里面没有实现拷贝构造,那么直接将对象作为参数传递就是浅拷贝。

浅拷贝和结构体传参类似,都是把整个对象复制到参数位置。

    foo(a);
00111119  sub         esp,30h  
0011111C  mov         ecx,0Ch  
00111121  lea         esi,[a]  
00111124  mov         edi,esp  
00111126  rep movs    dword ptr es:[edi],dword ptr [esi]  
00111128  call        foo (01110C0h)  
0011112D  add         esp,30h

对象传参但时是在调用函数中会将传入的对象进行析构。

void foo(Class a) {
001110C0  push        ebp  
001110C1  mov         ebp,esp  
    printf("%d", a.x);
001110C3  mov         eax,dword ptr [a]  
001110C6  push        eax  
001110C7  push        112120h  
001110CC  call        printf (0111040h)  
001110D1  add         esp,8  
}
001110D4  lea         ecx,[a]  
001110D7  call        Class::~Class (01110A0h)  
001110DC  pop         ebp  
001110DD  ret 
深拷贝

如果对象中存在些指针指向申请的内存那么浅拷贝会将这些指针复制一份,而在函数内部析构的时候会调用析构函数将这些内存释放。如果调用完函数之后再使用对象内的这些指针指向的内存就会造成 UAF 。

因此这里需要实现拷贝构造函数 Class(const Class& a) 。这里参数 a 必须是 const 类型,否则无法传参 const 类型的对象。

#include <iostream>

class Class {
public:
    int x, y, z[10];
    char *pMem;

    Class() {
        pMem = new char[0x100];
    }

    Class(const Class& a) {
        pMem = new char[0x100];
        memcpy(pMem, a.pMem, 0x100);
        x = a.x;
        y = a.y;
        for (int i = 0; i < 10; i++) {
            z[i] = a.z[i];
        }
    }

    ~Class() {
        delete[] pMem;
    }
};

void foo(Class a) {
    printf("%d", a.x);
}

int main() {
    const Class a;
    foo(a);
    return 0;
}

此时我们看到再调用 foo 函数前首先会在栈上开辟一块对象所需的内存空间,之后会调用拷贝构造函数,最后调用 foo 函数。由于拷贝构造函数的调用约定 __thiscall 是内平栈,因此调用完拷贝构造函数之后栈顶就是已经完成初始化的对象参数,作为下一步调用的 foo 函数的参数。

    foo(a);
00D01241  sub         esp,34h  
00D01244  mov         ecx,esp  					; 传入拷贝构造函数的 this 指针指向栈上开辟出的一块对象所需的内存空间
00D01246  mov         dword ptr [ebp-4Ch],esp  
00D01249  lea         eax,[a]  
00D0124C  push        eax						; 传入拷贝构造函数的局部变量 a 的地址  
00D0124D  call        Class::Class (0D010F0h)  
00D01252  call        foo (0D011E0h)  
00D01257  add         esp,34h  

拷贝构造函数本质上就是前面浅拷贝拷贝对象内存的操作交由用户去实现。

void __thiscall Class::Class(Class *this, const Class *a)
{
  int i; // [esp+Ch] [ebp-8h]

  this->pMem = (char *)operator new[](0x100u);
  qmemcpy(this->pMem, a->pMem, 0x100u);
  this->x = a->x;
  this->y = a->y;
  for ( i = 0; i < 10; ++i )
    this->z[i] = a->z[i];
}

同样在 foo 函数中会析构传入的对象。

void __cdecl foo(Class a)
{
  printf("%d", a.x);
  Class::~Class(&a);
}

对象作为返回值

指针对象

直接返回对象的地址。

int main() {
007E1120  push        ebp  
007E1121  mov         ebp,esp  
007E1123  push        ecx  
 
    Class* b = foo();
007E1124  call        foo (07E1060h)  
007E1129  mov         dword ptr [b],eax  

    return 0;
007E112C  xor         eax,eax  
}
007E112E  mov         esp,ebp  
007E1130  pop         ebp  
007E1131  ret  

不过如果取返回的对象指针的值赋值给局部变量会形成对象拷贝。

int main() {
    Class b = *foo();
    return 0;
}

int __cdecl main()
{
  const Class *v0; // eax
  Class b; // [esp+50h] [ebp-38h] BYREF

  __CheckForDebuggerJustMyCode(&1C2F97D9_ConsoleApplication2_cpp);
  v0 = foo();
  Class::Class(&b, v0);
  Class::~Class(&b);
  return 0;
}

临时对象

例如下面这段代码:

Class foo() {
    Class a;
    return a;
}

int main() {
    foo();
    return 0;
}

实际上是向 foo 函数传递一个 main 函数的局部变量地址,然后再 foo 函数内部构造,在 main 函数析构。这里的 __autoclassinit2 实际上是将对象初始化为全 0 。

在一些版本的编译器中 foo 函数可能会实现为构造一个局部变量 a 然后浅拷贝到函数外部的临时对象,但依旧满足函数内构造,函数外析构的原则。

Class *__cdecl foo(Class *result)
{
  Class::__autoclassinit2(result, 0x34u);
  Class::Class(result);
  return result;
}

int __cdecl main(int argc, const char **argv, const char **envp)
{
  Class result; // [esp+0h] [ebp-34h] BYREF

  foo(&result);
  Class::~Class(&result);
  return 0;
}

如果是使用一个局部变量保存返回的对象:

int main() {
    Class b = foo();
    puts("main end");
    return 0;
}

那么该局部变量析构的时间由局部变量作用域决定。

int __cdecl main()
{
  Class b; // [esp+50h] [ebp-38h] BYREF

  __CheckForDebuggerJustMyCode(&1C2F97D9_ConsoleApplication2_cpp);
  foo(&b);
  _puts("main end");
  Class::~Class(&b);
  return 0;
}

引用对象

例如下面这段代码:

Class &foo() {
    static Class a;
    return a;
}

int main() {
    Class &b = foo();
    return 0;
}

本质还是返回对象的指针。

Class *__cdecl foo()
{
  __CheckForDebuggerJustMyCode(&1C2F97D9_ConsoleApplication2_cpp);
  if ( _TSS0 > *(_DWORD *)(*((_DWORD *)NtCurrentTeb()->ThreadLocalStoragePointer + _tls_index) + 260) )
  {
    j___Init_thread_header(&_TSS0);
    if ( _TSS0 == -1 )
    {
      Class::Class(&a);
      j__atexit(foo_::_2_::_dynamic_atexit_destructor_for__a__);
      j___Init_thread_footer(&_TSS0);
    }
  }
  return &a;
}

    Class &b = foo();
00C91973  call        foo (0C913F2h)  
00C91978  mov         dword ptr [b],eax  

但是如果我们让 b 不在是引用:

int main() {
    Class b = foo();
    return 0;
}

那么会存在一个拷贝构造:

int __cdecl main()
{
  const Class *v0; // eax
  Class b; // [esp+50h] [ebp-38h] BYREF

  __CheckForDebuggerJustMyCode(&1C2F97D9_ConsoleApplication2_cpp);
  v0 = foo();
  Class::Class(&b, v0);
  Class::~Class(&b);
  return 0;
}

无名对象

无名对象就是不用变量存对象,而是直接返回:

Class foo() {
    return Class();
}

int main() {
    Class a = foo();
    return 0;
}

这种本质和返回临时对象一样:

Class *__cdecl foo(Class *result)
{
  __CheckForDebuggerJustMyCode(&1C2F97D9_ConsoleApplication2_cpp);
  Class::Class(result);
  return result;
}

int __cdecl main()
{
  Class a; // [esp+50h] [ebp-38h] BYREF

  __CheckForDebuggerJustMyCode(&1C2F97D9_ConsoleApplication2_cpp);
  foo(&a);
  Class::~Class(&a);
  return 0;
}

RTTI(运行时类型信息)

typeid 是 C++ 中的运算符,用于获取对象的类型信息。

typeid 运算符接受一个表达式作为参数,并返回一个表示该表达式类型的 std::type_info 对象。std::type_info 类定义在 <typeinfo> 头文件中。

std::type_info 类提供了一些成员函数和操作符,用于比较类型信息。以下是一些常用的成员函数:

  • name():返回一个指向类型名称的 C 字符串。
  • raw_name():返回一个指向类型名称的内部字符串,该字符串可能包含特定于实现的修饰符和命名约定。这个原始名称是特定于编译器和平台的。
  • hash_code():用于获取类型信息的哈希码。
  • before(const std::type_info& rhs):比较类型信息之间的顺序。如果当前类型在 rhs 之前,则返回 true;否则返回 false。before() 函数的比较结果是特定于实现的,并且不受 C++ 标准的具体规定。不同的编译器和平台可能会有不同的比较策略和结果。

为了实现这一功能,Visual C++ 会定义 RTTI 相关结构来存储类相关符号。
在这里插入图片描述
这就是为什么 IDA 即使没有符号依旧能识别出虚表的名称:

_DWORD *__thiscall sub_412D50(_DWORD *this)
{
  __CheckForDebuggerJustMyCode(&unk_41C067);
  *this = &CVirtual::`vftable';
  this[1] = 1;
  this[2] = 2;
  puts("CVirtual()");
  return this;
}

低版本的 Visual C++ 只有在使用 typeid 相关功能的时候才会出现 RTTI 相关结构,而高版本默认开启 RTTI 。可以在 Visual C++ 的 项目设置 -> 配置属性 -> C/C++ -> 所有选项 -> 启用运行时类型信息 开启或关闭 RTTI ,但是即使关闭如果使用 typeid 或者使用 try...catchcatch 有数据类型,一般在类的构造析构代码中编译器会自动加异常处理)会强制开启 RTTI ,不过不会与虚表关系起来,也就是 IDA 不能识别虚表符号。

关于 RTTI 具体结构见异常处理相关结构

虚函数

虚函数(Virtual Function)是面向对象编程中的一个重要概念,用于实现多态性(Polymorphism)。为了实现虚函数,Visual C++ 编译器引入了虚表和虚函数等结构。

注意:构造函数不能写为虚函数,但是析构函数可以写为虚函数。因为虚函数的调用依赖于对象的类型信息,而构造函数在创建对象时用于初始化对象的状态,此时对象的类型尚未确定。因此,构造函数不能是虚函数。

这里使用如下代码来介绍虚函数:

#include <iostream>

class CVirtual {
public:
    CVirtual() {
        m_nMember1 = 1;
        m_nMember2 = 2;
        puts("CVirtual()");
    }

    virtual ~CVirtual() {
        puts("~CVirtual()");
    }

    virtual void fun1() {
        puts("fun1()");
    }

    virtual void fun2() {
        puts("fun2()");
    }

private:
    int m_nMember1;
    int m_nMember2;
};

int main() {
    CVirtual object;
    object.fun1();
    object.fun2();
    return 0;
}

虚表

在构造函数中会初始化 CVirtual 中的虚表指针 __vftable 指向 .rdata 段中的虚表 CVirtual::vftable

struct __cppobj CVirtual
{
  CVirtual_vtbl *__vftable /*VFT*/;
  int m_nMember1;
  int m_nMember2;
};

void __thiscall CVirtual::CVirtual(CVirtual *this)
{
  this->__vftable = (CVirtual_vtbl *)CVirtual::`vftable';
  this->m_nMember1 = 1;
  this->m_nMember2 = 2;
  _puts("CVirtual()");
}

其中虚表的类型和虚表的定义如下:

struct /*VFT*/ CVirtual_vtbl
{
  void (__thiscall *~CVirtual)(CVirtual *this);
  void (__thiscall *fun1)(CVirtual *this);
  void (__thiscall *fun2)(CVirtual *this);
};

void (__cdecl *const ??_7CVirtual@@6B@[4])() =
{
  &CVirtual::`vector deleting destructor',
  &CVirtual::fun1,
  &CVirtual::fun2,
  NULL
};

在析构函数代码如下,可以看到再析构函数开始的地方会将类的虚表指针指向该析构函数对应的类的虚表。因为在存在继承的类中子类析构之后在调用子类的虚函数可能会访问到已释放的资源造成 UAF,因此需要再析构函数中还原虚表指针。

void __thiscall CVirtual::~CVirtual(CVirtual *this)
{
  this->__vftable = (CVirtual_vtbl *)CVirtual::`vftable';
  _puts("~CVirtual()");
}

因此虚表指针有如下特征:

  • 构造函数赋值虚表指针。
  • 析构函数还原虚表指针。

即使我们不实现构造析构函数,为了安全起见编译器还是会自动生成构造析构函数来赋值和还原虚表指针。不过这种函数比较短,通常被优化内联到代码中,不以单独函数存在。

另外我们发现虚表中记录的析构函数不是对象真正的析构函数,而是析构代理函数 vector deleting destructor

CVirtual *__thiscall CVirtual::`vector deleting destructor'(CVirtual *this, char a2)
{
  CVirtual::~CVirtual(this);
  if ( (a2 & 1) != 0 )
    operator delete(this, 0xCu);
  return this;
}

根据对上述代码的分析可知,虚表及在内存中的布局如下:
在这里插入图片描述
虚表的特征总结如下:

  • 不考虑继承的情况下一个类至少有一个虚函数才会存在虚表。
  • 不同类的虚表不同,相同类的对象共享一个虚表。
  • 虚表不可修改,通常存放在全局数据区,由编译器生成。
  • 虚表的结尾不一定为 0 因此从逆向角度不能确定虚表的范围。
  • 虚表由函数指针构成。
  • 虚表的成员函数顺序按照类中函数声明的顺序排列。
  • 对象首地址处保存虚表指针。

虚函数的调用

调用声明虚函数的成员函数实际上是直接 call 的函数地址,没有查虚表。

.text:004011B7 lea     ecx, [ebp+object]               ; this
.text:004011BA call    ?fun1@CVirtual@@UAEXXZ          ; CVirtual::fun1(void)

成员函数必须产生多态才会通过虚表调用成员函数。成员函数产生多态的条件有:

  • 是虚函数:成员函数必须在基类中声明为虚函数(使用 virtual 关键字),以便在派生类中进行覆盖(override)。
  • 使用指针或者使用引用:成员函数必须通过指针或引用进行调用,而不是直接通过对象进行调用。这样,编译器会在运行时根据实际对象的类型来确定要调用的虚函数。

只要满足这两个条件,即便是在析构函数中也可以进行多态(强转指针)。

另外强转指针是一个很危险的操作,以下面这段代码为例,虽然强转成 CDerived 但是虚表用的还是 CBase 的虚表,因此调用 CDerived 中的函数可能会调用到其它函数或者无效的函数指针。

    CBase base;
    ((CDerived *) &base)->fun2();

例如我们将 main 函数改为下面这种形式:

int main() {
    CVirtual object;
    CVirtual *p_object = &object;
    p_object->fun1();
    p_object->fun2();
    return 0;
}

这时候成员函数是通过虚表调用的。

.text:0040111D 8B 4D E0                      mov     ecx, [ebp+p_object]             ; ecx 是 CVirtual 的地址
.text:00401120 8B 11                         mov     edx, [ecx]                      ; edx 是虚表地址
.text:00401122 8B 4D E0                      mov     ecx, [ebp+p_object]             ; ecx 是 CVirtual 的地址
.text:00401125 8B 42 04                      mov     eax, [edx+4]                    ; eax 是函数 fun1 的地址
.text:00401128 FF D0                         call    eax                             ; 调用 fun1

继承

单重继承

这里使用如下代码来介绍单重继承:

#include <iostream>

class CBase {
public:
    CBase() {
        m_nMember = 1;
        puts(__FUNCTION__);
    }

    virtual ~CBase() {
        puts(__FUNCTION__);
    }

    virtual void fun1() {
        puts(__FUNCTION__);
    }

    virtual void fun3() {
        puts(__FUNCTION__);
    }

private:
    int m_nMember;
};

class CDerived : public CBase {
public:
    CDerived() {
        m_nMember = 2;
        puts(__FUNCTION__);
    }

    ~CDerived() {
        puts(__FUNCTION__);
    }

    virtual void fun1() {
        puts(__FUNCTION__);
    }

    virtual void fun2() {
        puts(__FUNCTION__);
    }

private:
    int m_nMember;
    CBase base;
};

int main() {
    CDerived Derived;
    return 0;
}

构造析构顺序

在类的构造和析构过程中,并不仅仅执行用户定义的构造和析构函数,还涉及到其他构造和析构操作的顺序。

构造顺序:

  • 构造基类
  • 构造成员对象(对象内部定义的一些成员变量)
  • 构造自身

析构顺序:

  • 析构自身
  • 析构成员对象
  • 析构基类

这里有以下几点需要注意:

  • 构造析构顺序通常是我们还原类的继承关系的一个重要依据,不过这里要区分基类和成员对象。
    • 区分基类和成员对象的构造可以根据传入的 this 指针。基类传的是整个对象的地址,而成员对象传的是成员变量的地址。如果这两个地址相同就根据代码可读性还原。
  • 基类的构造一定在修改虚表指针之前,而成员对象的构造时间看编译器版本。
    • 对于老版本编译器(例如 VC 6.0)成员对象的构造在修改虚表之前。
    • 对于新版本编译器成员对象的构造在修改虚表之后。(也可以作为区分基类和成员对象的一个依据)

构造函数:

void __thiscall CBase::CBase(CBase *this)
{
  this->__vftable = (CBase_vtbl *)CBase::`vftable';
  this->m_nMember = 1;
  _puts("CBase::CBase");
}

void __thiscall CDerived::CDerived(CDerived *this)
{
  // 构造基类
  CBase::CBase(this);
  this->__vftable = (CDerived_vtbl *)CDerived::`vftable';
  // 构造成员对象
  CBase::CBase(&this->base);
  // 构造自身
  this->m_nMember = 2;
  _puts("CDerived::CDerived");
}

析构函数:

void __thiscall CBase::~CBase(CBase *this)
{
  this->__vftable = (CBase_vtbl *)CBase::`vftable';
  _puts("CBase::~CBase");
}

void __thiscall CDerived::~CDerived(CDerived *this)
{
  this->__vftable = (CDerived_vtbl *)CDerived::`vftable';
  // 析构自身
  _puts("CDerived::~CDerived");
  // 析构成员对象
  CBase::~CBase(&this->base);
  // 析构基类
  CBase::~CBase(this);
}

内存结构

派生类的虚表填充过程:

  • 复制基类的虚表(函数顺序不变)。
  • 如果派生类虚函数中有覆盖基类的虚函数(与基类的对应函数同名同参),使用派生类的虚函数地址覆盖对应表项。
  • 如果派生类有新增的虚函数,将其放在虚表后面。

派生类的对象填充过程:

  • 虚表指针指向派生类对应的虚表。
  • 将派生类新增的成员放到基类的成员后面。

因此示例代码中的 CBaseCDerived 类的实例化的对象和虚表结构如下:
在这里插入图片描述
虚表函数重合是还原类继承关系的一个重要依据。

多重继承

这里使用如下代码来介绍多重继承:

#include <iostream>

class CBase1 {
public:
    CBase1() {
        m_nMember = 1;
        puts(__FUNCTION__);
    }

    virtual ~CBase1() {
        puts(__FUNCTION__);
    }

    virtual void fun1() {
        puts(__FUNCTION__);
    }

    virtual void fun2() {
        puts(__FUNCTION__);
    }

    virtual void fun3() {
        puts(__FUNCTION__);
    }

private:
    int m_nMember;
};

class CBase2 {
public:
    CBase2() {
        m_nMember = 2;
        puts(__FUNCTION__);
    }

    virtual ~CBase2() {
        puts(__FUNCTION__);
    }

    virtual void fun1() {
        puts(__FUNCTION__);
    }

    virtual void fun4() {
        puts(__FUNCTION__);
    }

    virtual void fun5() {
        puts(__FUNCTION__);
    }

private:
    int m_nMember;
};

class CDerived : public CBase1,public CBase2 {
public:
    CDerived() {
        m_nMember = 3;
        puts(__FUNCTION__);
    }

    ~CDerived() {
        puts(__FUNCTION__);
    }

    virtual void fun2() {
        puts(__FUNCTION__);
    }

    virtual void fun4() {
        puts(__FUNCTION__);
    }

    virtual void fun6() {
        puts(__FUNCTION__);
    }

private:
    int m_nMember;
    CBase1 base1;
    CBase2 base2;
};

int main() {
    CDerived Derived;
    return 0;
}

内存结构

下图是上面的示例代码的虚表结构:
在这里插入图片描述

多重继承对象的虚表结构规律:

  • 将继承的对象的虚表各复制一份(顺序不变)。
  • 如果派生类的函数重写了其中一个对象的函数(函数名同参数类型的虚函数)则将该函数地址覆盖到对应虚表的对应函数上。
  • 如果派生类的虚函数没有重写任何一个父类的函数则将该函数追加到第一个虚表后面(例如调用虚表 2 的函数相当于把对象当 CBase2 解析了,如果是没有重写的函数就不会有问题,但是如果是重写的函数需要将对象按照 CDerived 解析,为了提高效率就将新增的虚函数放到虚表 1 中)。
  • 如果派生类的虚函数同时重写多个对象的函数则将所有重写的函数对应虚表的对应函数指针都覆盖为重新的函数。不过对于虚表指针不在对象开头的虚表该函数由于调用时传入的是 this + 8 因此需要一个代理函数将传入的地址转换为整个对象的地址。(示例代码就是同时重写了两个父类的析构函数)
  • 如果继承的两个父类有同样的虚函数,不会出现语法错误,例如示例代码中的 fun1() 。但是如果调用了这种函数,由于编译器无法确定到底调用了哪个函数,因此会报语法错误。

多重继承对象的结构规律:

  • 按照继承顺序(例如 class A:piblic B, public C 的继承顺序是 BC)将父类的成员依次拷贝到对象中。
  • 将派生类新增的成员放到基类的成员后面。
  • 将所有父类的虚表指针指向对应复制并修改后的虚表,修改规则见多重继承对象的虚表结构规律。

构造析构顺序

构造顺序:

  • 构造第一个基类
  • 构造第二个基类
  • 构造成员对象
  • 构造自身

实例代码的构造函数如下:

void __thiscall CDerived::CDerived(CDerived *this)
{
  // 构造第一个基类
  CBase1::CBase1(this);
  // 构造第二个基类
  CBase2::CBase2(&this->CBase2);
  this->CBase1::__vftable = (CDerived_vtbl *)CDerived::`vftable'{for `CBase1'};
  this->CBase2::__vftable = (CBase2_vtbl *)CDerived::`vftable'{for `CBase2'};
  // 构造成员对象
  CBase1::CBase1(&this->base1);
  CBase2::CBase2(&this->base2);
  //构造自身
  this->m_nMember = 3;
  _puts("CDerived::CDerived");
}

析构顺序:

  • 析构自身
  • 析构成员对象
  • 析构第二个基类
  • 析构第一个基类

实例代码析构代码如下:

void __thiscall CDerived::~CDerived(CDerived *this)
{
  this->CBase1::__vftable = (CDerived_vtbl *)CDerived::`vftable'{for `CBase1'};
  this->CBase2::__vftable = (CBase2_vtbl *)CDerived::`vftable'{for `CBase2'};
  // 析构自身
  _puts("CDerived::~CDerived");
  // 析构成员对象
  CBase2::~CBase2(&this->base2);
  CBase1::~CBase1(&this->base1);
  // 析构第二个基类
  CBase2::~CBase2(&this->CBase2);
  // 析构第一个基类
  CBase1::~CBase1(this);
}

因此在逆向过程中判断多重继承的依据如下:

  • 在修改虚表指针之前多次调用构造函数(看编译器版本)。
  • 同一个虚表指针在构造函数调用的构造函数中被填之后在构造函数中又被修改(如果是成员对象构造完之后虚表指针就不会被修改了)。
  • 如果没有虚表无法判断,那么是继承还是成员对象已经无所谓了,因为没有多态。

抽象类

以下面这段代码为例:

#include <iostream>

class CBase {
public:
    CBase() {
        m_nMember = 1;
        puts(__FUNCTION__);
    }

    ~CBase() {
        puts(__FUNCTION__);
    }

    virtual void fun1() = 0;

private:
    int m_nMember;
};

class CDerived : public CBase {
public:
    CDerived() {
        m_nMember = 2;
        puts(__FUNCTION__);
    }

    virtual void fun1() {
        puts(__FUNCTION__);
    }

    virtual void fun2() {
        puts(__FUNCTION__);
    }

private:
    int m_nMember;
};

int main() {
    CDerived Derived;
    return 0;
}

CBase 为一个抽象类,因为该函数的成员函数 fun1 没有实现。因此如果 CDerived 想要继承 CBase 就一定要实现 fun1 函数。

CBase 函数的虚表中对应 fun3 的位置上是 _purecall 函数地址。

void (__cdecl *const ??_7CBase@@6B@[2])() =
{ &_purecall };

_purecall 函数实现如下,主要作用是终止程序。

extern "C" int __cdecl _purecall()
{
    _purecall_handler const purecall_handler = _get_purecall_handler();
    if (purecall_handler)
    {
        purecall_handler();

        // The user-registered purecall handler should not return, but if it does,
        // continue with the default termination behavior.
    }

    abort();
}

要想调到该函数我们只需要通过调用 CBase 的析构函数(这里需要定义 CBase 的析构函数,否则析构函数会被优化掉)将对象虚表指针指向 CBase 的虚表,之后触发多态调用虚表中的 fun1 即可。

int main() {
    CDerived Derived;
    ((CBase *) &Derived)->~CBase();
    (&Derived)->fun1();
    return 0;
}

在逆向过程中识别抽象类的一个很重要的依据就是虚表中是否有 _purecall 函数地址。

菱形继承

这里使用如下代码来介绍多重继承:

#include <iostream>

class A {
public:
    A() {
        m_nMember = 1;
        puts(__FUNCTION__);
    }

    ~A() {
        puts(__FUNCTION__);
    }

    virtual void fun1() {
        puts(__FUNCTION__);
    }

    virtual void fun2() {
        puts(__FUNCTION__);
    }

    virtual void fun3() {
        puts(__FUNCTION__);
    }

    virtual void fun4() {
        puts(__FUNCTION__);
    }

private:
    int m_nMember;
};

class B : virtual public A {
public:
    B() {
        m_nMember = 2;
        puts(__FUNCTION__);
    }

    ~B() {
        puts(__FUNCTION__);
    }

    virtual void fun2() {
        puts(__FUNCTION__);
    }

    virtual void fun4() {
        puts(__FUNCTION__);
    }

    virtual void fun5() {
        puts(__FUNCTION__);
    }

    virtual void fun6() {
        puts(__FUNCTION__);
    }

private:
    int m_nMember;
};

class C : virtual public A {
public:
    C() {
        m_nMember = 3;
        puts(__FUNCTION__);
    }

    ~C() {
        puts(__FUNCTION__);
    }

    virtual void fun2() {
        puts(__FUNCTION__);
    }

    virtual void fun5() {
        puts(__FUNCTION__);
    }

    virtual void fun7() {
        puts(__FUNCTION__);
    }

private:
    int m_nMember;
};

class BC : public B, public C {
public:
    BC() {
        m_nMember = 4;
        puts(__FUNCTION__);
    }

    ~BC() {
        puts(__FUNCTION__);
    }

    virtual void fun2() {
        puts(__FUNCTION__);
    }

    virtual void fun3() {
        puts(__FUNCTION__);
    }

    virtual void fun4() {
        puts(__FUNCTION__);
    }

    virtual void fun5() {
        puts(__FUNCTION__);
    }

    virtual void fun7() {
        puts(__FUNCTION__);
    }

    virtual void fun8() {
        puts(__FUNCTION__);
    }

private:
    int m_nMember;
};

int main() {
    BC bc;
    return 0;
}

内存结构

示例代码中各个类对应的对象的内存结构如下:
在这里插入图片描述
从中我们可以发现菱形重继承对象的虚表的结构规律:

  • 所有与类 A 重复的虚函数一律覆盖到 A 的虚表中。
  • 所有与类 A 不重复但与 BC 重复的函数一律覆盖到 BC 的虚表中。特别的,如果与类 BC 都重复的函数两个虚表的函数都覆盖,但是其中一个覆盖的是一个代理函数,例如实例代码中的 BC::fun5()
  • BC 独有的虚函数追加到类开头的虚表指针指向的虚表中。
  • BC::fun7() 函数由于优化问题在使用传入的 this 指针访问成员的时候自动加了个偏移,因此没有通过代理函数对 this 指针进行转换。

菱形重继承对象的结构规律:

  • 虚继承的 A 放在最后,与前面的对象隔一个 4 字节的 0 填充(看编译器和编译参数,有的没有)。
  • 虚表指针后面跟一个虚基址偏移表指针指向虚基址偏移表,该表的前 4 字节没有用到,后 4 字节记录虚基址偏移表指针到基类之间的距离。

构造析构顺序

示例代码对应的构造函数如下,调用构造函数的传参为 BC::BC(&bc, 1);

void __thiscall A::A(A *this)
{
  this->__vftable = (A_vtbl *)A::`vftable';
  this->m_nMember = 1;
  puts("A::A");
}

void __thiscall B::B(B *this, int NotInit)
{
  if ( NotInit )
  {
    this->__vbtable = (vbtable *)&C::`vbtable';
    A::A((A *)&this[1].__vbtable);
  }
  this->__vftable = (B_vtbl *)B::`vftable'{for `B'};
  *(vbtable **)((char *)&this->__vbtable + this->__vbtable->offset) = (vbtable *)B::`vftable'{for `A'};
  *(B_vtbl **)((char *)&this->__vftable + this->__vbtable->offset) = (B_vtbl *)(this->__vbtable->offset - 12);
  this->m_nMember = 2;
  puts("B::B");
}

void __thiscall C::C(C *this, int NotInit)
{
  if ( NotInit )
  {
    this->__vbtable = (vbtable *)&C::`vbtable';
    A::A((A *)&this[1].__vbtable);
  }
  this->__vftable = (C_vtbl *)C::`vftable'{for `C'};
  *(vbtable **)((char *)&this->__vbtable + this->__vbtable->offset) = (vbtable *)C::`vftable'{for `A'};
  *(C_vtbl **)((char *)&this->__vftable + this->__vbtable->offset) = (C_vtbl *)(this->__vbtable->offset - 12);
  this->m_nMember = 3;
  puts("C::C");
}

void __thiscall BC::BC(BC *this, int NotInit)
{
  if ( NotInit )
  {
    this->b.__vbtable = &BC::`vbtable'{for `B'};
    this->c.__vbtable = &BC::`vbtable'{for `C'};
    A::A(&this->a);
  }
  B::B(&this->b, 0);
  C::C(&this->c, 0);
  this->b.__vftable = (B_vtbl *)BC::`vftable'{for `B'};
  this->c.__vftable = (C_vtbl *)BC::`vftable'{for `C'};
  *(vbtable **)((char *)&this->b.__vbtable + this->b.__vbtable->offset) = (vbtable *)BC::`vftable'{for `A'};
  *(B_vtbl **)((char *)&this->b.__vftable + this->b.__vbtable->offset) = (B_vtbl *)(this->b.__vbtable->offset - 28);
  this->m_nMember = 4;
  puts("BC::BC");
}

观察发现除了 A 的构造函数外都会传入一个参数 NotInit ,该函数的作用是标记 A 是否已经被初始化,从而确保基类 A 只被初始化一次。

实际上有些编译器还生成两个函数,一个初始化基类 A 的情况下调用,另一个在基类 A 未初始化下调用。

根据构造代码可知 BC 对象的构造过程如下:
在这里插入图片描述
析构调用的是 BC 析构函数的代理函数 BC::vbase_destructor(&bc);,该函数先调用 BC 的析构函数然后调用基类 A 的析构函数(因为 A 一定是最后析构的,所以析构其它对象的时候都不会考虑是否析构 A)。析构过程和构造过程顺序刚好相反。

void __thiscall B::~B(B *this)
{
  this->__vftable = (B_vtbl *)B::`vftable'{for `B'};
  *(vbtable **)((char *)&this->__vbtable + this->__vbtable->offset) = (vbtable *)B::`vftable'{for `A'};
  *(B_vtbl **)((char *)&this->__vftable + this->__vbtable->offset) = (B_vtbl *)(this->__vbtable->offset - 12);
  puts("B::~B");
}

void __thiscall C::~C(C *this)
{
  this->__vftable = (C_vtbl *)C::`vftable'{for `C'};
  *(vbtable **)((char *)&this->__vbtable + this->__vbtable->offset) = (vbtable *)C::`vftable'{for `A'};
  *(C_vtbl **)((char *)&this->__vftable + this->__vbtable->offset) = (C_vtbl *)(this->__vbtable->offset - 12);
  puts("C::~C");
}

void __thiscall BC::~BC(BC *this)
{
  this->b.__vftable = (B_vtbl *)BC::`vftable'{for `B'};
  this->c.__vftable = (C_vtbl *)BC::`vftable'{for `C'};
  *(vbtable **)((char *)&this->b.__vbtable + this->b.__vbtable->offset) = (vbtable *)BC::`vftable'{for `A'};
  *(B_vtbl **)((char *)&this->b.__vftable + this->b.__vbtable->offset) = (B_vtbl *)(this->b.__vbtable->offset - 28);
  puts("BC::~BC");
  C::~C(&this->c);
  B::~B(&this->b);
}

void __thiscall A::~A(A *this)
{
  this->__vftable = (A_vtbl *)A::`vftable';
  puts("A::~A");
}

void __thiscall BC::`vbase destructor'(BC *this)
{
  BC::~BC(this);
  A::~A(&this->a);
}

判断一个对象是菱形继承的依据是:

  • 有偏移表。
  • 根据构造函数参数决定是否调用基类构造。

异常

C++ 异常处理

windows pwn 基础知识中提到了 Visual C++ 中内置了一个异常处理 __try{...}__except(...){...},不过在 C++ 标准中也有一个异常处理的语法:try{...}catch(...){...} 。该异常处理语法的使用方式如下:

#include <iostream>

int main() {
    try {
        // 可能引发异常的代码
        throw std::exception("Something went wrong.");
    } catch (const std::exception &e) {
        // 处理 std::exception 类型的异常
        std::cout << "Caught std::exception: " << e.what() << std::endl;
    } catch (...) {
        // 处理任何类型的异常
        std::cout << "Caught an unknown exception." << std::endl;
    }
    return 0;
}

实际上异常处理的 catch 是一种类似函数重载的基址,catch 的参数类型会与 throw 出来的异常类型进行匹配。另外如果两个 catch 参数都是同一个类型会优先匹配前一个 catch

#include <iostream>
#include<string>

int main() {
    try {
        int x;
        std::cin >> x;
        switch (x) {
            case 1:
                throw 1;
                break;
            case 2:
                throw 2.0f;
                break;
            case 3:
                throw '3';
                break;
            default:
                throw std::exception(std::to_string(x).c_str());
        }
    } catch (int e) {
        std::cout << "Caught int: " << e << std::endl;
    } catch (float e) {
        std::cout << "Caught float: " << e << std::endl;
    } catch (char e) {
        std::cout << "Caught char: " << e << std::endl;
    } catch (const std::exception &e) {
        std::cout << "Caught std::exception: " << e.what() << std::endl;
    }
    return 0;
}

C++ 标准的异常处理 try{...}catch(...){...} 与 Visual C++ 中内置了的异常处理 __try{...}__except(...){...} 相比有如下区别:

  • 在 VC++ 中,可以使用 __try{}__except{} 来捕获和处理结构化异常,例如访问非法内存时引发的异常,这可以提供更底层的异常处理(例如我想访问一个可能被另一个线程修改的指针指向的内存,如果使用 C++ 标准异常处理需要手动判断,这就导致判断的时候可能指针指向有效内存,但是访问的时候会访问到无效地址,而使用 VC++ 自带异常处理就可以确保访问到异常地址的错误一定会被捕获。)。而 C++ 标准异常处理只能捕获 throw 的异常,但可以使用 C++ 异常处理机制来处理其他类型的异常,例如逻辑错误或自定义异常。这提供了更高级别的异常处理。
  • 在 VC++ 的自带的异常处理没有资源释放相关的处理,而 C++ 标准的异常处理会自动调用类的析构函数进行资源释放。

异常处理相关结构

C++ 异常处理相关结构如下:
在这里插入图片描述

ThrowInfo

ThrowInfo 的定义在 include\ehdata_forceinclude.h 中,具体定义如下:

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;
  • attributes: 抛出异常类型标记。
  • pmfnUnwind: 异常对象的析构函数地址。
  • pCatchableTypeArraycatch 块类型表,指向 CatchTableTypeArray 表结构。

如果开启地址随机化则 pForwardCompatpCatchableTypeArray 由绝对地址改为相对地址(后续的很多结构体都有这个机制),使用 int 类型再定义一遍是因为指针类型参与加减运算的基本单位是指针指向的结构的大小,不是真正意义上的加减运算。

CatchableTypeArray

CatchableTypeArray 的定义在 include\ehdata_forceinclude.h 中,具体定义如下:

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:整数类型,表示可捕获类型的数量。例如 throw 一个对象指针,实际上 throw 的是对象指针和 void* ,因此可捕获类型为 2 。
  • arrayOfCatchableTypes:指向 CatchableType 类型的指针数组,数组中每一个成员代表着一种可以捕获的类型。

CatchableType

CatchableType 的定义在 include\ehdata_forceinclude.h 中,具体定义如下:

typedef struct PMD {
	int	mdisp;	// Offset of intended data within base
	int	pdisp;	// Displacement to virtual base pointer
	int	vdisp;	// Index within vbTable to offset of base
} PMD;

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;
  • properties:用于表示 Catchable Type 的属性,通常是一个位字段(在枚举 __CT_flags 中定义)。
    • CT_IsSimpleType(1):简单类型。
    • CT_ByReferenceOnly(2):指针类型。
    • CT_HasVirtualBase(4):虚基类。
    • CT_IsWinRTHandle(8)WinRT 句柄。
    • CT_IsStdBadAlloc(16):标准库的 std::bad_alloc 异常类型。
  • pType:指向异常类型结构,TypeDescriptor 表结构。
  • thisDisplacement:基类信息。
    • mdisp:基类偏移。
    • pdisp:虚基类偏移。
    • vdisp:基类虚表偏移。
  • sizeOrOffset:类的大小。
  • copyFunction:拷贝构造函数的指针。

TypeDescriptor

TypeDescriptor 的定义在 include\ehdata_forceinclude.h 中,具体定义如下:

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;
  • hash:类型名称的哈希值。
  • spare:保留,可用于 RTTI 的名称记录。
  • name[]:类型名称(符号修饰)。

FuncInfo

FuncInfo 的定义在 include\ehdata.h 中,具体定义如下:

typedef const struct _s_UnwindMapEntry {
	__ehstate_t	toState;					// State this action takes us to
#if _EH_RELATIVE_FUNCINFO
	int			action;						// Image relative offset of funclet
#else
	void		(__cdecl * action)(void);	// Funclet to call to effect state change
#endif
} UnwindMapEntry;

typedef const struct _s_FuncInfo
{
	unsigned int		magicNumber:29;		// Identifies version of compiler
	unsigned int		bbtFlags:3;			// flags that may be set by BBT processing
	__ehstate_t			maxState;			// Highest state number plus one (thus
											// number of entries in unwind map)
#if _EH_RELATIVE_FUNCINFO
	int					dispUnwindMap;		// Image relative offset of the unwind map
	unsigned int		nTryBlocks;			// Number of 'try' blocks in this function
	int					dispTryBlockMap;	// Image relative offset of the handler map
	unsigned int		nIPMapEntries;		// # entries in the IP-to-state map. NYI (reserved)
	int					dispIPtoStateMap;	// Image relative offset of the IP to state map
	int					dispUwindHelp;		// Displacement of unwind helpers from base
	int					dispESTypeList;		// Image relative list of types for exception specifications
#else
	UnwindMapEntry*		pUnwindMap;			// Where the unwind map is
	unsigned int		nTryBlocks;			// Number of 'try' blocks in this function
	TryBlockMapEntry*	pTryBlockMap;		// Where the handler map is
	unsigned int		nIPMapEntries;		// # entries in the IP-to-state map. NYI (reserved)
	void*				pIPtoStateMap;		// An IP to state map.  NYI (reserved).
	ESTypeList*			pESTypeList;		// List of types for exception specifications
#endif
	int					EHFlags;			// Flags for some features.
} FuncInfo;
  • magicNumberFuncInfo 结构体标志,通常填充为 19930522h
  • maxState:最大栈展开数的下标值,也是 pUnwindMap 数组中的成员个数。
  • pUnwindMap: 指向栈展开函数表的指针,指向 UnwindMapEntry 表结构。
    • toState:栈展开数下标值。
    • lpFunAction:展开执行函数,主要是释放各种资源。
  • nTryBlocks:函数中 try 的数量,也是 pTryBlockMap 数组中成员的个数。
  • pTryBlockMap:指向一个 TryBlockMapEntry 类型的数组,记录了 try 相关信息。

TryBlockMapEntry

TryBlockMapEntry 的定义在 include\ehdata.h 中,具体定义如下:

typedef const struct _s_TryBlockMapEntry {
	__ehstate_t		tryLow;				// Lowest state index of try
	__ehstate_t		tryHigh;			// Highest state index of try
	__ehstate_t		catchHigh;			// Highest state index of any associated catch
	int				nCatches;			// Number of entries in array
#if _EH_RELATIVE_FUNCINFO
	int				dispHandlerArray;	// Image relative offset of list of handlers for this try
#else
	HandlerType* pHandlerArray;	// List of handlers for this try
#endif
} TryBlockMapEntry;
  • tryLowtryHigh:表示 try 块的最低(高)状态索引(try 嵌套),用于范围检查。
  • catchHighcatch 块的最高状态索引,用于范围检查,通常是 tryHigh + 1
  • nCatchestry 对应的 catch 的数量。
  • pHandlerArray:指向 HandlerType 类型的指针,指向此 try 块的处理程序数组。

HandlerType

HandlerType 的定义在 include\ehdata.h 中,具体定义如下:

typedef const struct _s_HandlerType {
	unsigned int	adjectives;			// Handler Type adjectives (bitfield)
#if _EH_RELATIVE_FUNCINFO
	int				dispType;			// Image relative offset of the corresponding type descriptor
	int				dispCatchObj;		// Displacement of catch object from base
	int				dispOfHandler;		// Image relative offset of 'catch' code
#if defined(_WIN64) || defined(_CHPE_X86_ARM64_EH_)
	int				dispFrame;			// displacement of address of function frame wrt establisher frame
#endif
#else   // _EH_RELATIVE_FUNCINFO
	TypeDescriptor*	pType;				// Pointer to the corresponding type descriptor
	ptrdiff_t		dispCatchObj;		// Displacement of catch object from base
	void *			addressOfHandler;	// Address of 'catch' code
#endif // _EH_RELATIVE_FUNCINFO
} HandlerType;
  • adjectives:用于 catch 块的匹配检查。
  • pType:指向 TypeDescriptor 类型结构,用于描述 catch 块要捕捉的类型。如果 catch 匹配所有类型则这个值为 NULL 。
  • dispCatchObj:用于定位异常对象的偏移量。
  • addressOfHandlercatch 块代码的地址。

异常处理流程

针对下面这段示例代码讲解 C++ 异常处理流程。这里分析的是关闭全局优化的 Debug 版。

#include <stdio.h>

int main(int argc, char *argv[]) {
    try {
        throw 1;// 抛出异常
    } catch (int e) {
        printf(" 触发int异常\r\n");
    } catch (float e) {
        printf(" 触发float异常\r\n");
    }
    return 0;
}

异常注册

main 函数的开头程序想 SEH 链表插入了一个节点,其中处理函数为 __ehhandler$_main ,另外节点上还有一个 TryLevel 用来记录异常发生的位置,初始值 -1 表示不在 try 块中。

.text:0045A8C0 push    ebp
.text:0045A8C1 mov     ebp, esp
.text:0045A8C3 push    0FFFFFFFFh                      ; state
.text:0045A8C5 push    offset __ehhandler$_main        ; frameHandler
.text:0045A8CA mov     eax, large fs:0
.text:0045A8D0 push    eax                             ; pNext
.text:0045A8D1 mov     large fs:0, esp

这个注册异常的节点实际上是 EHRegistrationNode 结构体,定义如下:

struct EHRegistrationNode
{
  EHRegistrationNode *pNext;
  void *frameHandler;
  int state;
};

异常产生

main 函数中 throw 抛出异常时会调用 _CxxThrowException 函数并传入 pExceptionObjectpThrowInfo 两个参数,其中 pExceptionObject 存放异常值。

.text:0045A8F3 mov     [ebp+pExceptionObject], 1
.text:0045A8FA push    offset __TI1H                   ; pThrowInfo
.text:0045A8FF lea     eax, [ebp+pExceptionObject]
.text:0045A902 push    eax                             ; pExceptionObject
.text:0045A903 call    j___CxxThrowException@8         ; _CxxThrowException(x,x)

_CxxThrowException 函数最终调用 RaiseException 产生异常,异常号为 0xE06D7363 。

void __stdcall __noreturn _CxxThrowException(_DWORD *pExceptionObject, const _s_ThrowInfo *pThrowInfo)
{
  unsigned int parameters[3]; // [esp+0h] [ebp-24h] BYREF
  void (__stdcall *PrepareThrow)(void *); // [esp+Ch] [ebp-18h]
  unsigned int magicNumber; // [esp+10h] [ebp-14h]
  unsigned int Target; // [esp+14h] [ebp-10h]
  WinRTExceptionInfo *pWei; // [esp+18h] [ebp-Ch]
  WinRTExceptionInfo **ppWei; // [esp+1Ch] [ebp-8h]
  const _s_ThrowInfo *pTI; // [esp+20h] [ebp-4h]

  pTI = pThrowInfo;
  magicNumber = 0x19930520;
  if ( pThrowInfo && (pTI->attributes & 0x10) != 0 )
  {
    ppWei = (WinRTExceptionInfo **)(*pExceptionObject - 4);
    pWei = *ppWei;
    pTI = pWei->throwInfo;
    PrepareThrow = pWei->PrepareThrow;
    Target = (unsigned int)PrepareThrow;
    ((void (__thiscall *)(void (__stdcall *)(void *), WinRTExceptionInfo **))PrepareThrow)(PrepareThrow, ppWei);
  }
  if ( pTI && (pTI->attributes & 8) != 0 )
    magicNumber = 0x1994000;
  parameters[0] = magicNumber;
  parameters[1] = (unsigned int)pExceptionObject;
  parameters[2] = (unsigned int)pTI;
  RaiseException(0xE06D7363, 1u, 3u, parameters);
}

异常派发

__ehhandler$_main 函数将 eax 传入了 FuncInfo 结构体地址。之所以用 eax 是因为这个函数还要传其它参数,为了不影响函数传参这里使用 eax 额外传入 FuncInfo 结构体地址。

.text:005035E0 __ehhandler$_main proc near             ; DATA XREF: _main+5↑o
.text:005035E0 nop
.text:005035E1 nop
.text:005035E2 mov     eax, offset FuncInfo
.text:005035E7 jmp     j____CxxFrameHandler3
.text:005035E7
.text:005035E7 __ehhandler$_main endp

FuncInfo 结构体内容如下,从中可以看出 UnwindMap 有两项,TryBlockMap 有 1 项(一个 try)。

_s_FuncInfo FuncInfo =
{ 0x19930522, 0u, 2, &UnwindMap, 1u, &TryBlockMap, 0u, NULL, NULL, 1 };

__CxxFrameHandler3 函数定义在 crt\src\i386\trnsctrl.cpp 中,内容如下:

//
// __CxxFrameHandler3 - Real entry point to the runtime
//                                              __CxxFrameHandler2 is an alias for __CxxFrameHandler3
//                                              since they are compatible in VC version of CRT
//                      These function should be separated out if a change makes
//                                              __CxxFrameHandler3 incompatible with __CxxFrameHandler2
//
extern "C" _VCRTIMP __declspec(naked) DECLSPEC_GUARD_SUPPRESS EXCEPTION_DISPOSITION __cdecl __CxxFrameHandler3(
/*
    EAX=FuncInfo   *pFuncInfo,          // Static information for this frame
*/
    EHExceptionRecord  *pExcept,        // Information for this exception
    EHRegistrationNode *pRN,            // Dynamic information for this frame
    void               *pContext,       // Context info (we don't care what's in it)
    DispatcherContext  *pDC             // More dynamic info for this frame (ignored on Intel)
) {
    FuncInfo   *pFuncInfo;
    EXCEPTION_DISPOSITION result;

    __asm {
        //
        // Standard function prolog
        //
        push    ebp
        mov     ebp, esp
        sub     esp, __LOCAL_SIZE		// 申请局部变量空间,__LOCAL_SIZE = 8。
        push    ebx
        push    esi
        push    edi
        cld             // A bit of paranoia -- Our code-gen assumes this
        				// 将 DF 位置 0,每次操作后,esi、edi 递增

        //
        // Save the extra parameter
        //
        mov     pFuncInfo, eax		// 传入的 FuncInfo 结构体地址
        }

    EHTRACE_FMT1("pRN = 0x%p", pRN);

    result = __InternalCxxFrameHandlerWrapper<RENAME_EH_EXTERN(__FrameHandler3)>( pExcept, pRN, (PCONTEXT)pContext, pDC, pFuncInfo, 0, nullptr, FALSE );

    EHTRACE_HANDLER_EXIT(result);

    __asm {
        pop     edi
        pop     esi
        pop     ebx
        mov     eax, result
        mov     esp, ebp
        pop     ebp
        ret     0
        }
}

__InternalCxxFrameHandlerWrapper 函数调用了 __InternalCxxFrameHandler 函数,该函数没有源码,但从 IDA 反编译结果可以看到该函数的主要功能是完成异常类型的检查以及栈展开,并最终调用查找 try 块和 catch 块的核心函数 FindHandler___FrameHandler3_

void __cdecl __FrameHandler3::FrameUnwindToState(
        EHRegistrationNode *pRN,
        void *pDC,
        const _s_FuncInfo *pFuncInfo,
        int targetState)
{
  __vcrt_ptd *v4; // eax
  int nxtState; // [esp+14h] [ebp-24h]
  int *p_ProcessingThrow; // [esp+18h] [ebp-20h]
  int curState; // [esp+1Ch] [ebp-1Ch]

  curState = __FrameHandler3::GetCurrentState(pRN, pDC, pFuncInfo);
  p_ProcessingThrow = &j____vcrt_getptd()->_ProcessingThrow;
  ++*p_ProcessingThrow;
  while ( curState != targetState )
  {
    if ( curState <= -1 || curState >= pFuncInfo->maxState )
      j__abort();
    nxtState = pFuncInfo->pUnwindMap[curState].toState;
    if ( pFuncInfo->pUnwindMap[curState].action )
    {
      __FrameHandler3::SetState(pRN, pFuncInfo, nxtState);
      _CallSettingFrame((unsigned int)pFuncInfo->pUnwindMap[curState].action, (unsigned int)pRN, 0x103u);
    }
    curState = nxtState;
  }
  if ( j____vcrt_getptd()->_ProcessingThrow > 0 )
  {
    v4 = j____vcrt_getptd();
    --v4->_ProcessingThrow;
  }
  if ( curState != targetState )
    j__abort();
  __FrameHandler3::SetState(pRN, pFuncInfo, curState);
}

_EXCEPTION_DISPOSITION __cdecl __InternalCxxFrameHandler<__FrameHandler3>(
        EHExceptionRecord *pExcept,
        EHRegistrationNode *pRN,
        _CONTEXT *pContext,
        void *pDC,
        const _s_FuncInfo *pFuncInfo,
        int CatchDepth,
        EHRegistrationNode *pMarkerRN,
        unsigned __int8 recursive)
{
  _EXCEPTION_DISPOSITION v8; // eax
  __FrameHandler3::TryBlockMap tryBlockMap; // [esp+0h] [ebp-18h] BYREF
  _EXCEPTION_DISPOSITION result; // [esp+8h] [ebp-10h]
  int (*v11)(...); // [esp+Ch] [ebp-Ch]
  unsigned int Target; // [esp+10h] [ebp-8h]
  int (*pfn)(...); // [esp+14h] [ebp-4h]

  j____except_validate_context_record(pContext);
  if ( !j____vcrt_getptd()->_cxxReThrow
    && pExcept->ExceptionCode != 0xE06D7363
    && pExcept->ExceptionCode != 0x80000026
    && __FrameHandler3::getMagicNum(pFuncInfo) >= 0x19930522
    && __FrameHandler3::isEHs(pFuncInfo) )
  {
    return 1;
  }
  if ( (pExcept->ExceptionFlags & 0x66) != 0 )
  {
    if ( __FrameHandler3::GetMaxState(pDC, pFuncInfo) )
    {
      if ( !CatchDepth )
        __FrameHandler3::FrameUnwindToEmptyState(pRN, pDC, pFuncInfo);
    }
    return 1;
  }
  else
  {
    __FrameHandler3::TryBlockMap::TryBlockMap(&tryBlockMap, pFuncInfo, 0);
    if ( __FrameHandler3::TryBlockMap::getNumTryBlocks(&tryBlockMap)
      || __FrameHandler3::getMagicNum(pFuncInfo) >= 0x19930521 && __FrameHandler3::getESTypes(pFuncInfo)
      || __FrameHandler3::getMagicNum(pFuncInfo) >= 0x19930522 && __FrameHandler3::isNoExcept(pFuncInfo) )
    {
      if ( pExcept->ExceptionCode == 0xE06D7363
        && pExcept->NumberParameters >= 3
        && pExcept->params.magicNumber > 0x19930522 )
      {
        pfn = pExcept->params.pThrowInfo->pForwardCompat;
        if ( pfn )
        {
          v11 = pfn;
          Target = (unsigned int)pfn;
          v8 = pfn(pExcept, pRN, pContext, pDC, pFuncInfo, CatchDepth, pMarkerRN, recursive);
          result = v8;
          return v8;
        }
      }
      // 最终调用的函数
      FindHandler___FrameHandler3_(pExcept, pRN, pContext, pDC, pFuncInfo, recursive, CatchDepth, pMarkerRN);
    }
    return 1;
  }
}

FindHandler___FrameHandler3_ 函数首先从传入的异常处理节点指针 EHRegistrationNode *pRN 中取出 state,也就是记录异常发生的位置的值。之后判断这个值的范围是否合法(在 [-1, pFuncInfo->maxState] 范围内),如果不合法就终止程序。

  curState = __FrameHandler3::GetCurrentState(pRN, pDC, pFuncInfo);
  if ( curState < -1 || curState >= __FrameHandler3::GetMaxState(pDC, pFuncInfo) )
    j__abort();

之后先处理 ThrowInfo 为空的情况。如果 ThrowInfo 为空则使用使用当前异常和上下文。如果 j____vcrt_getptd()->_curexception 为空则说明没有异常,直接返回,否则再次判断 ThrowInfo 是否为空,如果依然为空说明出现错误,需要终止程序。之后调用 IsInExceptionSpec 判断异常类型是否是能捕获的类型,如果不是会终止程序。

unsigned __int8 __cdecl IsInExceptionSpec(EHExceptionRecord *pExcept, const _s_ESTypeList *pESTypeList)
{
  const _s_CatchableType *const *ppCatchable; // [esp+4h] [ebp-10h]
  int catchables; // [esp+8h] [ebp-Ch]
  int i; // [esp+Ch] [ebp-8h]
  unsigned __int8 bFoundMatchingTypeInES; // [esp+13h] [ebp-1h]

  if ( !pESTypeList )
    j__abort();
  bFoundMatchingTypeInES = 0;
  for ( i = 0; i < pESTypeList->nCount; ++i )
  {
    ppCatchable = pExcept->params.pThrowInfo->pCatchableTypeArray->arrayOfCatchableTypes;
    for ( catchables = pExcept->params.pThrowInfo->pCatchableTypeArray->nCatchableTypes; catchables > 0; --catchables )
    {
      if ( j____TypeMatch(&pESTypeList->pTypeArray[i], *ppCatchable, pExcept->params.pThrowInfo) )
      {
        bFoundMatchingTypeInES = 1;
        break;
      }
      ++ppCatchable;
    }
  }
  return bFoundMatchingTypeInES;
}

unsigned __int8 __cdecl Is_bad_exception_allowed(const _s_ESTypeList *pESTypeList)
{
  int i; // [esp+4h] [ebp-4h]

  for ( i = 0; i < pESTypeList->nCount; ++i )
  {
    if ( type_info::operator==(
           (type_info *)pESTypeList->pTypeArray[i].pType,
           &std::bad_exception `RTTI Type Descriptor') )
    {
      return 1;
    }
  }
  return 0;
}

  if ( pExcept->ExceptionCode == 0xE06D7363
    && pExcept->NumberParameters == 3
    && (pExcept->params.magicNumber == 0x19930520
     || pExcept->params.magicNumber == 0x19930521
     || pExcept->params.magicNumber == 0x19930522)
    && !pExcept->params.pThrowInfo )            
  {
    if ( !j____vcrt_getptd()->_curexception )
      return;                                   
    pExcept = (EHExceptionRecord *)j____vcrt_getptd()->_curexception;
    pContext = (_CONTEXT *)j____vcrt_getptd()->_curcontext;
    IsRethrow = 1;
    if ( !pExcept
      || pExcept->ExceptionCode == 0xE06D7363
      && pExcept->NumberParameters == 3
      && (pExcept->params.magicNumber == 0x19930520
       || pExcept->params.magicNumber == 0x19930521
       || pExcept->params.magicNumber == 0x19930522)
      && !pExcept->params.pThrowInfo )          
    {
      j__abort();                               
    }
    if ( j____vcrt_getptd()->_curexcspec )
    {
      pCurrentFuncInfo = (const _s_ESTypeList *)j____vcrt_getptd()->_curexcspec;
      j____vcrt_getptd()->_curexcspec = 0;
      if ( !IsInExceptionSpec(pExcept, pCurrentFuncInfo) )
      {
        if ( Is_bad_exception_allowed(pCurrentFuncInfo) )
        {
          j____DestructExceptionObject(pExcept, 1u);
          std::bad_exception::bad_exception(&pExceptionObject);
          _CxxThrowException(&pExceptionObject, &_TI2_AVbad_exception_std__);
        }
        j__terminate();
      }
    }
  }

构造一个 TryBlockMap 类记录 FuncInfo 地址。之后再判断异常类型,不过由于此时满足条件的异常的 pThrowInfo 一定不为空,所以不需要判断。

void __thiscall __FrameHandler3::TryBlockMap::TryBlockMap(
        __FrameHandler3::TryBlockMap *this,
        const _s_FuncInfo *pFuncInfo,
        unsigned int imageBase)
{
  this->_pFuncInfo = pFuncInfo;
  this->_imageBase = imageBase;
}

__FrameHandler3::TryBlockMap::TryBlockMap(&tryBlockMap, pFuncInfo, 0);
  if ( pExcept->ExceptionCode == 0xE06D7363
    && pExcept->NumberParameters == 3
    && (pExcept->params.magicNumber == 0x19930520
     || pExcept->params.magicNumber == 0x19930521
     || pExcept->params.magicNumber == 0x19930522) )
     ...

遍历 try 块,找到满足条件的 try 块调用 CatchIt___FrameHandler3_ 函数。该函数两层循环,外层循环遍历 try 块,内层遍历 try 块对应的 catch 块。注意这里看似是遍历所有 try 块,但实际上 CatchIt___FrameHandler3_ 函数不会返回(如果处理函数正常写),因此只会匹配一个处理函数。

    if ( __FrameHandler3::TryBlockMap::getNumTryBlocks(&tryBlockMap) )
    {
      __FrameHandler3::GetRangeOfTrysToCheck(&startStop, &tryBlockMap, curState, pDC, pFuncInfo, CatchDepth);
      for ( iter = startStop.first;
            __FrameHandler3::TryBlockMap::iterator::operator<(&iter, &startStop.second);
            __FrameHandler3::TryBlockMap::iterator::operator++(&iter) )
      {
        __FrameHandler3::TryBlockMap::iterator::operator*(&iter, &tryBlock);
        if ( tryBlock.tryLow <= curState && curState <= tryBlock.tryHigh )
        {
          __FrameHandler3::HandlerMap::HandlerMap(&handlerMap, &tryBlock, 0, 0);
          p_handlerMap = &handlerMap;
          __FrameHandler3::HandlerMap::begin(&handlerMap, &result);
          __FrameHandler3::HandlerMap::end(p_handlerMap, &other);
          while ( __FrameHandler3::HandlerMap::iterator::operator!=(&result, &other) )
          {
            __FrameHandler3::HandlerMap::iterator::operator*(&result, &handler);
            ppCatchable = pExcept->params.pThrowInfo->pCatchableTypeArray->arrayOfCatchableTypes;
            catchables = pExcept->params.pThrowInfo->pCatchableTypeArray->nCatchableTypes;
            while ( catchables > 0 )
            {
              pCatchable = *ppCatchable;
              if ( __FrameHandler3::TypeMatch(&handler, pCatchable, pExcept->params.pThrowInfo) )
              {
                CatchIt___FrameHandler3_(
                  pExcept,
                  pRN,
                  pContext,
                  pDC,
                  pFuncInfo,
                  &handler,
                  pCatchable,
                  &tryBlock,
                  CatchDepth,
                  pMarkerRN);
                goto LABEL_32;
              }
              --catchables;
              ++ppCatchable;
            }
            __FrameHandler3::HandlerMap::iterator::operator++(&result);
          }
        }
LABEL_32:
        ;
      }
    }

CatchIt___FrameHandler3_ 函数通过调用 CallCatchBlock 函数来调用 try 块对应的异常处理函数,之后调用_JumpToContinuation 函数,之后调用 _JumpToContinuation 函数返回用户代码继续执行。

void __cdecl CatchIt___FrameHandler3_(
        EHExceptionRecord *pExcept,
        EHRegistrationNode *pRN,
        _CONTEXT *pContext,
        void *pDC,
        const _s_FuncInfo *pFuncInfo,
        const _s_HandlerType *pCatch,
        const _s_CatchableType *pConv,
        const _s_TryBlockMapEntry *pEntry,
        int CatchDepth,
        EHRegistrationNode *pMarkerRN)
{
  void *continuationAddress; // [esp+0h] [ebp-8h]

  if ( pConv )
    __FrameHandler3::BuildCatchObject(pExcept, pRN, pCatch, pConv);
  if ( pMarkerRN )
    _UnwindNestedFrames(pMarkerRN, pExcept);
  else
    _UnwindNestedFrames(pRN, pExcept);
  __FrameHandler3::FrameUnwindToState(pRN, pDC, pFuncInfo, pEntry->tryLow);
  __FrameHandler3::SetState(pRN, pFuncInfo, pEntry->tryHigh + 1);
  continuationAddress = CallCatchBlock(pExcept, pRN, pContext, pFuncInfo, pCatch->addressOfHandler, CatchDepth, 0x100u);
  if ( continuationAddress )
    _JumpToContinuation(continuationAddress, pRN);
}

CallCatchBlock 函数有如下调用链:

CallCatchBlock(pExcept, pRN, pContext, pFuncInfo, pCatch->addressOfHandler, CatchDepth, 0x100u);
	_CallCatchBlock2(pRN, pFuncInfo, handlerAddress, CatchDepth, NLGCode);
		_CallSettingFrame((unsigned int)handlerAddress, (unsigned int)pRN, NLGCode);

_CallSettingFrame 函数中有如下汇编代码,也就是说 _CallSettingFrame 函数会直接调用异常处理函数。而通过分析汇编可知,CallCatchBlock 函数的返回值为异常处理函数的返回值。

.text:00460AD1 mov     eax, [ebp+handlerAddress]
...
.text:00460AE5 call    eax

JumpToContinuation 函数只有一行有效汇编,也就是说 _JumpToContinuation 会跳转到异常处理函数的返回值继续执行。

void __stdcall _JumpToContinuation(void *target, EHRegistrationNode *pRN)
{
  __asm { jmp     eax }
}

异常处理

异常处理函数最好通过 FuncInfo->TryBlockMap->HandlerArray->addressOfHandler 来寻找,因为有些版本编译器不会把异常处理函数编译到对应函数中。或者 IDA 无法把异常处理函数归属到某个函数中。

示例代码 main 函数中有两个异常处理函数,分别是 __catch$_main$0__catch$_main$1

.rdata:0052991C HandlerType _s_HandlerType <0, offset ??_R0H@8, 0FFFFFFECh, offset __catch$_main$0>
.rdata:0052992C _s_HandlerType <0, offset ??_R0M@8, 0FFFFFFE8h, offset __catch$_main$1>

__catch$_main$0 函数为例,汇编代码如下。异常处理函数在执行完核心逻辑后会在 eax 寄存器写恢复执行的地址,根据对异常派发的分析可知,在执行完异常处理函数后紧接着会跳转到 eax 也就是恢复执行的地址继续执行。

.text:0045A91B __catch$_main$1:                        ; DATA XREF: .rdata:0052992C↓o
.text:0045A91B push    offset asc_506E64               ; " "
.text:0045A920 call    j__printf
.text:0045A925 add     esp, 4
.text:0045A928 mov     eax, offset $LN10
.text:0045A92D retn

$LN10 在将 EHRegistrationNode.state 置为 -1 后跳转到 main 函数的结尾,卸载完 SEH 列表后返回。

.text:0045A937 $LN10:                                  
.text:0045A937 mov     [ebp+state], 0FFFFFFFFh
.text:0045A93E jmp     short loc_45A949
...
.text:0045A949 loc_45A949:
.text:0045A949 xor     eax, eax
.text:0045A94B mov     ecx, [ebp+var_C]
.text:0045A94E mov     large fs:0, ecx
.text:0045A955 pop     edi
.text:0045A956 pop     esi
.text:0045A957 pop     ebx
.text:0045A958 mov     esp, ebp
.text:0045A95A pop     ebp
.text:0045A95B retn

其他

缺省参数

例如下面这段代码:

void foo(int x = 1) {}

int main() {
    foo();
    return 0;
}

在关闭优化后调用 foo 函数的汇编如下:

    foo();
0045A863  push        1  
0045A865  call        foo (0455441h)  
0045A86A  add         esp,4 

因此得出结论:

  • 缺省函数无还原依据
  • 需要根据可读性还原

重载

函数重载

例如下面这段代码:

#include<stdio.h>

void foo(int x) {
    printf("%d\n", x);
}

void foo(long long x) {
    printf("%lld\n", x);
}

int main() {
    foo(1);
    foo(1LL);
    return 0;
}

实际反编译出来的 main 函数中分别调了两个不同的函数。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  sub_401000(1);
  sub_401020(1i64);
  return 0;
}

但是如果我们将这两个函数导出:

#include<stdio.h>

__declspec(dllexport) void foo(int x) {
    printf("%d\n", x);
}

__declspec(dllexport) void foo(long long x) {
    printf("%lld\n", x);
}

int main() {
    foo(1);
    foo(1LL);
    return 0;
}

因为导出表保存了导出符号信息,因此 IDA 能识别出函数名:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  foo(1);
  foo(1i64);
  return 0;
}

因此有如下结论:

  • 如果是导出或导入函数可以还原。
  • 否则没有还原依据,需要根据代码可读性还原。

运算符重载

重载赋值符号 =

class CObject {
public:
    CObject &operator=(int x) {
        this->x = x;
        return *this;
    }

private:
    int x;
};

int main() {
    CObject obj;
    obj = 1;
    return 0;
}

反编译代码如下,只能看出是(还原成)一个成员函数。

void __thiscall setX(CObject *this, int x)
{
  this->x = x;
}

int __cdecl main(int argc, const char **argv, const char **envp)
{
  CObject obj; // [esp+0h] [ebp-8h] BYREF

  setX(&obj, 1);
  return 0;
}

但是如果将类导出:

class __declspec(dllexport) CObject {
public:
    CObject &operator=(int x) {
        this->x = x;
        return *this;
    }

private:
    int x;
};

int main() {
    CObject obj;
    obj = 1;
    return 0;
}

那么成员函数就有符号可以识别。

CObject *__thiscall CObject::operator=(CObject *this, int x)
{
  this->x = x;
  return this;
}

int __cdecl main(int argc, const char **argv, const char **envp)
{
  CObject obj; // [esp+0h] [ebp-8h] BYREF

  CObject::operator=(&obj, 1);
  return 0;
}

因此有如下结论:

  • 如果是导入或导出类,则有还原依据。
  • 否则无还原依据,需要根据可读性还原。

lambda 表达式

首先是函数内只使用参数:

int main() {
    auto foo = [](int x, int y) {
        return x + y;
    };
    foo(1, 2);
}

调用方式是 __thiscall 不过由于 this 指针在函数内部没有用到,因此本质相当于 __stdcall 的函数调用。

push    2
push    1
lea     ecx, [ebp+var_5]
call    sub_401000

当函数使用局部变量的时候:

int main() {
    int x = 1, y = 2;
    auto foo = [=]() {
        return x + y;
    };
    foo();
}

本质是将参数赋值到 Args 对象中,然后将 Args 对象指针传入 foo 函数。

struct Args
{
  int x;
  int y;
};

Args *__thiscall Args::Args(Args *this, int *x, int *y)
{
  this->x = x;
  this->y = y;
  return this;
}

int __thiscall foo(Args *this)
{
  return this->y + this->x;
}

int __cdecl main(int argc, const char **argv, const char **envp)
{
  Args this; // [esp+0h] [ebp-14h] BYREF
  int x; // [esp+8h] [ebp-Ch] BYREF
  int y; // [esp+Ch] [ebp-8h] BYREF

  x = 1;
  y = 2;
  Args::Args(&this, &x, &y);
  foo(&this);
  return 0;
}

当函数使用和修改局部变量的时候:

int main() {
    int x = 1, y = 2;
    auto foo = [&]() {
        x = 10, y = 20;
        return x + y;
    };
    foo();
}

此时 Args 对象中存储的不再是变量的值而是变量的地址。

struct Args
{
  int *x;
  int *y;
};

Args *__thiscall Args::Args(Args *this, int *x, int *y)
{
  this->x = x;
  this->y = y;
  return this;
}

int __thiscall foo(Args *this)
{
  *this->x = 10;
  *this->y = 20;
  return *this->y + *this->x;
}

int __cdecl main(int argc, const char **argv, const char **envp)
{
  Args this; // [esp+0h] [ebp-14h] BYREF
  int x; // [esp+8h] [ebp-Ch] BYREF
  int y; // [esp+Ch] [ebp-8h] BYREF

  x = 1;
  y = 2;
  Args::Args(&this, &x, &y);
  foo(&this);
  return 0;
}

如果在使用修改局部变量的同时还传递参数:

int main() {
    int x = 1, y = 2;
    auto foo = [&](int a,int b) {
        x = 10, y = 20;
        return x + y;
    };
    foo(3, 4);
}

观察反编译代码发现实际上传入的参数作为 foo 函数 __thiscallthis 指针外的参数。

struct Args
{
  int *x;
  int *y;
};

Args *__thiscall Args::Args(Args *this, int *a2, int *a3)
{
  this->x = a2;
  this->y = a3;
  return this;
}

int __thiscall foo(Args *this, int a, int b)
{
  *this->x = 10;
  *this->y = 20;
  return *this->y + *this->x;
}

int __cdecl main(int argc, const char **argv, const char **envp)
{
  Args this; // [esp+0h] [ebp-14h] BYREF
  int x; // [esp+8h] [ebp-Ch] BYREF
  int y; // [esp+Ch] [ebp-8h] BYREF

  x = 1;
  y = 2;
  Args::Args(&this, &x, &y);
  foo(&this, 3, 4);
  return 0;
}

综上有如下结论:

  • 可根据类 + 成员函数实现。
  • 根据可读性还原。

模板

以如下代码为例:

template<typename T>
T max(T x, T y) {
    return x > y ? x : y;
}

int main() {
    max(1, 2);
    max(1.0, 2.0);
    return 0;
}

反编译代码如下:

int __cdecl MaxInt(int x, int y)
{
  if ( x <= y )
    return y;
  else
    return x;
}

double __cdecl MaxDouble(double x, double y)
{
  if ( x <= y )
    return y;
  else
    return x;
}

int __cdecl main(int argc, const char **argv, const char **envp)
{
  MaxInt(1, 2);
  MaxDouble(1.0, 2.0);
  return 0;
}

有如下结论:

  • 无还原依据,需要根据代码可读性还原。
  • 无法做 sig 文件。例如 STL 库在没有符号的时候 IDA 很难识别,因为根据模板传入的类型和值的不同,生成的汇编的特征不同。

STL

  • 主要靠经验识别和还原。
  • 熟悉数据结构的内存结构。
  • 注意 xxx too long 等错误日志字符串,通过查找引用可以大致判断出 STL 代码。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_sky123_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值