C++中new和delete的背后(最后还是调用了MSVCR90的malloc)

关于 C++中new背后的行为, 以前已经写过一篇了 理解C++中new背后的行为, 但是里面也只是泛泛而谈,没有真凭实据, 下面我们从汇编的角度看C++编译器究竟在背后干了什么?

我们的代码很简单, 如下:

#include <iostream>

class A

{

public:

virtual void print()

{

std::cout << 10;

}

virtual ~A()

{

std::cout << "~A()";

}

};

class B: public A

{

public:

virtual void print()

{

std::cout << 100;

}

};

int _tmain(int argc, _TCHAR* argv[])

{

A* p = new B();

p->print();

delete p;

return 0;

}

我用WinDbg可以看到main函数生成的汇编代码如下:

NewTest!wmain:

00aa1020 56              push    esi

00aa1021 6a04            push    4

00aa1023 e8b4030000      call    NewTest!operator new (00aa13dc) //调用operator new分配大小为4字节的空间

00aa1028 83c404          add     esp,4

00aa102b 85c0            test    eax,eax

00aa102d 740a            je      NewTest!wmain+0x19 (00aa1039)

00aa102f c7005421aa00    mov     dword ptr [eax],offset NewTest!B::`vftable‘ (00aa2154) //将虚表地址写入对象地址的头4个字节(虚表指针)

00aa1035 8bf0            mov     esi,eax

00aa1037 eb02            jmp     NewTest!wmain+0x1b (00aa103b)

00aa1039 33f6            xor     esi,esi

00aa103b 8b06            mov     eax,dword ptr [esi]

00aa103d 8b10            mov     edx,dword ptr [eax]

00aa103f 8bce            mov     ecx,esi

00aa1041 ffd2            call    edx //调用虚表内的第一个函数print

00aa1043 8b06            mov     eax,dword ptr [esi]

00aa1045 8b5004          mov     edx,dword ptr [eax+4]

00aa1048 6a01            push    1

00aa104a 8bce            mov     ecx,esi

00aa104c ffd2            call    edx //调用虚表内的第二个函数(析构函数)

00aa104e 33c0            xor     eax,eax

00aa1050 5e              pop     esi

00aa1051 c3              ret

00aa1052 cc              int     3

从上面代码中我们可以看到我们构造的B对象一共只有4个字节,而这四个字节包含的就是对象的虚表指针,对于C++对象内存布局, 对于C++对象的内存布局,可以看我这篇 探索C++对象模型。同时我们可以看到, C++里确实是通过虚表来实现多态的。

上面的代码也告诉了我们为什么不能在构造函数里通过调用虚函数实现多态? 因为虚表是在最终派生类的构造函数中生成的的, 执行基类构造函数时虚表都还没有生成。

接下来我们看看operator new背后的行为:

0:000> u 00aa13dc

NewTest!operator new:

00aa13dc ff25cc20aa00    jmp     dword ptr [NewTest!_imp_??2YAPAXIZ (00aa20cc)]

里面是一个直接跳转:

0:000> u poi(00aa20cc) L10

MSVCR90!operator new:

74603e99 8bff            mov     edi,edi

74603e9b 55              push    ebp

74603e9c 8bec            mov     ebp,esp

74603e9e 83ec0c          sub     esp,0Ch

74603ea1 eb0d            jmp     MSVCR90!operator new+0x17 (74603eb0)

74603ea3 ff7508          push    dword ptr [ebp+8]

74603ea6 e859dcfbff      call    MSVCR90!_callnewh (745c1b04)

74603eab 59              pop     ecx

74603eac 85c0            test    eax,eax

74603eae 740f            je      MSVCR90!operator new+0x26 (74603ebf)

74603eb0 ff7508          push    dword ptr [ebp+8]

74603eb3 e887feffff      call    MSVCR90!malloc (74603d3f)

74603eb8 59              pop     ecx

74603eb9 85c0            test    eax,eax

74603ebb 74e6            je      MSVCR90!operator new+0xa (74603ea3)

74603ebd c9              leave

我们可以看到operator new最终调用的是malloc, 如果再深入下去, 会发现malloc调用的是Kernel32!HeapAlloc, 而HeapAlloc调用的又是ntdll!RtlAllocateHeap, 关于heap的布局和分配算法,可以看张银奎的 软件调试

上面论证了new操作符背后的行为:

首先调用operator new分配空间, 我们可以重载operator new, 定义自己的内存分配算法

然后在分配的空间上调用构造函数创建对象, 构造函数内部可能会赋值虚表指针。

接下来我们看下delete背后的行为。

我们看到delete调用的是虚表里的第二个函数, 我们先看虚表内容:

0:000> dps 00aa2154

00aa2154  00aa1010 NewTest!B::print [f:\test\newtest\newtest\newtest.cpp @ 26]

00aa2158  00aa1060 NewTest!B::`scalar deleting destructor‘

00aa215c  00000000

00aa2160  00000048

00aa2164  00000000

上面看到虚表里有2个函数, 一个是print, 还有一个是destructor, 我们看下第二个函数的内容:

0:000> u 00aa1060  L10

NewTest!B::`scalar deleting destructor‘:

00aa1060 56              push    esi

00aa1061 8bf1            mov     esi,ecx

00aa1063 c7064821aa00    mov     dword ptr [esi],offset NewTest!A::`vftable‘ (00aa2148)

00aa1069 a15820aa00      mov     eax,dword ptr [NewTest!_imp_?coutstd (00aa2058)]

00aa106e 50              push    eax

00aa106f e84c010000      call    NewTest!std::operator<<<std::char_traits<char> > (00aa11c0)

00aa1074 83c404          add     esp,4

00aa1077 f644240801      test    byte ptr [esp+8],1

00aa107c 7409            je      NewTest!B::`scalar deleting destructor‘+0x27 (00aa1087)

00aa107e 56              push    esi

00aa107f e806030000      call    NewTest!operator delete (00aa138a)

00aa1084 83c404          add     esp,4

00aa1087 8bc6            mov     eax,esi

00aa1089 5e              pop     esi

00aa108a c20400          ret     4

我们可以看到虚表里放的是 B 的 scalar deleting destructor , 它里面包含两部分代码, 一个是我们真正定义的析构函数的代码,还有一部分就是operator delete ( operator delete又会去调用free, free调用kernel32!HeapFree)。这里的 scalar deleting destructor显然不是B的析构函数~B(), 这是编译器帮我产生的一个函数,它就是给delete B类型对象用的。

接下来我们看看对于数组类型的指针, C++编译器背后是如何处理的, 把代码改成如下:

int _tmain(int argc, _TCHAR* argv[])

{

A* p = new A[10];

delete []p;

return 0;

}

下面是生成的汇编代码:

NewTest!wmain:

01181030 6a2c            push    2Ch

01181032 e8c4030000      call    NewTest!operator new[] (011813fb) //通过operator new分配44自己

01181037 83c404          add     esp,4

0118103a 85c0            test    eax,eax

0118103c 7444            je      NewTest!wmain+0x52 (01181082)

0118103e 56              push    esi

0118103f 6810101801      push    offset NewTest!A::~A (01181010) //A的析构函数

01181044 6800111801      push    offset NewTest!A::A (01181100)  //A的构造函数

01181049 6a0a            push    0Ah //10

0118104b 8d7004          lea     esi,[eax+4] //跨过了头四个字节

0118104e 6a04            push    4    //对象大小

01181050 56              push    esi //esi里放的是对象列表的起始地址(跨过了头四个字节)

01181051 c7000a000000    mov     dword ptr [eax],0Ah //头四个字节写入对象列表数量(10)

01181057 e812040000      call    NewTest!`eh vector constructor iterator‘ (0118146e)

0118105c 85f6            test    esi,esi

0118105e 7421            je      NewTest!wmain+0x51 (01181081)

01181060 837efc00        cmp     dword ptr [esi-4],0 //判断对象数量是否 为 0

01181064 8d46fc          lea     eax,[esi-4] //包含对象数量的地址保存到  eax

01181067 740f            je      NewTest!wmain+0x48 (01181078)

01181069 8b06            mov     eax,dword ptr [esi] //取A的虚表地址

0118106b 8b5004          mov     edx,dword ptr [eax+4] //虚表里的第二个函数

0118106e 6a03            push    3

01181070 8bce            mov     ecx,esi

01181072 ffd2            call    edx

01181074 5e              pop     esi

01181075 33c0            xor     eax,eax

01181077 c3              ret

重点看上面红色的代码, 我们可以看到, 在new一个数组时,编译器帮我们做了下面一些事情:

(1)调用数组的operator new[] 分配内存, 大小为 4 + sizeof(object) * count, 其中头四个字节为对象数量

(2)调用NewTest!`eh vector constructor iterator(pArrayAddress, sizeof(object),  object_count, pFunConstructor, pFunDestructor),

其中 pFunDestructor为析构函数, pFunConstructor为构造函数, object_count为对象数量, sizeof(object)为对象大小,pArrayAddress为起始地址。,

下面我们反汇编 NewTest!`eh vector constructor iterator:

0:000> u 0118146e L50

NewTest!`eh vector constructor iterator‘:

0118146e 6a10            push    10h

01181470 6890221801      push    offset NewTest!__rtc_tzz+0x8 (01182290)

01181475 e8d2040000      call    NewTest!__SEH_prolog4 (0118194c)

0118147a 33c0            xor     eax,eax

0118147c 8945e0          mov     dword ptr [ebp-20h],eax

0118147f 8945fc          mov     dword ptr [ebp-4],eax

01181482 8945e4          mov     dword ptr [ebp-1Ch],eax

01181485 8b45e4          mov     eax,dword ptr [ebp-1Ch] //临时计数,初始为0

01181488 3b4510          cmp     eax,dword ptr [ebp+10h]  //将临时计数和对象数量比较

0118148b 7d13            jge     NewTest!`eh vector constructor iterator‘+0x32 (011814a0) //如果临时计数大于对象数量则退出循环

0118148d 8b7508          mov     esi,dword ptr [ebp+8] //保存第一个参数(起始地址)到 esi

01181490 8bce            mov     ecx,esi //赋this指针到ecx

01181492 ff5514          call    dword ptr [ebp+14h] //调用构造函数

01181495 03750c          add     esi,dword ptr [ebp+0Ch] //移动指针, 加上对象大小

01181498 897508          mov     dword ptr [ebp+8],esi //保存新对象地址到第一个参数

0118149b ff45e4          inc     dword ptr [ebp-1Ch] //增加临时计数

0118149e ebe5            jmp     NewTest!`eh vector constructor iterator‘+0x17 (01181485)

011814a0 c745e001000000  mov     dword ptr [ebp-20h],1

011814a7 c745fcfeffffff  mov     dword ptr [ebp-4],0FFFFFFFEh

011814ae e808000000      call    NewTest!`eh vector constructor iterator‘+0x4d (011814bb)

011814b3 e8d9040000      call    NewTest!__SEH_epilog4 (01181991)

011814b8 c21400          ret     14h

我们可以看到NewTest!`eh vector constructor iterator是编译器帮我们生成的函数, 它的作用就是为数组中的每个对象都调用构造函数。

接下我们再看看数组形式的delete []在背后究竟干了什么?

重点看上面紫色的代码:

NewTest!wmain:

....

01181060 837efc00        cmp     dword ptr [esi-4],0 //判断对象数量是否 为 0

01181064 8d46fc          lea     eax,[esi-4] //包含对象数量的地址保存到  eax

01181067 740f            je      NewTest!wmain+0x48 (01181078)

01181069 8b06            mov     eax,dword ptr [esi] //取A的虚表地址

0118106b 8b5004          mov     edx,dword ptr [eax+4] //虚表里的第二个函数

0118106e 6a03            push    3

01181070 8bce            mov     ecx,esi

01181072 ffd2            call    edx

....

可以看到它将对象列表起始地址保存到ecx, 然后调用对象虚表里的第二个函数, 并且传入参数是3, 我们先看对象虚表内容:

0:000> dps 01182148

01182148  01181000 NewTest!A::print [f:\test\newtest\newtest\newtest.cpp @ 11]

0118214c  01181090 NewTest!A::`vector deleting destructor‘

我们看看该函数究竟干了什么:

0:000> u 01181090  L40

NewTest!A::`vector deleting destructor‘:

01181090 53              push    ebx

01181091 8a5c2408        mov     bl,byte ptr [esp+8]

01181095 56              push    esi

01181096 8bf1            mov     esi,ecx

01181098 f6c302          test    bl,2 //是否需要调用析构函数

0118109b 742b            je      NewTest!A::`vector deleting destructor‘+0x38 (011810c8)

0118109d 8b46fc          mov     eax,dword ptr [esi-4]

011810a0 57              push    edi

011810a1 6810101801      push    offset NewTest!A::~A (01181010)

011810a6 8d7efc          lea     edi,[esi-4]

011810a9 50              push    eax

011810aa 6a04            push    4

011810ac 56              push    esi

011810ad e87f040000      call    NewTest!`eh vector destructor iterator‘ (01181531)

011810b2 f6c301          test    bl,1 //是否需要释放内存

011810b5 7409            je      NewTest!A::`vector deleting destructor‘+0x30 (011810c0)

011810b7 57              push    edi

011810b8 e85f030000      call    NewTest!operator delete[] (0118141c)

011810bd 83c404          add     esp,4

011810c0 8bc7            mov     eax,edi

011810c2 5f              pop     edi

011810c3 5e              pop     esi

011810c4 5b              pop     ebx

011810c5 c20400          ret     4

可以看到它内部调用的是NewTest!`eh vector destructor iterator, 而如果再跟踪NewTest!`eh vector destructor iterator,

会看所有数组里的对象调用析构函数, 最后调用operator delete[]释放所有内存。

我们可以看到数组new[]和delete[]的关键是, C++编译器在数组起始地址之前的4个字节保存了对象的数量N,后面会根据这个数量值进行N次的构造和析构 。

最后申明下, 上面的分析仅限于VS2008, 实际上在符合C++标准的前提下, 各个C++编译器有各自不同的实现。

我们可以看到C++ 编译器在背后干了很多事情,可能会内联我们的函数, 也可以修改和产生其他一些函数, 而这是很多C开发者受不了的事情, 所以在内核级别, 很多人宁愿用C来减少编译器背后的干扰。

最后思考一下, 如果我们代码这样写,会怎么样?

int _tmain(int argc, _TCHAR* argv[])

{

A* p = new B[10];

delete []p;

return 0;

}

答案请看 这里

http://www.cppblog.com/weiym/archive/2013/11/17/204292.html

时间: 2024-08-07 21:18:54

C++中new和delete的背后(最后还是调用了MSVCR90的malloc)的相关文章

C++中new和delete的背后( call edx 调用虚表内的第二个函数(析构函数))

关于 C++中new背后的行为, 以前已经写过一篇了 理解C++中new背后的行为, 但是里面也只是泛泛而谈,没有真凭实据, 下面我们从汇编的角度看C++编译器究竟在背后干了什么? 我们的代码很简单, 如下: #include <iostream> class A { public: virtual void print() { std::cout << 10; } virtual ~A() { std::cout << "~A()"; } }; c

C++中new和delete的背后

转自:http://www.cppblog.com/weiym/archive/2013/11/17/204292.html 关于 C++中new背后的行为, 以前已经写过一篇了 理解C++中new背后的行为, 但是里面也只是泛泛而谈,没有真凭实据, 下面我们从汇编的角度看C++编译器究竟在背后干了什么? 我们的代码很简单, 如下: #include <iostream> class A { public: virtual void print() { std::cout << 1

【转】浅谈 C++ 中的 new/delete 和 new[]/delete[]

原文 在 C++ 中,你也许经常使用 new 和 delete 来动态申请和释放内存,但你可曾想过以下问题呢? new 和 delete 是函数吗? new [] 和 delete [] 又是什么?什么时候用它们? 你知道 operator new 和 operator delete 吗? 为什么 new [] 出来的数组有时可以用 delete 释放有时又不行? - 如果你对这些问题都有疑问的话,不妨看看我这篇文章. new 和 delete 到底是什么? 如果找工作的同学看一些面试的书,我相

C++中的new/delete与operator new/operator delete

new operator/delete operator就是new和delete操作符,而operator new/operator delete是函数. new operator(1)调用operator new分配足够的空间,并调用相关对象的构造函数(2)不可以被重载 operator new(1)只分配所要求的空间,不调用相关对象的构造函数.当无法满足所要求分配的空间时,则        ->如果有new_handler,则调用new_handler,否则        ->如果没要求不

数据库设计中的Soft Delete模式

最近几天有点忙,所以我们今天来一篇短的,简单地介绍一下数据库设计中的一种模式——Soft Delete. 可以说,该模式毁誉参半,甚至有非常多的人认为该模式是一个Anti-Pattern.因此在本篇文章中,我们不仅仅会对该模式进行介绍,同时也会列出该模式可能导致的一系列问题,以帮助大家正确地决定是否使用该模式. Soft Delete简介 首先先来想一个需求,那就是对用户操作的回滚支持.例如我现在正在用Word编写这篇文章.当我执行了一个错误操作的时候,我仅仅需要键入Ctrl + Z就可以进行回

C++中的new/delete、构造/析构函数、dynamic_cast分析

1,new 关键字和 malloc 函数区别(自己.功能.应用): 1,new 关键字是 C++ 的一部分: 1,如果是 C++ 编译器,则肯定可以用 new 申请堆空间内存: 2,malloc 是由 C 库提供的函数: 1,如果没有相应的库,malloc 将不能使用: 2,有些特殊的嵌入式开发中,少了 C 库,则就不能动态内存分配: 3,new 以具体类型为单位进行内存分配: 1,面向对象中一般用 new,不用 malloc: 4,malloc 以字节为单位进行内存分配: 5,new 在申请内

《在WebView中如何让JS与Java安全地互相调用》核心JS全解析

1.说明: <[在WebView中如何让JS与Java安全地互相调用](http://www.pedant.cn/2014/07/04/webview-js-java-interface-research/)>核心JS全解析 2. 核心JS解析如下,欢迎拍砖!!! javascript: (function(win) {     console.log("HostApp initialization begin");     //win.HostApp对象     var 

c++学习笔记5,多重继承中派生类的构造函数与析构函数的调用顺序(二)

现在来测试一下在多重继承,虚继承,MI继承中虚继承中构造函数的调用情况. 先来测试一些普通的多重继承.其实这个是显而易见的. 测试代码: //测试多重继承中派生类的构造函数的调用顺序何时调用 //Fedora20 gcc version=4.8.2 #include <iostream> using namespace std; class base { public: base() { cout<<"base created!"<<endl; }

Eclipse中Android公共库的正确建立及调用方法(转)

转自http://www.cnblogs.com/SkyD/archive/2011/09/01/2161502.html 引言 之前一直头痛于没有办法在多个程序中共享资源,用作公共类库的方法也是使用的导出jar再导入的办法,现在终于初步搞明白了,可算解脱了~,分享出来. 建立公共库 首先建立公共库的Android项目,名为MyCoreLib: 完成后在左侧包浏览器里右键点选我们新建的这个项目,选择Properties项: 左侧选择Android分类,右侧勾选Is Library选项: 完成后在