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

news2024/11/23 21:25:47

结构体

结构体对齐

设置结构体对齐值

  • 方法1:在 Visual Studio 中可以在 项目属性 -> 配置属性 -> C/C++ -> 所有选项 -> 结构体成员对齐 中设置结构体对齐大小。
  • 方法2:使用 #pragma pack(对齐值) 来设置,不过要想单独设置一个结构体的对齐大小需要保存和恢复原先的结构体对齐值。
    #pragma pack(push)  // 保存原先的结构体对齐值
    #pragma pack(2)     // 设置结构体对齐值为 2
    struct Struct {     // sizeof(Struct) = 6
        char x;
        int y;
    };
    #pragma pack(pop)   // 恢复原先的结构体对齐值
    
  • 方法3:在 C++11 及以后标准中,可使用 alians 关键字设置结构体的对齐值。不过请注意,alignas 关键字的参数必须是常量表达式,对齐值必须是 2 的幂且不能小于结构体中最大的成员。
    struct alignas(32) Struct {     // sizeof(Struct) = 32
        char x;
        int y;
    };
    

结构体对齐策略

假设一个结构体中有 n n n 个元素,每个元素大小为 a i ( 1 ≤ i ≤ n ) a_i(1\le i\le n) ai(1in) 并且按照 k k k 字节对齐,则结构体大小计算方式如下:

#include <bits/stdc++.h>

int main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);

    int n, k;
    std::cin >> n >> k;
    assert(__builtin_popcount(k) == 1);

    std::vector<int> a(n);
    for (int i = 0; i < n; i++) {
        std::cin >> a[i];
        assert(__builtin_popcount(a[i]) == 1);
    }

    k = std::min(k, *std::max_element(a.begin(), a.end()));

    int ans = 0;
    for (int i = 0; i < n; i++) {
        if ((ans + a[i] - 1) / a[i] * a[i] + a[i] <= (ans + k - 1) / k * k) {
            ans = (ans + a[i] - 1) / a[i] * a[i] + a[i];
        } else {
            ans = (ans + k - 1) / k * k + a[i];
        }
    }

    ans = (ans + k - 1) / k * k;

    std::cout << ans << std::endl;

    return 0;
}

注意以下特殊情况:

  • 如果是 alignas 设置的对齐值则结构体严格按照对齐值对齐(IDA 中设置的结构体 align 属性也按照这个规则对齐结构体),否则按照对齐值和结构体最大成员中最小的那个进行对齐。
  • 如果是结构体套结构体则内部的结构体的成员需要看做是外部结构体的成员进行对齐,而不是内部的结构体整个参与到结构体对齐中去。

结构体的识别

通常采用 [base + offset] 的方式访问结构体成员。

  • 如果结构体中成员大小相同则结构体初始化代码等价与数组的初始化代码,无法区分。
  • 如果结构体中成员的大小或类型(整型与浮点数)不同会造成结构体成员在内存中不连续或者访问的汇编指令不同,可以识别出结构体。
  • 如果采用 [esp + xxx] 或者 [ebp - xxx] 访问则不能区分是结构体还是多个局部变量。

结构体拷贝

如果结构体比较小则利用寄存器进行拷贝。

    Struct b = *a;
006B186C  mov         eax,dword ptr [a]  
006B186F  mov         ecx,dword ptr [eax]  
006B1871  mov         dword ptr [b],ecx  
006B1874  mov         edx,dword ptr [eax+4]  
006B1877  mov         dword ptr [ebp-18h],edx	; [ebp - 18h][b + 4] 
006B187A  mov         eax,dword ptr [eax+8]  
006B187D  mov         dword ptr [ebp-14h],eax	; [ebp - 14h][b + 8] 

如果结构体比较大则优化为 rep 指令。

    Struct b = *a;
00F8186C  mov         ecx,0Ch  
00F81871  mov         esi,dword ptr [a]  
00F81874  lea         edi,[b]  
00F81877  rep movs    dword ptr es:[edi],dword ptr [esi]

结构体传参

例如下面这个代码:

#include <iostream>

struct Struct {
    int x;
    int y;
};

void foo(Struct a) {
    printf("%d %d\n", a.x, a.y);
}

int main() {
    Struct a;
    scanf_s("%d%d", &a.x, &a.y);
    foo(a);
}

在结构体成员比较少的情况下调用 foo 函数时会依次将结构体成员 push 到栈上。类似于函数正常传参。

    foo(a);
007C45E4  mov         eax,dword ptr [ebp-0Ch]	; [ebp - 0Ch][a + 4]
007C45E7  push        eax  
007C45E8  mov         ecx,dword ptr [a]  
007C45EB  push        ecx  
007C45EC  call        foo (07C13CFh)  
007C45F1  add         esp,8  

Struct 修改为如下定义:

struct Struct {
    int x;
    int y;
    int z[10];
};

foo 函数通过 rep 指令栈拷贝传参,而如果是数组传参则会传数组的地址,这是区分数组和结构体的一个依据。

    foo(a);
005345E4  sub         esp,30h  
005345E7  mov         ecx,0Ch  
005345EC  lea         esi,[a]  
005345EF  mov         edi,esp  
005345F1  rep movs    dword ptr es:[edi],dword ptr [esi]  
005345F3  call        foo (05313CFh)  
005345F8  add         esp,30h  

如果传入的参数是结构体引用或是结构体指针,则于数组参数一样传的是结构体的地址,这样就只能根据函数中对结构体成员访问来判断参数类型是否是结构体。

    foo(a); // a 是一个结构体引用
006017F8  lea         eax,[a]  
006017FB  push        eax  
006017FC  call        foo (060105Fh)  
00601801  add         esp,4  

结构体返回值

首先让结构体只有一个成员变量:

#include <iostream>

struct Struct {
    int x;
};

Struct bar() {
    Struct a;
    printf("%d\n", a.x);
    return a;
}

int main() {
    Struct a = bar();
    printf("%d\n", a.x);
    return 0;
}

此时会将结构体存放在 eax 寄存器中返回。

    Struct a = bar();
00AC1B93  call        bar (0AC10D2h)  
00AC1B98  mov         dword ptr [ebp-48h],eax  
00AC1B9B  mov         eax,dword ptr [ebp-48h]  
00AC1B9E  mov         dword ptr [a],eax  

将结构体中添加一个成员变量 y

struct Struct {
    int x, y;
};

此时返回值结构体中的两个成员变量分别使用 eax 和 edx 寄存器存储。这与 32 位下返回 64 位变量相似。

    Struct a = bar();
009A1B93  call        bar (09A10D2h)  
009A1B98  mov         dword ptr [ebp-50h],eax  
009A1B9B  mov         dword ptr [ebp-4Ch],edx  
009A1B9E  mov         eax,dword ptr [ebp-50h]  
009A1BA1  mov         ecx,dword ptr [ebp-4Ch]  
009A1BA4  mov         dword ptr [a],eax  
009A1BA7  mov         dword ptr [ebp-4],ecx  

因此结构体大小不超过 8 字节的时候采用值返回。

将结构体中再添加一个成员变量 z

struct Struct {
    int x, y, z;
};

此时不再使用寄存器存返回值,而是向函数中传一个 ebp - 0x24 的地址作为参数。

bar 函数返回后先将返回值 eax 指向的 12 字节内存拷贝到 ebp - 0x0C 处的内存,之后再将 ebp - 0x0C 处的内存拷贝到 ebp -0x18 也就是局部变量 b 所在的内存。

    Struct b = bar();
.text:00401146 lea     eax, [ebp+a]
.text:00401149 push    eax                             ; a
.text:0040114A call    ?bar@@YA?AUStruct@@XZ           ; bar(void)
.text:0040114A
.text:0040114F add     esp, 4
.text:00401152 mov     ecx, [eax+Struct.x]
.text:00401154 mov     [ebp+temp.x], ecx
.text:00401157 mov     edx, [eax+Struct.y]
.text:0040115A mov     [ebp+temp.y], edx
.text:0040115D mov     eax, [eax+Struct.z]
.text:00401160 mov     [ebp+temp.z], eax
.text:00401163 mov     ecx, [ebp+temp.x]
.text:00401166 mov     [ebp+b.x], ecx
.text:00401169 mov     edx, [ebp+temp.y]
.text:0040116C mov     [ebp+b.y], edx
.text:0040116F mov     eax, [ebp+temp.z]
.text:00401172 mov     [ebp+b.z], eax

传入的参数在 bar 函数中被看做是局部变量 a

Struct *__cdecl bar(Struct *a)
{
  a->x = 1;
  a->y = 0;
  a->z = 0;
  scanf_s("%d%d%d\n", a, &a->y, &a->z);
  return a;
}

因此整个过程中发生 2 次结构体拷贝。
在这里插入图片描述

如果 bar 函数本身还传参,则结构体(局部变量 a)地址作为第一个参数。

    Struct b = bar(x);
00761161  mov         ecx,dword ptr [x]  
00761164  push        ecx  
00761165  lea         edx,[ebp-2Ch]  
00761168  push        edx  
00761169  call        bar (0761100h)  
0076116E  add         esp,8  

将结构体定义的再大一些,此时同样会发生 2 次拷贝,不过会使用 rep 指令进行优化。

    Struct b = bar();
00A8114B  lea         eax,[ebp-90h]  
00A81151  push        eax  
00A81152  call        bar (0A81100h)  
00A81157  add         esp,4  
00A8115A  mov         ecx,0Ch  
00A8115F  mov         esi,eax  
00A81161  lea         edi,[ebp-30h]  
00A81164  rep movs    dword ptr es:[edi],dword ptr [esi]  
00A81166  mov         ecx,0Ch  
00A8116B  lea         esi,[ebp-30h]  
00A8116E  lea         edi,[b]  
00A81171  rep movs    dword ptr es:[edi],dword ptr [esi] 

成员函数

普通成员函数

调用约定是 __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]  
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;
  }
}

对于全局对象数组,会在 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);
}

对象的传递

对象作为参数

指针\引用对象传参

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

    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 函数。

    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];
}

由于 __thiscall 是内平栈,因此调用完拷贝构造函数之后栈顶就是已经完成初始化的对象参数,作为下一步调用的 foo 函数的参数。

同样在 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 不能识别虚表符号。

虚函数

虚函数(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;
}

内存结构

构造析构顺序

虚表

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1038246.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2023 “华为杯” 中国研究生数学建模竞赛(D题)深度剖析|数学建模完整代码+建模过程全解全析

问题一&#xff1a;区域碳排放量以及经济、人口、能源消费量的现状分析 思路&#xff1a; 定义碳排放量 Prediction 模型: CO2 P * (GDP/P) * (E/GDP) * (CO2/E) 其中: CO2:碳排放量 P:人口数量 GDP/P:人均GDP E/GDP:单位GDP能耗 CO2/E:单位能耗碳排放量 2.收集并统计相关…

vuereact质检工具(eslint)安装使用总结

1、ESLint ESLint工具主要类似java中的checkStyle和findbugs&#xff0c;是检查代码样式和逻辑规范的工具。 1.1、ESLint安装流程 打开VSCode软件&#xff0c;打开扩展中心&#xff0c;下载ESLint插件 图1.1 点击后面的install按进行安装&#xff0c;如图1.2所示&#xff1…

MySQL(8) 优化、MySQL8、常用命令

一、MySQL优化 从上图可以看出SQL及索引的优化效果是最好的&#xff0c;而且成本最低&#xff0c;所以工作中我们要在这块花更多时间。 服务端参数配置&#xff1b; max_connections3000 连接的创建和销毁都需要系统资源&#xff0c;比如内存、文件句柄&#xff0c;业务说的支持…

代码随想录算法训练营第四十二天| 01背包问题,你该了解这些! 01背包问题,你该了解这些! 滚动数组 416. 分割等和子集

正式开始背包问题&#xff0c;背包问题还是挺难的&#xff0c;虽然大家可能看了很多背包问题模板代码&#xff0c;感觉挺简单&#xff0c;但基本理解的都不够深入。 如果是直接从来没听过背包问题&#xff0c;可以先看文字讲解慢慢了解 这是干什么的。 如果做过背包类问题&…

网络安全深入学习第七课——热门框架漏洞(RCE— Fastjson反序列化漏洞)

文章目录 一、什么是json?二、Fastjson介绍三、Fastjson-历史漏洞四、Fastjson特征1、在请求包里面有json格式的2、报错信息中会显示fastjson字眼 五、Fastjson序列化和反序列化六、Fastjson反序列化漏洞原理七、Fastjson反序列化漏洞过程八、Fastjson反序列化漏洞&#xff08…

农林种植类VR虚拟仿真实验教学整体解决方案

传统的葡萄嫁接需要在固定月份&#xff0c;实操成本高&#xff0c;管理周期长&#xff0c;葡萄嫁接VR虚拟仿真实训是VR虚拟仿真公司深圳华锐视点通过在虚拟环境中模拟葡萄嫁接过程&#xff0c;融入教学和实训考核多种模式&#xff0c;打造了全新的职业技能培训方式。 葡萄嫁接V…

【Java实战项目】【超详细过程】—— 大饼的图片服务器6

目录 前言一、引入MD51.引入md5计算依赖2.按照md5值查找图片属性3.存储图片4.删除图片 二、防盗链三、分类查看图片1.思路&#xff1a;2.数据库3.Image4.from表单5.ImageDao类中原有方法6.按照类型sort在数据库中查找图片属性7.ImageServlet 类8.ImageSortServlet类9.WEB.xml绑…

DevExpress WinForms图表组件 - 直观的数据信息呈现新方式!(一)

凭借界面控件DevExpress WinForms全面的2D和3D图表类型的集合&#xff0c;DevExpress WinForms的图表控件设计大大简化了开发者直观地向最终用户呈现信息的方式。 DevExpress WinForms有180组件和UI库&#xff0c;能为Windows Forms平台创建具有影响力的业务解决方案。同时能完…

[python 刷题] 739 Daily Temperatures

[python 刷题] 739 Daily Temperatures 题目&#xff1a; Given an array of integers temperatures represents the daily temperatures, return an array answer such that answer[i] is the number of days you have to wait after the ith day to get a warmer temperatur…

记一次实战案例

1、目标&#xff1a;inurl:news.php?id URL&#xff1a;https://www.lghk.com/news.php?id5 网站标题&#xff1a;趋时珠宝首饰有限公司 手工基础判断&#xff1a; And用法 and 11: 这个条件始终是为真的, 也就是说, 存在SQL注入的话, 这个and 11的返回结果必定是和正常页…

修改vscode底部栏背景和字体颜色

修改vscode底部栏背景和字体颜色 如图&#xff1a; 首先打开齿轮&#xff0c;打开设置搜索workbench.colorCustomizations,然后点击编辑setting.json修改setting.json内内容 "workbench.colorCustomizations": {"statusBar.foreground": "#FFFFFF…

为什么u盘在mac上显示不出来

插入U盘是个看似简单的操作&#xff0c;但有时候在Mac电脑上却出现了无法显示U盘的情况。这样的问题是非常让人头疼的&#xff0c;特别是当你急需使用U盘中的文件时。那么&#xff0c;究竟为什么U盘在Mac上会显示不出来呢&#xff1f;今天就让我们一起来深入了解一下这个问题&a…

OR54 字符串中找出连续最长的数字串

目录 一、题目 二、解答 &#xff08;一&#xff09;问题一&#xff1a;在记录完一组连续字符串后&#xff0c;没有注意判别紧随其后的非数字字符 &#xff08;二&#xff09;问题二&#xff1a;越界访问 &#xff08;三&#xff09;正确 一、题目 字符串中找出连续最长的…

powerDesigner 的基本使用

打开powerDesigner 新建 PDM(物理数据模型) 添加表字段 双击表&#xff0c;设置ID自增 选择导出数据库表SQL 导出成功 使用三方工具连接数据库&#xff0c;然后运行对应SQL文件即可 导入SQL文件数据到powerDesigner

Ae 效果:CC Tiler

扭曲/CC Tiler Distort/CC Tiler CC Tiler &#xff08;CC 平铺器&#xff09;主要用于将图像以平铺的方式重复显示&#xff0c;可以创建有趣的复制和平铺的视觉效果。 平铺的范围限制在图层大小。如果想在合成大小内进行平铺&#xff0c;最简单的方法是先将源图像&#xff08;…

【数据结构】逻辑结构与物理结构

&#x1f984;个人主页:修修修也 &#x1f38f;所属专栏:数据结构 ⚙️操作环境:Visual Studio 2022 根据视点的不同,我们把数据结构分为逻辑结构和物理结构. &#x1f333;逻辑结构 逻辑结构:是指数据对象中数据元素之间的相互关系. 逻辑结构分为以下四种: 1.集合结构 集合结…

计算机中实数的比较

计算机中实数的比较 最近被问到了实数与0为什么不能直接比较的问题&#xff0c;要想说清楚还真不容易。 这里从浮点数的表示、内存存储角度加以总结。 科学计数法 科学计数法表示十进制数的浮点数遵循了小数点前面只有一个数&#xff08;1~9&#xff09;的规则。 例如&…

python+nodejs+php+springboot+vue 法律知识分享科普系统平台

在设计过程中&#xff0c;充分保证了系统代码的良好可读性、实用性、易扩展性、通用性、便于后期维护、操作方便以及页面简洁等特点。 随着社会的发展&#xff0c;社会的各行各业都在利用信息化时代的优势。计算机的优势和普及使得各种信息系统的开发成为必需。 要想实现法律知…

93 # 实现 express 错误处理中间件

上一节实现了 express 的中间件&#xff0c;这一节来实现错误处理中间件 执行某一步出错了&#xff0c;统一规定调用 next 传递的参数就是错误信息 先看 express 实现的demo const express require("express"); const app express();app.use("/", (re…

2023-09-23 Windows系统rust开发环境配置真经

Windows系统rust开发环境配置真经 前言一、配置C编译链和VsCode二、安装rust编译工具三、配置VsCode一. 安装rust-analyzer插件二. 安装Error Lens插件三. 安装Even Better TOML插件四. 配置 launch.json五. 配置 tasks.json六. 配置 Cargo.toml 总结 前言 有了配置C语言环境的…