当前位置: 首页 > news >正文

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

结构体

结构体对齐

设置结构体对齐值

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

内存结构

构造析构顺序

虚表

相关文章:

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

结构体 结构体对齐 设置结构体对齐值 方法1&#xff1a;在 Visual Studio 中可以在 项目属性 -> 配置属性 -> C/C -> 所有选项 -> 结构体成员对齐 中设置结构体对齐大小。方法2&#xff1a;使用 #pragma pack(对齐值) 来设置&#xff0c;不过要想单独设置一个结…...

医药行业安全生产信息化建设分享

随着科技的快速发展和全球化进程的推进&#xff0c;医药行业作为人类健康和安全的重要组成部分&#xff0c;面临着日益严峻的安全生产挑战。近年来&#xff0c;医药企业对于安全生产的需求越来越强烈&#xff0c;安全生产信息化建设成为了医药行业发展的重要趋势。本文将探讨医…...

C 语言简单入门

C 语言发展历史|标准 1972年&#xff0c;丹尼斯里奇&#xff08;Dennis Ritch&#xff09;和肯汤普逊&#xff08;Ken Tompson&#xff09;在贝尔实验室开发 UNIX 操作系统时基于 B 语言设计出 C 语言。 1987年&#xff0c;布莱恩柯林汉&#xff08;Brian Kernighan&#xff…...

Levels - UE5中的建模相关

一些日常的笔记&#xff1b; 可以使用Shapes面板建立基础模型&#xff1a; 可以在PolyModel中继续细分模型&#xff1a; UE5中的建模有PolyGroups概念&#xff0c;可以在Attributes面板中直接编辑&#xff1a; 使用GrpPnt方式可以直接用笔刷设定新的PolyGroups&#xff0c;这样…...

数据中心与数据仓库的区别

在数字化时代&#xff0c;数据已经成为企业竞争的核心资源&#xff0c;数据处理和数据管理也变得越来越重要。在数据处理方面&#xff0c;数据中台和数据仓库是两种常见的数据处理方式&#xff0c;它们有着不同的特点和适用场景。本文将从技术角度对数据中台和数据仓库的区别进…...

[2023.09.18]: Rust中类型转换在错误处理中的应用解析

随着项目的进展&#xff0c;关于Rust的故事又翻开了新的一页&#xff0c;今天来到了服务器端的开发场景&#xff0c;发现错误处理中的错误类型转换有必要分享一下。 Rust抽象出来了Result<T,E>&#xff0c;T是返回值的类型&#xff0c;E是错误类型。只要函数的返回值的类…...

前端工作日常

机缘 记录和遇到的问题作为记录 收获 收获代码提高和认知 日常 使用js去操作数组或者对象 空闲时间可以多学学基础算法 比如&#xff08;冒泡&#xff0c;倒序&#xff0c;去重&#xff0c;笛卡尔积算法&#xff0c;各种各样的排序方法等等等&#xff09; 正确良好的使用循环…...

C++:C++哪些时候用到const

声明常量&#xff1a;使用const关键字定义一个常量&#xff0c;不允许对其进行更改。例如&#xff1a; const int PI 3.1415926;修饰函数参数&#xff1a;加上const限定符可以确保函数不会修改传入的参数值。例如&#xff1a; void print(const int num) {// num不能在函数内…...

OpenCV之九宫格图像

将一张图像均等分成九份&#xff0c;然后将这九个小块按一定间隔&#xff08;九宫格效果&#xff09;拷贝到新画布上。效果如下图所示&#xff1a; 源码&#xff1a; #include<iostream> #include<opencv2/opencv.hpp> using namespace std; using namespace cv;i…...

OpenGLES:绘制一个颜色渐变的圆

一.概述 今天使用OpenGLES实现一个圆心是玫红色&#xff0c;向圆周渐变成蓝色的圆。 本篇博文的内容也是后续绘制3D图形的基础。 实现过程中&#xff0c;需要重点关注的点是&#xff1a;如何使用数学公式求得图形的顶点&#xff0c;以及加载颜色值。 废话不多说&#xff0c…...

javascript数据类型错误造成的前端分页不准的问题

有个react项目是自己写的mock后端api&#xff0c;使用的是json文件模拟DB, slice函数模拟分页&#xff0c;但是在实际分页时&#xff0c;发现了分页不准的问题&#xff0c;现象如下&#xff1a; 当pageSize为5的时候&#xff08;共16条数据&#xff09;&#xff0c;总共分4页&…...

[Qt]QListView 重绘实例之二:列表项覆盖的问题处理

0 环境 Windows 11Qt 5.15.2 MinGW x64 1 系列文章 简介&#xff1a;本系列文章&#xff0c;是以纯代码方式实现 Qt 控件的重构&#xff0c;尽量不使用 Qss 方式。 《[Qt]QListView 重绘实例之一&#xff1a;背景重绘》 《[Qt]QListView 重绘实例之二&#xff1a;列表项覆…...

Java 函数式编程思考 —— 授人以渔

引言 最近在使用函数式编程时&#xff0c;突然有了一点心得体会&#xff0c;简单说&#xff0c;用好了函数式编程&#xff0c;可以极大的实现方法调用的解耦&#xff0c;业务逻辑高度内聚&#xff0c;同时减少不必要的分支语句&#xff08;if-else&#xff09;。 一、函数式编…...

操作系统权限提升(二十八)之数据库提权-SQL Server 数据库安装

SQL Server 数据库安装 SQL Server介绍 SQL Server 是Microsoft 公司推出的关系型数据库管理系统。具有使用方便可伸缩性好与相关软件集成程度高等优点,可跨越从运行Microsoft Windows 98 的膝上型电脑到运行Microsoft Windows 2012 的大型多处理器的服务器等多种平台使用。…...

腾讯mini项目-【指标监控服务重构-会议记录】2023-08-18

2023-08-18 会议纪要 进度 venus 的 metrics 独立分支开发venus 的 trace 修复了一些bug 返回 error 主动调用 span.end() profile 的 watemill pub/sub 和 trace 上报还原原本功能profile 的 hyperscan 的继续调研 待办 调研如何关闭otel&#xff0c;设置开关配置性能benc…...

如何通过axios拦截器,给除了登录请求以外,axios的所有异步请求添加JWT令牌!

在 Vue 项目中配置除了登录请求以外的所有请求的令牌&#xff0c;通常涉及到在请求头中添加令牌&#xff08;Token&#xff09;信息。这可以通过使用 Axios 或其他 HTTP 请求库来实现。以下是一般的步骤&#xff1a; 1. **安装 Axios**&#xff1a; 如果你还没有安装 Axios&a…...

Spring学习笔记9 SpringIOC注解式开发

Spring学习笔记8 Bean的循环依赖问题_biubiubiu0706的博客-CSDN博客 注解的存在主要是为了简化XML的配置.Spring6倡导全注解式开发 回顾下 注解怎么定义,注解中的属性怎么定义 注解怎么使用 通过反射机制怎么读取注解 注解的自定义 注解的使用 通过反射机制怎么读取注解 I…...

【新日标习题集】第13課 までのまとめ (discarded)

2. 学校にコンピューターがごだいあります。 这个句子好像有点问题&#xff0c;辞典中没有查到有「ごだい」这个单词 学校里有5台电脑。 5. わたしは英語がよくわかります。 我很懂英语。...

Java基础常考知识点(基础、集合、异常、JVM)

作者&#xff1a;逍遥Sean 简介&#xff1a;一个主修Java的Web网站\游戏服务器后端开发者 主页&#xff1a;https://blog.csdn.net/Ureliable 觉得博主文章不错的话&#xff0c;可以三连支持一下~ 如有需要我的支持&#xff0c;请私信或评论留言&#xff01; Java基础常考知识点…...

虚拟机桥接模式下没有无线网卡选项

我以为是雷电模拟器占用了网卡的缘故&#xff0c;但想起之前可能修改了无线网卡的某些内容&#xff0c;于是到网络属性里面查看。 如下所示&#xff0c;原来是之前我不小心把这个红箭头指向的项目取消勾选了。...

Docker 离线安装指南

参考文章 1、确认操作系统类型及内核版本 Docker依赖于Linux内核的一些特性&#xff0c;不同版本的Docker对内核版本有不同要求。例如&#xff0c;Docker 17.06及之后的版本通常需要Linux内核3.10及以上版本&#xff0c;Docker17.09及更高版本对应Linux内核4.9.x及更高版本。…...

Linux 文件类型,目录与路径,文件与目录管理

文件类型 后面的字符表示文件类型标志 普通文件&#xff1a;-&#xff08;纯文本文件&#xff0c;二进制文件&#xff0c;数据格式文件&#xff09; 如文本文件、图片、程序文件等。 目录文件&#xff1a;d&#xff08;directory&#xff09; 用来存放其他文件或子目录。 设备…...

C++:std::is_convertible

C++标志库中提供is_convertible,可以测试一种类型是否可以转换为另一只类型: template <class From, class To> struct is_convertible; 使用举例: #include <iostream> #include <string>using namespace std;struct A { }; struct B : A { };int main…...

云启出海,智联未来|阿里云网络「企业出海」系列客户沙龙上海站圆满落地

借阿里云中企出海大会的东风&#xff0c;以**「云启出海&#xff0c;智联未来&#xff5c;打造安全可靠的出海云网络引擎」为主题的阿里云企业出海客户沙龙云网络&安全专场于5.28日下午在上海顺利举办&#xff0c;现场吸引了来自携程、小红书、米哈游、哔哩哔哩、波克城市、…...

dedecms 织梦自定义表单留言增加ajax验证码功能

增加ajax功能模块&#xff0c;用户不点击提交按钮&#xff0c;只要输入框失去焦点&#xff0c;就会提前提示验证码是否正确。 一&#xff0c;模板上增加验证码 <input name"vdcode"id"vdcode" placeholder"请输入验证码" type"text&quo…...

【SQL学习笔记1】增删改查+多表连接全解析(内附SQL免费在线练习工具)

可以使用Sqliteviz这个网站免费编写sql语句&#xff0c;它能够让用户直接在浏览器内练习SQL的语法&#xff0c;不需要安装任何软件。 链接如下&#xff1a; sqliteviz 注意&#xff1a; 在转写SQL语法时&#xff0c;关键字之间有一个特定的顺序&#xff0c;这个顺序会影响到…...

unix/linux,sudo,其发展历程详细时间线、由来、历史背景

sudo 的诞生和演化,本身就是一部 Unix/Linux 系统管理哲学变迁的微缩史。来,让我们拨开时间的迷雾,一同探寻 sudo 那波澜壮阔(也颇为实用主义)的发展历程。 历史背景:su的时代与困境 ( 20 世纪 70 年代 - 80 年代初) 在 sudo 出现之前,Unix 系统管理员和需要特权操作的…...

土地利用/土地覆盖遥感解译与基于CLUE模型未来变化情景预测;从基础到高级,涵盖ArcGIS数据处理、ENVI遥感解译与CLUE模型情景模拟等

&#x1f50d; 土地利用/土地覆盖数据是生态、环境和气象等诸多领域模型的关键输入参数。通过遥感影像解译技术&#xff0c;可以精准获取历史或当前任何一个区域的土地利用/土地覆盖情况。这些数据不仅能够用于评估区域生态环境的变化趋势&#xff0c;还能有效评价重大生态工程…...

Maven 概述、安装、配置、仓库、私服详解

目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...

从面试角度回答Android中ContentProvider启动原理

Android中ContentProvider原理的面试角度解析&#xff0c;分为​​已启动​​和​​未启动​​两种场景&#xff1a; 一、ContentProvider已启动的情况 1. ​​核心流程​​ ​​触发条件​​&#xff1a;当其他组件&#xff08;如Activity、Service&#xff09;通过ContentR…...