结构体
结构体对齐
设置结构体对齐值
- 方法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(1≤i≤n) 并且按照 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 则为显式调用析构函数,因此直接跳过 delete 。
008211D7 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 指针是否为空,如果为空则跳过析构和 delete。
002F10BA 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 指针为空,因此将析构函数执行结果置为 0 。
return 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...catch
(catch
有数据类型)会强制开启 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);
}
内存结构
派生类的虚表填充过程:
- 复制基类的虚表(函数顺序不变)。
- 如果派生类虚函数中有覆盖基类的虚函数(与基类的对应函数同名同参),使用派生类的虚函数地址覆盖对应表项。
- 如果派生类有新增的虚函数,将其放在虚表后面。
派生类的对象填充过程:
- 虚表指针指向派生类对应的虚表。
- 将派生类新增的成员放到基类的成员后面。
因此示例代码中的 CBase
和 CDerived
类的实例化的对象和虚表结构如下:
虚表函数重合是还原类继承关系的一个重要依据。
多重继承
这里使用如下代码来介绍多重继承:
#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;
}