由多重继承导致的内存释放错误
由多重继承导致的内存释放错误
问题提出
C++中的delete expression或者(delete operator)在多继承下,如果使用不正确的话,可能存在程序崩溃的情况。如下代码所示:
// main.cpp
class Base1
{
int mem_b1;
public:
Base1() : mem_b1(0) {}
~Base1() {}
};
class Base2
{
int mem_b2;
public:
Base2() : mem_b2(0) {}
~Base2() {}
};
class Derived : public Base1, public Base2
{
int mem_d;
public:
Derived() : mem_d(0) {}
~Derived() {}
};
int main()
{
Base2 *b2 = new Derived();
delete b2;
}
上面的代码在delete b2;
就会报错,错误信息如下
错误原因
错误原因是因为对象上转型的过程中指针向后移动了,而析构的时候没有将指针移回来,由于创建动态内存的指针和释放动态内存的指针不相同,而导致无效指针错误
main()
.text:00000001400015B0 push rdi
.text:00000001400015B2 sub rsp, 60h
.text:00000001400015B6 mov ecx, 0Ch ; size
.text:00000001400015BB call j_??2@YAPEAX_K@Z ; operator new(unsigned __int64)
.text:00000001400015C0 mov [rsp+68h+var_38], rax
.text:00000001400015C5 cmp [rsp+68h+var_38], 0
.text:00000001400015CB jz short loc_1400015DE
.text:00000001400015CD mov rcx, [rsp+68h+var_38] ; this
.text:00000001400015D2 call j_??0Derived@@QEAA@XZ ; Derived::Derived(void)
.text:00000001400015D7 mov [rsp+68h+var_28], rax
.text:00000001400015DC jmp short loc_1400015E7
.text:00000001400015DE ; ---------------------------------------------------------------------------
.text:00000001400015DE
.text:00000001400015DE loc_1400015DE: ; CODE XREF: main+1B↑j
.text:00000001400015DE mov [rsp+68h+var_28], 0
.text:00000001400015E7
.text:00000001400015E7 loc_1400015E7: ; CODE XREF: main+2C↑j
.text:00000001400015E7 mov rax, [rsp+68h+var_28]
.text:00000001400015EC mov [rsp+68h+var_40], rax
.text:00000001400015F1 cmp [rsp+68h+var_40], 0
.text:00000001400015F7 jz short loc_140001609
.text:00000001400015F9 mov rax, [rsp+68h+var_40]
.text:00000001400015FE add rax, 4 //指针向后调整4个位置
.text:0000000140001602 mov [rsp+68h+var_20], rax
.text:0000000140001607 jmp short loc_140001612
.text:0000000140001609 ; ---------------------------------------------------------------------------
.text:0000000140001609
.text:0000000140001609 loc_140001609: ; CODE XREF: main+47↑j
.text:0000000140001609 mov [rsp+68h+var_20], 0
.text:0000000140001612
.text:0000000140001612 loc_140001612: ; CODE XREF: main+57↑j
.text:0000000140001612 mov rax, [rsp+68h+var_20]
.text:0000000140001617 mov [rsp+68h+var_48], rax
.text:000000014000161C mov rax, [rsp+68h+var_48]
.text:0000000140001621 mov [rsp+68h+var_30], rax
.text:0000000140001626 cmp [rsp+68h+var_30], 0
.text:000000014000162C jz short loc_140001644
.text:000000014000162E mov edx, 1 ; unsigned int
.text:0000000140001633 mov rcx, [rsp+68h+var_30] ; this
.text:0000000140001638 call j_??_GBase2@@QEAAPEAXI@Z ; Base2::`scalar deleting destructor'(uint)
.text:000000014000163D mov [rsp+68h+var_18], rax
.text:0000000140001642 jmp short loc_14000164D
.text:0000000140001644 ; ---------------------------------------------------------------------------
.text:0000000140001644
.text:0000000140001644 loc_140001644: ; CODE XREF: main+7C↑j
.text:0000000140001644 mov [rsp+68h+var_18], 0
.text:000000014000164D
.text:000000014000164D loc_14000164D: ; CODE XREF: main+92↑j
.text:000000014000164D xor eax, eax
.text:000000014000164F add rsp, 60h
.text:0000000140001653 pop rdi
.text:0000000140001654 retn
Base2::`scalar deleting destructor'(uint)
.text:0000000140001730 arg_0 = qword ptr 8
.text:0000000140001730 arg_8 = dword ptr 10h
.text:0000000140001730
.text:0000000140001730 mov [rsp+arg_8], edx
.text:0000000140001734 mov [rsp+arg_0], rcx
.text:0000000140001739 push rdi
.text:000000014000173A sub rsp, 20h
.text:000000014000173E mov rcx, [rsp+28h+arg_0] ; this
.text:0000000140001743 call j_??1Base2@@QEAA@XZ ; Base2::~Base2(void)
.text:0000000140001748 mov eax, [rsp+28h+arg_8]
.text:000000014000174C and eax, 1
.text:000000014000174F test eax, eax
.text:0000000140001751 jz short loc_140001762
.text:0000000140001753 mov edx, 4 ; __formal
.text:0000000140001758 mov rcx, [rsp+28h+arg_0] ; block
.text:000000014000175D call j_??3@YAXPEAX_K@Z ; operator delete(void *,unsigned __int64)
.text:0000000140001762
.text:0000000140001762 loc_140001762: ; CODE XREF: Base2::`scalar deleting destructor'(uint)+21↑j
.text:0000000140001762 mov rax, [rsp+28h+arg_0]
.text:0000000140001767 add rsp, 20h
.text:000000014000176B pop rdi
.text:000000014000176C retn
如上所示,在main函数中,在对象上转型的过程中,指针向后移动了4,但是在Base2::scalar deleting destructor(uint)
却没有调整回来,直接调用了delete函数,导致因创建和释放内存的位置不一样而错误
解决办法
将所有析构函数都变成虚函数。
为什么这样可以解决问题呢,我们加上virtual 后再看看汇编代码
main()
.text:0000000140001600 ; __unwind { // __CxxFrameHandler4_0
.text:0000000140001600 push rdi
.text:0000000140001602 sub rsp, 60h
.text:0000000140001606 mov ecx, 28h ; '(' ; size
.text:000000014000160B call j_??2@YAPEAX_K@Z ; operator new(unsigned __int64)
.text:0000000140001610 mov [rsp+68h+var_38], rax
.text:0000000140001615 cmp [rsp+68h+var_38], 0
.text:000000014000161B jz short loc_14000162E
.text:000000014000161D mov rcx, [rsp+68h+var_38] ; this
.text:0000000140001622 call j_??0Derived@@QEAA@XZ ; Derived::Derived(void)
.text:0000000140001627 mov [rsp+68h+var_28], rax
.text:000000014000162C jmp short loc_140001637
.text:000000014000162E ; ---------------------------------------------------------------------------
.text:000000014000162E
.text:000000014000162E loc_14000162E: ; CODE XREF: main+1B↑j
.text:000000014000162E mov [rsp+68h+var_28], 0
.text:0000000140001637
.text:0000000140001637 loc_140001637: ; CODE XREF: main+2C↑j
.text:0000000140001637 mov rax, [rsp+68h+var_28]
.text:000000014000163C mov [rsp+68h+var_40], rax
.text:0000000140001641 cmp [rsp+68h+var_40], 0
.text:0000000140001647 jz short loc_140001659
.text:0000000140001649 mov rax, [rsp+68h+var_40]
.text:000000014000164E add rax, 10h // 整了0x10个位置
.text:0000000140001652 mov [rsp+68h+var_20], rax
.text:0000000140001657 jmp short loc_140001662
.text:0000000140001659 ; ---------------------------------------------------------------------------
.text:0000000140001659
.text:0000000140001659 loc_140001659: ; CODE XREF: main+47↑j
.text:0000000140001659 mov [rsp+68h+var_20], 0
.text:0000000140001662
.text:0000000140001662 loc_140001662: ; CODE XREF: main+57↑j
.text:0000000140001662 mov rax, [rsp+68h+var_20]
.text:0000000140001667 mov [rsp+68h+var_48], rax
.text:000000014000166C mov rax, [rsp+68h+var_48]
.text:0000000140001671 mov [rsp+68h+var_30], rax
.text:0000000140001676 cmp [rsp+68h+var_30], 0
.text:000000014000167C jz short loc_140001699
.text:000000014000167E mov rax, [rsp+68h+var_30]
.text:0000000140001683 mov rax, [rax]
.text:0000000140001686 mov edx, 1
.text:000000014000168B mov rcx, [rsp+68h+var_30]
.text:0000000140001690 call qword ptr [rax] // 用的是虚表的第一个函数
.text:0000000140001692 mov [rsp+68h+var_18], rax
.text:0000000140001697 jmp short loc_1400016A2
.text:0000000140001699 ; ---------------------------------------------------------------------------
.text:0000000140001699
.text:0000000140001699 loc_140001699: ; CODE XREF: main+7C↑j
.text:0000000140001699 mov [rsp+68h+var_18], 0
.text:00000001400016A2
.text:00000001400016A2 loc_1400016A2: ; CODE XREF: main+97↑j
.text:00000001400016A2 xor eax, eax
.text:00000001400016A4 add rsp, 60h
.text:00000001400016A8 pop rdi
.text:00000001400016A9 retn
上图,指针向后调整0x10个位置后,调用构造析构函数,调用的是虚表的第一个函数,我们看看构造函数,看看虚表是怎么被赋值的
Derived::Derived(void)
.text:0000000140001760 mov [rsp+arg_0], rcx
.text:0000000140001765 push rdi
.text:0000000140001766 sub rsp, 20h
.text:000000014000176A mov rcx, [rsp+28h+arg_0] ; this
.text:000000014000176F call j_??0Base1@@QEAA@XZ ; Base1::Base1(void)
.text:0000000140001774 nop
.text:0000000140001775 mov rax, [rsp+28h+arg_0]
.text:000000014000177A add rax, 10h
.text:000000014000177E mov rcx, rax ; this
.text:0000000140001781 call j_??0Base2@@QEAA@XZ ; Base2::Base2(void)
.text:0000000140001786 mov rax, [rsp+28h+arg_0]
.text:000000014000178B lea rcx, ??_7Derived@@6BBase1@@@ ; const Derived::`vftable'{for `Base1'}
.text:0000000140001792 mov [rax], rcx
.text:0000000140001795 mov rax, [rsp+28h+arg_0]
.text:000000014000179A lea rcx, ??_7Derived@@6BBase2@@@ ; const Derived::`vftable'{for `Base2'}
.text:00000001400017A1 mov [rax+10h], rcx
.text:00000001400017A5 mov rax, [rsp+28h+arg_0]
.text:00000001400017AA mov dword ptr [rax+20h], 0
.text:00000001400017B1 mov rax, [rsp+28h+arg_0]
.text:00000001400017B6 add rsp, 20h
.text:00000001400017BA pop rdi
.text:00000001400017BB retn
从上面可知,调用的虚表应该是const Derived::
vftable'{for Base2'}
查看const Derived::
vftable'{for Base2'}
虚表只有一个函数,我们查看这个函数
.text:000000014000187C ; [thunk]:public: virtual void * Derived::`vector deleting destructor'`adjustor{16}' (unsigned int)
.text:000000014000187C ??_EDerived@@WBA@EAAPEAXI@Z proc near ; CODE XREF: [thunk]:Derived::`vector deleting destructor'`adjustor{16}' (uint)↑j
.text:000000014000187C sub rcx, 10h ; this
.text:0000000140001880 jmp j_??_GDerived@@UEAAPEAXI@Z_0 ; Derived::`scalar deleting destructor'(uint)
.text:0000000140001880 ??_EDerived@@WBA@EAAPEAXI@Z endp
看到这里应该明白了,上面向前调整了0x10个位置,然后再调用析构代理函数,所以内存释放就不会出现错误了