重识new


layout: post
title: 重识new
categories: C/C++
description: 重识new
keywords:
url: https://lichao890427.github.io/ https://github.com/lichao890427/


一、new primer

  new操作符并不是c++的专属,c运行库有new.h头文件。C++中的new操作符,众所周知是用来分配动态内存的,而要能达到“动态”这种灵活性的特征,非堆区莫属,因为堆区支持手动分配。如果内存空间不够,new操作符返回空,或抛出异常(下面会讲述3种new操作符,常规new操作符、定位new操作符和禁止抛出异常的new操作符,如果禁止抛异常,那么只好返回空了)下面代码为MSDN中的,检测new分配是否成功,内存分配失败处理还可以通过_set_new_handler注册分配异常函数结果

// insufficient_memory_conditions.cpp
// compile with: /EHsc
#include <iostream>
using namespace std;
#define BIG_NUMBER 100000000
int main() {
   int *pI = new int[BIG_NUMBER];
   if( pI == 0x0 ) {
      cout << "Insufficient memory" << endl;
      return -1;
   }
}

  对于对象使用new操作符的情况,生成的代码执行的流程一般为:先调用内部合适的new函数,该函数最终调用内存分配API进行堆内存分配,同时初始化一些方便内存管理的结构体和数据。成功分配后,将该地址视为this,如有必要则设置虚表指针,之后调用构造函数对该地址处对象进行构造。构造函数一般也是执行初始化功能而已,修改修改数据啥的。这些都是老生常谈不再赘述。
  即使请求的空间是0字节大小,new也会返回不同的地址,也就说总是在不同区域分配。这里注意和空类的区别,空类的大小是1,传给new函数(该函数在之后给出)之后,new函数接受到的参数只会>=1,而对于特殊情况:char* pch=new char[0];new函数接受的参数确实是0,不过分配的空间并不是0字节,因为存在内存管理相关的结构体也会占用内存。
  new操作符有2种作用域,一种是全局new,一种是类作用域new。用户在自定义类中可以重载自定义new函数。代码如下:

#include <malloc.h>
#include <memory.h>
class Blanks
{
public:
    Blanks(){}
    void *operator new( size_t stAllocateBlock, char chInit );
};
void *Blanks::operator new( size_t stAllocateBlock, char chInit )
{
    void *pvTemp = malloc( stAllocateBlock );
    if( pvTemp != 0 )
        memset( pvTemp, chInit, stAllocateBlock );
    return pvTemp;
}
// 对于Blanks对象,全局new操作符已被替换,因此下面的代码将分配sizeof(Blanks)大小的空间并把数据赋值为0xa5
int main()
{
   Blanks *a5 = new(0xa5) Blanks;
   return a5 != 0;
}

二、new primer plus

  new和delete操作符是通过一种称为自由存储区的内存池分配内存的,而new和delete操作符本身在编译时会由编译器选择合适的new函数和delete函数进行实现。C运行库的new函数会在失败时抛出std::bad_alloc异常,如果要使用不抛出异常的new版本,则需要链接nothrownew.obj,然而一旦链接了该文件,标准C++库中的new就不起作用了。new有2种语法形式

  • [::] new [placement] new-type-name [new-initializer]
  • [::] new [placement] ( type-name ) [new-initializer]

2种new函数原型为:

  • void* operator new( std::size_t _Count ) throw(bad_alloc);
  • void* operator new( std::size_t _Count, const std::nothrow_t& ) throw( );
  • void* operator new( std::size_t _Count, void* _Ptr ) throw( );
  • void *operator new[]( std::size_t _Count ) throw(std::bad_alloc);
  • void *operator new[]( std::size_t _Count, const std::nothrow_t& ) throw( );
  • void *operator new[]( std::size_t _Count, void* _Ptr ) throw( );

其用法如下:

#include<new>
#include<iostream>
using namespace std;
class MyClass 
{
public: 
   MyClass( )
   {
      cout << "Construction MyClass." << this << endl;
   };
   ~MyClass( )
   {
      imember = 0; cout << "Destructing MyClass." << this << endl;
   };
   int imember;
};

int main( ) 
{
   // The first form of new delete
   MyClass* fPtr = new MyClass;
   delete fPtr;
   // The second form of new delete
   MyClass* fPtr2 = new( nothrow ) MyClass;
   delete fPtr2;
   // The third form of new delete
   char x[sizeof( MyClass )];
   MyClass* fPtr3 = new( &x[0] ) MyClass;
   fPtr3 -> ~MyClass();
   cout << "The address of x[0] is : " << ( void* )&x[0] << endl;
}


Construction MyClass.000B3F30
Destructing MyClass.000B3F30
Construction MyClass.000B3F30
Destructing MyClass.000B3F30
Construction MyClass.0023FC60
Destructing MyClass.0023FC60
The address of x[0] is : 0023FC60

#include <new>
#include <iostream>
using namespace std;

class MyClass {
public:
   MyClass() {
      cout << "Construction MyClass." << this << endl;
   };

   ~MyClass() {
      imember = 0; cout << "Destructing MyClass." << this << endl;
      };
   int imember;
};

int main() {
   // The first form of new delete
   MyClass* fPtr = new MyClass[2];
   delete[ ] fPtr;
   // The second form of new delete
   char x[2 * sizeof( MyClass ) + sizeof(int)];
   MyClass* fPtr2 = new( &x[0] ) MyClass[2];
   fPtr2[1].~MyClass();
   fPtr2[0].~MyClass();
   cout << "The address of x[0] is : " << ( void* )&x[0] << endl;
   // The third form of new delete
   MyClass* fPtr3 = new( nothrow ) MyClass[2];
   delete[ ] fPtr3;
}


Construction MyClass.00311AEC
Construction MyClass.00311AF0
Destructing MyClass.00311AF0
Destructing MyClass.00311AEC
Construction MyClass.0012FED4
Construction MyClass.0012FED8
Destructing MyClass.0012FED8
Destructing MyClass.0012FED4
The address of x[0] is : 0012FED0
Construction MyClass.00311AEC
Construction MyClass.00311AF0
Destructing MyClass.00311AF0
Destructing MyClass.00311AEC

探究第一种new形式实现

  第一种形式为常规new,MyClass* fPtr1 = new MyClass;

// new.cpp
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{       // try to allocate size bytes
    void *p;
    while ((p = malloc(size)) == 0)
    if (_callnewh(size) == 0)
    {       // report no memory
        _THROW_NCEE(_XSTD bad_alloc, );
    }
    return (p);
}

  从上面new函数实现可以看到是使用malloc函数进行分配,如果失败则调用_callnewh调用注册过的“new操作符失败回调”函数(注册用_set_new_handler),如果原先没注册new失败回调,则抛出bad_alloc异常,可见在默认情况下,该while只会执行1次,仅当自定义new失败回调函数返回true,才可能多次尝试分配。
  该种形式delete实现方式,单步以后vs不能定位到源码,不过我们可以换一种思路,既然知道一定执行析构函数,那么就在析构中下断点,断下后查看反汇编,并执行到上一级调用即可找到delete实现方法,因此看汇编实现,发现是一个名为“scalar deleting destructor”的内部函数:

// 
00EB3470  push        ebp 
00EB3471  mov         ebp,esp 
00EB3473  sub         esp,0CCh  
00EB3479  push        ebx 
00EB347A  push        esi 
00EB347B  push        edi 
00EB347C  push        ecx 
00EB347D  lea         edi,[ebp-0CCh]  
00EB3483  mov         ecx,33h  
00EB3488  mov         eax,0CCCCCCCCh  
00EB348D  rep stos    dword ptr es:[edi]  
00EB348F  pop         ecx  //以上部分为debug版API常见头,无需理会
00EB3490  mov         dword ptr [this],ecx 
00EB3493  mov         ecx,dword ptr [this]  
00EB3496  call        MyClass::~MyClass (0EB1023h)  //执行析构
00EB349B  mov         eax,dword ptr [ebp+8]  
00EB349E  and         eax,1  
00EB34A1  je          MyClass::`scalar deleting destructor'+3Fh (0EB34AFh)  //如果传入参数允许释放则进行调用对应delete函数(对于定位new对应的delete该参数是设置为不允许的)
00EB34A3  mov         eax,dword ptr [this]  
00EB34A6  push        eax 
00EB34A7  call        operator delete (0EB1154h)  
00EB34AC  add         esp,4  
00EB34AF  mov         eax,dword ptr [this]  //以下是无关的收尾工作
00EB34B2  pop         edi 
00EB34B3  pop         esi 
00EB34B4  pop         ebx 
00EB34B5  add         esp,0CCh  
00EB34BB  cmp         ebp,esp 
00EB34BD  call        __RTC_CheckEsp (0EB1352h)  
00EB34C2  mov         esp,ebp 
00EB34C4  pop         ebp 
00EB34C5  ret         4 

  执行到call operator delete这行,步入之后转到源码,可以看到使用的是dbgdel.cpp的delete函数。实现如下

// dbgdel.cpp
void operator delete( void *pUserData )
{
    _CrtMemBlockHeader * pHead;
    RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));
    if (pUserData == NULL)
        return;
    _mlock(_HEAP_LOCK);  /* 阻塞其他线程*/
    __TRY
        /* 得到用于内存块信息头指针*/
        pHead = pHdr(pUserData);
        /* 检查区块类型 */
        _ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));
        _free_dbg( pUserData, pHead->nBlockUse );//调用free函数释放内存
    __FINALLY
        _munlock(_HEAP_LOCK);  /* 解锁其他线程*/
    __END_TRY_FINALLY
    return;
}

探究第二种new形式实现

  第二种方式为不抛出异常的new,MyClass* fPtr2 = new( nothrow ) MyClass;,可见,这里用try块捕获了异常,因此不再抛出异常,余下的就是调用常规new函数而已。

// newopnt.cpp
void * __CRTDECL operator new(size_t count, const std::nothrow_t&) _THROW0()
{   // try to allocate count bytes
    void *p;
    _TRY_BEGIN
    p = operator new(count);
    _CATCH_ALL
    p = 0;
    _CATCH_END
    return (p);
}
#define _TRY_BEGIN try {
#define _CATCH(x)  } catch (x) {
#define _CATCH_ALL } catch (...) {
#define _CATCH_END }

探究第三种new形式实现

  第三种方式为布局new:
char x1[sizeof( MyClass )];MyClass* fPtr3 = new( &x1[0] ) MyClass;
  这里所谓的布局就是说告诉new我们已经有一个内存位置了:

// new
inline void *__CRTDECL operator new(size_t, void *_Where) _THROW0()
{   // construct array with placement at _Where
    return (_Where);
}

  可以发现该new什么都没做,那么为什么还要new呢?仔细想想可以知道,编译器对new的处理是调用new函数后,之后将该地址作为this指针进行初始化操作(比如设置虚表),再调用构造函数,而构造函数这玩意不能直接调用,不像析构函数那样,因为构造之前还没有对象和指针呢,对象和指针是构造以后才有的,而调用析构函数的时候,是已经有对象或指针的。所以这种定位new在我理解,就是可以相当于可以直接构造了。
  从上面可以看到new操作符先调用合适的new函数分配空间,之后调用构造函数构造,而delete函数刚好相对,先进行析构之后调用析构函数析构;同时可以看到布局new操作符的好处是可以手动指定构造和析构的时间,对于new无论哪种形式,在调用new函数分配好内存后都会调用构造函数进行构造,而定位new函数实则是直接返回,这就导致直接使用当前地址进行构造,相当于显示调用构造函数,而析构时由于没有实际分配空间,因此不能用delete,而是显示调用析构函数进行析构。
  上面都是对于有构造函数和析构函数对象的情况,用delete时,编译器会为该类专门生成一个scalar deleting destructor函数,该函数中先进行析构,之后调用operator delete函数。当然,如果没有析构函数,那么就不会有scalar deleting destructor函数了,此时单步是可以看到delete源码的,即dbgdel.cpp中的void operator delete(void *pUserData)函数。这一点在delete用于基本类型时显而易见。

探究第一种new[]形式实现

  第一种类型new,MyClass* fPtr4 = new MyClass[2]

// newaop.cpp
void *__CRTDECL operator new[](size_t count) _THROW1(std::bad_alloc)
{   // try to allocate count bytes for an array
    return (operator new(count));
}

  而编译器传给该new[]函数的参数count是sizeof(MyClass[2])+sizeof(int),该sizeof(int)用于内存管理。new仍然调用了new();没有本质区别,即这么多对象占用的内存是当作整体分配的。再分配好之后,就需要对每个对象this指针处进行初始化和构造了。
  通过逆向分析可知先调用了new[],如果成功分配内存,则调用数组构造迭代器vector_constructor_iterator对每个对象进行构造。void* base=new[](sizeof(int)+sizeof(MyClass[2]));,起始4字节存储要初始化的对象个数,剩余空间为对象占用内存

                push    0Ch             ; count
                call    j_??_U@YAPAXI@Z ; operator new[](uint)
                add     esp, 4
                mov     [ebp+var_1A0], eax
                mov     [ebp+var_4], 3
                cmp     [ebp+var_1A0], 0
                jz      short loc_41851B
                mov     eax, [ebp+var_1A0]
                mov     dword ptr [eax], 2
                push    offset j_??1MyClass@@QAE@XZ ; pDtor
                push    offset j_??0MyClass@@QAE@XZ ; pCtor
                push    2               ; count
                push    4               ; size
                mov     ecx, [ebp+var_1A0]
                add     ecx, 4
                push    ecx             ; ptr
                call    j_??_L@YGXPAXIHP6EX0@Z1@Z ; `eh vector constructor iterator'(void *,uint,int,void (*)(void *),void (*)(void *))
; ---------------------------------------------------------------------------
                mov     edx, [ebp+var_1A0]
                add     edx, 4
                mov     [ebp+var_22C], edx
                jmp     short loc_418525
; ---------------------------------------------------------------------------

loc_41851B:                             ; CODE XREF: _main+1FF j
                mov     [ebp+var_22C], 0

loc_418525:                             ; CODE XREF: _main+239 j
                mov     eax, [ebp+var_22C]
                mov     [ebp+var_1AC], eax
                mov     [ebp+var_4], 0FFFFFFFFh
                mov     ecx, [ebp+var_1AC]
                mov     [ebp+fPtr4], ecx
if(base)
{
  *(int*)base=2;//2个对象
  vector_construtor_iterator((MyClass*)((char*)base+4),sizeof(MyClass[2]),2,&MyClass::MyClass,&MyClass::~MyClass);
}

vector_constructor_iterator对应代码为:

 ; void __stdcall `eh vector constructor iterator'(void *ptr, unsigned int size, int count, void (__thiscall *pCtor)(void *), void (__thiscall *pDtor)(void *))
 ??_L@YGXPAXIHP6EX0@Z1@Z proc near       ; CODE XREF: `eh vector constructor iterator'(void *,uint,int,void (*)(void *),void (*)(void *)) j

 success         = dword ptr -20h
 i               = dword ptr -1Ch
 ms_exc          = CPPEH_RECORD ptr -18h
 ptr             = dword ptr  8
 size            = dword ptr  0Ch
 count           = dword ptr  10h
 pCtor           = dword ptr  14h
 pDtor           = dword ptr  18h

                 push    ebp
                 mov     ebp, esp
                 push    0FFFFFFFEh
                 push    offset stru_41F9A0
                 push    offset j___except_handler4
                 mov     eax, large fs:0
                 push    eax
                 add     esp, 0FFFFFFF0h
                 push    ebx
                 push    esi
                 push    edi
                 mov     eax, ___security_cookie
                 xor     [ebp+ms_exc.registration.ScopeTable], eax
                 xor     eax, ebp
                 push    eax
                 lea     eax, [ebp+ms_exc.registration]
                 mov     large fs:0, eax
                 mov     [ebp+success], 0
                 mov     [ebp+ms_exc.registration.TryLevel], 0
                 mov     [ebp+i], 0
                 jmp     short loc_415730
 ; ---------------------------------------------------------------------------

 loc_415727:                             ; CODE XREF: `eh vector constructor iterator'(void *,uint,int,void (*)(void *),void (*)(void *))+67 j
                 mov     eax, [ebp+i]
                 add     eax, 1
                 mov     [ebp+i], eax

 loc_415730:                             ; CODE XREF: `eh vector constructor iterator'(void *,uint,int,void (*)(void *),void (*)(void *))+45 j
                 mov     ecx, [ebp+i]
                 cmp     ecx, [ebp+count]
                 jge     short loc_415749
                 mov     ecx, [ebp+ptr]
                 call    [ebp+pCtor]
                 mov     edx, [ebp+ptr]
                 add     edx, [ebp+size]
                 mov     [ebp+ptr], edx
                 jmp     short loc_415727
 ; ---------------------------------------------------------------------------

 loc_415749:                             ; CODE XREF: `eh vector constructor iterator'(void *,uint,int,void (*)(void *),void (*)(void *))+56 j
                 mov     [ebp+success], 1
                 mov     [ebp+ms_exc.registration.TryLevel], 0FFFFFFFEh
                 call    $LN9            ; Finally handler 0 for function 4156E0
 ; ---------------------------------------------------------------------------

 loc_41575C:                             ; CODE XREF: `eh vector constructor iterator'(void *,uint,int,void (*)(void *),void (*)(void *)):$LN10 j
                 jmp     short $LN12
 ; ---------------------------------------------------------------------------

 $LN9:                                   ; CODE XREF: `eh vector constructor iterator'(void *,uint,int,void (*)(void *),void (*)(void *))+77 j
                                         ; DATA XREF: .rdata:stru_41F9A0 o
                 cmp     [ebp+success], 0 ; Finally handler 0 for function 4156E0
                 jnz     short $LN10
                 mov     eax, [ebp+pDtor]
                 push    eax             ; pDtor
                 mov     ecx, [ebp+i]
                 push    ecx             ; count
                 mov     edx, [ebp+size]
                 push    edx             ; size
                 mov     eax, [ebp+ptr]
                 push    eax             ; ptr
                 call    j_?__ArrayUnwind@@YGXPAXIHP6EX0@Z@Z ; __ArrayUnwind(void *,uint,int,void (*)(void *))

 $LN10:                                  ; CODE XREF: `eh vector constructor iterator'(void *,uint,int,void (*)(void *),void (*)(void *))+82 j
                 retn
 ; ---------------------------------------------------------------------------

 $LN12:                                  ; CODE XREF: `eh vector constructor iterator'(void *,uint,int,void (*)(void *),void (*)(void *)):loc_41575C j
                 mov     ecx, [ebp+ms_exc.registration.Next]
                 mov     large fs:0, ecx
                 pop     ecx
                 pop     edi
                 pop     esi
                 pop     ebx
                 mov     esp, ebp
                 pop     ebp
                 retn    14h
 ??_L@YGXPAXIHP6EX0@Z1@Z endp

逆向分析得到C++语法:

void __stdcall vector_constructor_iterator(MyClass *objs, unsigned int size, int count, void (__thiscall *pCtor)(void *), void (__thiscall *pDtor)(void *))
{
    int i=0;
    __try
    {
        for(;i<count;i++,objs++)
        {
            objs->pCtor();//用构造函数构造
        }       
    }
    __except(1)
    {
        __ArrayUnwind(objs,size,i,pDtor);//如果某个构造函数产生异常,则进行栈解退,用到析构函数 
    }
}

  栈解退,这里引用C++ Primer Plus的解释:“现在假设函数由于出现异常而终止(而不是由于返回),则程序也将释放栈中的内存,但不会在释放栈的第一个返回地址后停止,而是继续释放栈,直到找到一个位于try块中的返回地址,随后控制权将转到块尾的异常处理程序,而不会函数调用后面的第一条语句。这个过程称为栈解退,引发机制的一个非常重要的特性是,和函数返回一样,对于栈中的自动类对象,而throw语句则处理try块和throw之间的整个函数调用徐丽放在栈中的对象。如果没有栈解退这种特性,则引发异常后,对于中间函数调用放在栈中的自动类对象,其析构函数将不会被调用。”。unwind就是解退的意思,现在来查看__ArrayUnwind的源码,根据函数名可知该函数用于对象数组解退:


                 push    ebp
                 mov     ebp, esp
                 push    0FFFFFFFEh
                 push    offset stru_41F9E0
                 push    offset j___except_handler4
                 mov     eax, large fs:0
                 push    eax
                 sub     esp, 8
                 push    ebx
                 push    esi
                 push    edi
                 mov     eax, ___security_cookie
                 xor     [ebp+ms_exc.registration.ScopeTable], eax
                 xor     eax, ebp
                 push    eax
                 lea     eax, [ebp+ms_exc.registration]
                 mov     large fs:0, eax
                 mov     [ebp+ms_exc.old_esp], esp
                 mov     [ebp+ms_exc.registration.TryLevel], 0

 loc_41591A:                             ; CODE XREF: __ArrayUnwind(void *,uint,int,void (*)(void *))+54 j
                 mov     eax, [ebp+count]
                 sub     eax, 1
                 mov     [ebp+count], eax
                 js      short loc_415936
                 mov     ecx, [ebp+ptr]
                 sub     ecx, [ebp+size]
                 mov     [ebp+ptr], ecx
                 mov     ecx, [ebp+ptr]
                 call    [ebp+pDtor]
                 jmp     short loc_41591A
 ; ---------------------------------------------------------------------------

 loc_415936:                             ; CODE XREF: __ArrayUnwind(void *,uint,int,void (*)(void *))+43 j
                 mov     [ebp+ms_exc.registration.TryLevel], 0FFFFFFFEh
                 jmp     short loc_415956
 ; ---------------------------------------------------------------------------

 $LN7:                                   ; DATA XREF: .rdata:stru_41F9E0 o
                 mov     edx, [ebp+ms_exc.exc_ptr] ; Exception filter 0 for function 4158E0
                 push    edx             ; pExPtrs
                 call    ArrayUnwindFilter
                 add     esp, 4

 $LN9_1:
                 retn
 ; ---------------------------------------------------------------------------

 $LN8_0:                                 ; DATA XREF: .rdata:stru_41F9E0 o
                 mov     esp, [ebp+ms_exc.old_esp] ; Exception handler 0 for function 4158E0
                 mov     [ebp+ms_exc.registration.TryLevel], 0FFFFFFFEh

 loc_415956:                             ; CODE XREF: __ArrayUnwind(void *,uint,int,void (*)(void *))+5D j
                 mov     ecx, [ebp+ms_exc.registration.Next]
                 mov     large fs:0, ecx
                 pop     ecx
                 pop     edi
                 pop     esi
                 pop     ebx
                 mov     esp, ebp
                 pop     ebp
                 retn    10h

逆向分析得到C++代码:

void __stdcall __ArrayUnwind(MyClass* objs,unsigned size,int count,void (__thiscall *pDtor)(void*))
{//第[count]对象由于没有构造成功,因此从[count-1]个对象开始析构
    __try
    {
        while(count--)
        {
           objs--;
           objs->pDtor();
        }
    }
    __except(terminate(),0)//如果析构发生异常,则终止程序
    {
        return;
    }
}

探究第一种形式delete[]形式实现

                mov     eax, [ebp+fPtr4]
                mov     [ebp+var_188], eax
                mov     ecx, [ebp+var_188]
                mov     [ebp+var_194], ecx
                cmp     [ebp+var_194], 0
                jz      short loc_418574//如果之前new成功,则往下执行
                push    3               ; unsigned int
                mov     ecx, [ebp+var_194] ; this
                call    j_??_EMyClass@@QAEPAXI@Z ; MyClass::`vector deleting destructor'(uint)
                mov     [ebp+var_22C], eax
                jmp     short loc_41857E
; ---------------------------------------------------------------------------

loc_418574:                             ; CODE XREF: _main+27D j
                mov     [ebp+var_22C], 0

  可见vector_deleting_destructor是用来析构对象数组的,原型为void* __thiscall MyClass::vector_deleting_destructor(usigned int flag);,该函数是编译器内部为MyClass类加的成员函数
flag含义未知,所以需要分析该函数源码:

                 push    ebp
                 mov     ebp, esp
                 sub     esp, 0CCh
                 push    ebx
                 push    esi
                 push    edi
                 push    ecx
                 lea     edi, [ebp+var_CC]
                 mov     ecx, 33h
                 mov     eax, 0CCCCCCCCh
                 rep stosd
                 pop     ecx
                 mov     [ebp+this], ecx
                 mov     eax, [ebp+arg_0]
                 and     eax, 2
                 jz      short loc_413431
                 push    offset j_??1MyClass@@QAE@XZ ; pDtor
                 mov     eax, [ebp+this]
                 mov     ecx, [eax-4]
                 push    ecx             ; count
                 push    4               ; size
                 mov     edx, [ebp+this]
                 push    edx             ; ptr
                 call    j_??_M@YGXPAXIHP6EX0@Z@Z ; `eh vector destructor iterator'(void *,uint,int,void (*)(void *))
 ; ---------------------------------------------------------------------------
                 mov     eax, [ebp+arg_0]
                 and     eax, 1
                 jz      short loc_413429
                 mov     eax, [ebp+this]
                 sub     eax, 4
                 push    eax             ; void *
                 call    j_??_V@YAXPAX@Z_0 ; operator delete[](void *)
                 add     esp, 4

 loc_413429:                             ; CODE XREF: MyClass::`vector deleting destructor'(uint)+48 j
                 mov     eax, [ebp+this]
                 sub     eax, 4
                 jmp     short loc_413450
 ; ---------------------------------------------------------------------------

 loc_413431:                             ; CODE XREF: MyClass::`vector deleting destructor'(uint)+29 j
                 mov     ecx, [ebp+this] ; this
                 call    j_??1MyClass@@QAE@XZ ; MyClass::~MyClass(void)
                 mov     eax, [ebp+arg_0]
                 and     eax, 1
                 jz      short loc_41344D
                 mov     eax, [ebp+this]
                 push    eax             ; void *
                 call    j_??3@YAXPAX@Z_0 ; operator delete(void *)
                 add     esp, 4

 loc_41344D:                             ; CODE XREF: MyClass::`vector deleting destructor'(uint)+6F j
                 mov     eax, [ebp+this]

 loc_413450:                             ; CODE XREF: MyClass::`vector deleting destructor'(uint)+5F j
                 pop     edi
                 pop     esi
                 pop     ebx
                 add     esp, 0CCh
                 cmp     ebp, esp
                 call    j___RTC_CheckEsp
                 mov     esp, ebp
                 pop     ebp
                 retn    4

经过逆向分析得到C++代码:

void* __thiscall MyClass::vector_deleting_destructor(usigned int flag)
{
    if(flag&2)//由于push的是3,因此这里成立
    {
         vector_destructor_iterator(this,sizeof(MyClass),*(int*)((char*)this-4),MyClass::~MyClass);
         if(flag&1))//由于push的是3,因此这里成立
      {
          delete[]((char*)this-4);
      }
    }
    else
    {
         this->~MyClass();
         if(flag&1)
         {
          delete(this);
      }
    }
}

仅从以上代码可以分析出以下几点:

  • 1.this-4这个地址为之前new成功分配所返回值,可以将其看成sizeof(int)+sizeof(MyClass[2])大小的结构体,第一个成员为对象个数。
  • 2.该函数对数组和非数组进行了分别处理,可以分析出第2个二进制位为1时,是析构对象数组,为0时是析构普通对象。而第1个二进制位是规定是否释放内存,可以想象如果这里是定位new,那么这里是不应该释放的。
  • 3.vector_destructor_iterator起实际析构作用原型void __stdcall vector_destructor_iterator(MyClass *objs, unsigned int size, int count, void (__thiscall *pDtor)(void *));,下面来看该函数
                push    ebp
                mov     ebp, esp
                push    0FFFFFFFEh
                push    offset stru_41F9C0
                push    offset j___except_handler4
                mov     eax, large fs:0
                push    eax
                add     esp, 0FFFFFFF4h
                push    ebx
                push    esi
                push    edi
                mov     eax, ___security_cookie
                xor     [ebp+ms_exc.registration.ScopeTable], eax
                xor     eax, ebp
                push    eax
                lea     eax, [ebp+ms_exc.registration]
                mov     large fs:0, eax
                mov     [ebp+success], 0
                mov     eax, [ebp+size]
                imul    eax, [ebp+count]
                add     eax, [ebp+ptr]
                mov     [ebp+ptr], eax
                mov     [ebp+ms_exc.registration.TryLevel], 0

loc_41580B:                             ; CODE XREF: `eh vector destructor iterator'(void *,uint,int,void (*)(void *))+65 j
                mov     ecx, [ebp+count]
                sub     ecx, 1
                mov     [ebp+count], ecx
                js      short loc_415827
                mov     edx, [ebp+ptr]
                sub     edx, [ebp+size]
                mov     [ebp+ptr], edx
                mov     ecx, [ebp+ptr]
                call    [ebp+pDtor]
                jmp     short loc_41580B
; ---------------------------------------------------------------------------

loc_415827:                             ; CODE XREF: `eh vector destructor iterator'(void *,uint,int,void (*)(void *))+54 j
                mov     [ebp+success], 1
                mov     [ebp+ms_exc.registration.TryLevel], 0FFFFFFFEh
                call    $LN8            ; Finally handler 0 for function 4157C0
; ---------------------------------------------------------------------------

loc_41583A:                             ; CODE XREF: `eh vector destructor iterator'(void *,uint,int,void (*)(void *)):$LN9_0 j
                jmp     short $LN11
; ---------------------------------------------------------------------------

$LN8:                                   ; CODE XREF: `eh vector destructor iterator'(void *,uint,int,void (*)(void *))+75 j
                                        ; DATA XREF: .rdata:stru_41F9C0 o
                cmp     [ebp+success], 0 ; Finally handler 0 for function 4157C0
                jnz     short $LN9_0
                mov     eax, [ebp+pDtor]
                push    eax             ; pDtor
                mov     ecx, [ebp+count]
                push    ecx             ; count
                mov     edx, [ebp+size]
                push    edx             ; size
                mov     eax, [ebp+ptr]
                push    eax             ; ptr
                call    j_?__ArrayUnwind@@YGXPAXIHP6EX0@Z@Z ; __ArrayUnwind(void *,uint,int,void (*)(void *))

$LN9_0:                                 ; CODE XREF: `eh vector destructor iterator'(void *,uint,int,void (*)(void *))+80 j
                retn
; ---------------------------------------------------------------------------

$LN11:                                  ; CODE XREF: `eh vector destructor iterator'(void *,uint,int,void (*)(void *)):loc_41583A j
                mov     ecx, [ebp+ms_exc.registration.Next]
                mov     large fs:0, ecx
                pop     ecx
                pop     edi
                pop     esi
                pop     ebx
                mov     esp, ebp
                pop     ebp
                retn    10h

经过逆向分析得到C++代码:

void __stdcall vector_destructor_iterator(MyClass *objs, unsigned int size, int count, void (__thiscall *pDtor)(void *))
{
    int i=0;
    __try
    {
        MyClass* last=objs+size-1;//从最后一个对象开始析构
        while(count--)
        {
           last->pDtor();
           last--;
        }
    }
    __except(1)
    {
        __ArrayUnwind(objs,size,count,pDtor);//如果某个析构函数产生异常,则跳过该对象,继续析构之前的对象 
    }
}

  鉴于__ArrayUnwind前面已经介绍过,这里就不分析了。如果仔细分析上一节开头给出main汇编代码,会发现只要new成功了,delete都会去执行析构,即使出现对象数组中某个对象构造失败导致已经进行析构,delete时所有元素仍会析构一次。

探究第二种情况new[]形式实现

MyClass* fPtr5 = new( nothrow ) MyClass[2];

// newaopnt.cpp
void * __CRTDECL operator new[](::size_t count, const std::nothrow_t& x)
    _THROW0()
    {   // try to allocate count bytes for an array
    return (operator new(count, x));
    }

可见调用了单对象的第二种new形式,与非数组形式类似,不再赘述

探究第三种情况new[]形式实现

char x2[2*sizeof( MyClass ) + sizeof(int)];
MyClass* fPtr6 = new ( &x2[0] ) MyClass[2];

inline void *__CRTDECL operator new[](size_t, void *_Where) _THROW0()
{   // construct array with placement at _Where
    return (_Where);
}

  可见等同于对象第三种new形式,不再赘述。下面我们来看看其他内存分配函数

malloca

void* _malloca(size_t size);
  MSDN里是这么描述的:在栈上分配内存,是_alloca的安全性增强版本。返回指针是根据对象大小对齐,如果size是0则返回长度0的合法指针。如果地址空间无法分配会抛出一个栈溢出异常,该异常不是C++异常,需要使用SEH。_malloca和_alloca的区别在于_alloc无论大小总是在栈上分配,且无需free释放内存。而_malloca需要使用_freea释放内存,在调试模式下,_malloca总是在堆上分配。在异常处理时显式调用_malloca有一些限制,x86架构处理器异常处理例程会自动控制函数栈帧,在执行操作时并不基于当前闭合函数栈帧,这一点在Windows NT SEH和C++异常处理的catch语句中很常见。因此在以下情况显示调用_malloca,在执行异常处理例程后会产生程序崩溃。
  Windows NT SEH异常过滤表达式:__except(_malloca())
  Windows NT SEH最终执行表达式:__finally(_malloca())
  C++ 异常处理 catch语句
  然而_malloca可以从异常处理例程中除上述情况以外的情况下直接调用,或在异常处理所触发的回调函数中调用也是允许的。先来看一个例子:

#include <windows.h>
#include <stdio.h>
#include <malloc.h>
 
int main()
{
    int     size;
    int     numberRead = 0;
    int     errcode = 0;
    void    *p = NULL;
    void    *pMarker = NULL;
 
    while (numberRead == 0)
    {
        printf_s("Enter the number of bytes to allocate "
                 "using _malloca: ");
        numberRead = scanf_s("%d", &size);
    }
 
    // Do not use try/catch for _malloca,
    // use __try/__except, since _malloca throws
    // Structured Exceptions, not C++ exceptions.
 
    __try
    {
        if (size > 0)
        {
            p =  _malloca( size );
        }
        else
        {
            printf_s("Size must be a positive number.");
        }
        _freea( p );
    }
 
    // Catch any exceptions that may occur.
    __except( GetExceptionCode() == STATUS_STACK_OVERFLOW )
    {
        printf_s("_malloca failed!\n");
 
        // If the stack overflows, use this function to restore.
        errcode = _resetstkoflw();
        if (errcode)
        {
            printf("Could not reset the stack!");
            _exit(1);
        }
    };
}
// malloc.h
#define _ALLOCA_S_THRESHOLD     1024
#define _ALLOCA_S_STACK_MARKER  0xCCCC
#define _ALLOCA_S_HEAP_MARKER   0xDDDD
 
#if defined(_M_IX86)
#define _ALLOCA_S_MARKER_SIZE   8
#elif defined(_M_X64)
#define _ALLOCA_S_MARKER_SIZE   16
#elif defined(_M_ARM)
#define _ALLOCA_S_MARKER_SIZE   8
#elif !defined (RC_INVOKED)
#error Unsupported target platform.
#endif
 
......
 
#if !defined(__midl) && !defined(RC_INVOKED)
#pragma warning(push)
#pragma warning(disable:6540)
__inline void *_MarkAllocaS(_Out_opt_ __crt_typefix(unsigned int*) void *_Ptr, unsigned int _Marker)
{
    if (_Ptr)
    {
        *((unsigned int*)_Ptr) = _Marker;
        _Ptr = (char*)_Ptr + _ALLOCA_S_MARKER_SIZE;
    }
    return _Ptr;
}
#pragma warning(pop)
#endif
 
#if defined(_DEBUG)
#if !defined(_CRTDBG_MAP_ALLOC)
#undef _malloca
#define _malloca(size) \
__pragma(warning(suppress: 6255)) \
        _MarkAllocaS(malloc((size) + _ALLOCA_S_MARKER_SIZE), _ALLOCA_S_HEAP_MARKER)
#endif
#else
#undef _malloca
#define _malloca(size) \
__pragma(warning(suppress: 6255)) \
    ((((size) + _ALLOCA_S_MARKER_SIZE) <= _ALLOCA_S_THRESHOLD) ? \
        _MarkAllocaS(_alloca((size) + _ALLOCA_S_MARKER_SIZE), _ALLOCA_S_STACK_MARKER) : \
        _MarkAllocaS(malloc((size) + _ALLOCA_S_MARKER_SIZE), _ALLOCA_S_HEAP_MARKER))
#endif

  可以分析出DEBUG版下,宏调用了malloc进行分配,之后使用_MarkAllocaS对分配内存进行一些处理(后面讨论),而RELEASE版下,宏先判断要分配的内存是否过大,该门限为_ALLOCA_S_THRESHOLD-_ALLOCA_S_MARKER_SIZE=1016,如果超过该值则调用malloc,否则调用_alloca。从字面意思上可以知道_ALLOCA_S_HEAP_MARKER这个标志位说明该内存区是在堆上分配的,而_ALLOCA_S_STACK_MARKER标志是在栈上分配的。在malloc或_alloca分配成功后,总会调用_MarkAllocaS进行调整。结合字面意思和5行C语言代码可知,在执行过内存分配后,返回的指针前sizeof(unigned int*)字节为分配内存类型标志,之后指针调整到空闲位置丢给用户操作。那么所有的问题都落在_alloca和malloc的源码上,下面会进行分析。
  _alloca(我第一次见栈上分配内存是在逆向一个易语言程序时,用的是sub esp,而微软这个函数是第一次见)void* _alloca(size_t size);,该函数只在程序栈中分配字节,而函数退出时该空间会自动释放,因此无需手动释放。用此函数的限制和_malloca相同。

#include <windows.h>
#include <stdio.h>
#include <malloc.h>
 
int main()
{
    int     size = 1000;
    int     errcode = 0;
    void    *pData = NULL;
 
    // 注意:不要使用try/catch,而要使用__try/__except,因为_alloca抛出SEH而不是C++异常
    __try {
        // 使用_alloca分配太大的空间很容易崩溃,推荐1024字节以下的空间  
        if (size > 0 && size < 1024)
        {
            pData = _alloca( size );
            printf_s( "Allocated %d bytes of stack at 0x%p",
                      size, pData);
        }
        else
        {
            printf_s("Tried to allocate too many bytes.\n");
        }
    }
 
    // 如果溢出
    __except( GetExceptionCode() == STATUS_STACK_OVERFLOW )
    {
        printf_s("_alloca failed!\n");
 
        // 使用下面的函数恢复函数栈
        errcode = _resetstkoflw();
        if (errcode)
        {
            printf_s("Could not reset the stack!\n");
            _exit(1);
        }
    };
}

来看反汇编

; int __cdecl main()
_main           proc near               ; CODE XREF: j__main j

pAllocaBase     = dword ptr -120h
cbSize          = dword ptr -11Ch
var_114         = dword ptr -114h
allocaList      = dword ptr -48h
pData           = dword ptr -3Ch
errcode         = dword ptr -30h
size            = dword ptr -24h
var_1C          = dword ptr -1Ch
ms_exc          = CPPEH_RECORD ptr -18h

                push    ebp
                mov     ebp, esp
                push    0FFFFFFFEh
                push    offset stru_416F80
                push    offset j___except_handler4
                mov     eax, large fs:0
                push    eax
                add     esp, 0FFFFFEF0h
                push    ebx
                push    esi
                push    edi
                lea     edi, [ebp+pAllocaBase]
                mov     ecx, 42h
                mov     eax, 0CCCCCCCCh
                rep stosd
                mov     eax, ___security_cookie
                xor     [ebp+ms_exc.registration.ScopeTable], eax
                xor     eax, ebp
                mov     [ebp+var_1C], eax
                push    eax
                lea     eax, [ebp+ms_exc.registration]
                mov     large fs:0, eax
                mov     [ebp+ms_exc.old_esp], esp
                mov     [ebp+allocaList], 0
                mov     [ebp+size], 3E8h
                mov     [ebp+errcode], 0
                mov     [ebp+pData], 0
                mov     [ebp+ms_exc.registration.TryLevel], 0
                cmp     [ebp+size], 0
                jle     short loc_4114D3
                cmp     [ebp+size], 400h
                jge     short loc_4114D3
                mov     eax, [ebp+size]
                add     eax, 24h
                mov     [ebp+cbSize], eax
                mov     eax, [ebp+cbSize]
                call    j___alloca_probe_16
                mov     [ebp+pAllocaBase], esp
                mov     [ebp+ms_exc.old_esp], esp
                lea     ecx, [ebp+allocaList]
                push    ecx             ; pAllocaInfoList
                mov     edx, [ebp+cbSize] ; cbSize
                mov     ecx, [ebp+pAllocaBase] ; pAllocaBase
                call    j_@_RTC_AllocaHelper@12 ; _RTC_AllocaHelper(x,x,x)
                add     [ebp+pAllocaBase], 20h
                mov     edx, [ebp+pAllocaBase]
                mov     [ebp+pData], edx
                mov     esi, esp
                mov     eax, [ebp+pData]
                push    eax
                mov     ecx, [ebp+size]
                push    ecx
                push    offset Format   ; "Allocated %d bytes of stack at 0x%p"
                call    ds:__imp__printf_s
                add     esp, 0Ch
                cmp     esi, esp
                call    j___RTC_CheckEsp
                jmp     short loc_4114EA
; ---------------------------------------------------------------------------

loc_4114D3:                             ; CODE XREF: _main+72 j
                                        ; _main+7B j
                mov     esi, esp
                push    offset aTriedToAllocat ; "Tried to allocate too many bytes.\n"
                call    ds:__imp__printf_s
                add     esp, 4
                cmp     esi, esp
                call    j___RTC_CheckEsp

loc_4114EA:                             ; CODE XREF: _main+E1 j
                mov     [ebp+ms_exc.registration.TryLevel], 0FFFFFFFEh
                jmp     loc_41158D
; ---------------------------------------------------------------------------

$LN10:                                  ; DATA XREF: .rdata:stru_416F80 o
                mov     eax, [ebp+ms_exc.exc_ptr] ; Exception filter 0 for function 4113F0
                mov     ecx, [eax]
                mov     edx, [ecx]
                mov     [ebp+var_114], edx
                cmp     [ebp+var_114], 0C00000FDh
                jnz     short loc_41151B
                mov     [ebp+cbSize], 1
                jmp     short loc_411525
; ---------------------------------------------------------------------------

loc_41151B:                             ; CODE XREF: _main+11D j
                mov     [ebp+cbSize], 0

loc_411525:                             ; CODE XREF: _main+129 j
                mov     eax, [ebp+cbSize]

$LN12:
                retn
; ---------------------------------------------------------------------------

$LN11:                                  ; DATA XREF: .rdata:stru_416F80 o
                mov     esp, [ebp+ms_exc.old_esp] ; Exception handler 0 for function 4113F0
                mov     esi, esp
                push    offset a_allocaFailed ; "_alloca failed!\n"
                call    ds:__imp__printf_s
                add     esp, 4
                cmp     esi, esp
                call    j___RTC_CheckEsp
                mov     esi, esp
                call    ds:__imp___resetstkoflw
                cmp     esi, esp
                call    j___RTC_CheckEsp
                mov     [ebp+errcode], eax
                cmp     [ebp+errcode], 0
                jz      short loc_411586
                mov     esi, esp
                push    offset aCouldNotResetT ; "Could not reset the stack!\n"
                call    ds:__imp__printf_s
                add     esp, 4
                cmp     esi, esp
                call    j___RTC_CheckEsp
                mov     esi, esp
                push    1               ; Code
                call    ds:__imp___exit
; ---------------------------------------------------------------------------
                cmp     esi, esp
                call    j___RTC_CheckEsp

loc_411586:                             ; CODE XREF: _main+16C j
                mov     [ebp+ms_exc.registration.TryLevel], 0FFFFFFFEh

loc_41158D:                             ; CODE XREF: _main+101 j
                jmp     short loc_411591
; ---------------------------------------------------------------------------
                jmp     short loc_411593
; ---------------------------------------------------------------------------

loc_411591:                             ; CODE XREF: _main:loc_41158D j
                xor     eax, eax

loc_411593:                             ; CODE XREF: _main+19F j
                push    edx
                mov     ecx, ebp        ; frame
                push    eax
                lea     edx, v          ; v
                push    [ebp+allocaList] ; allocaList
                call    j_@_RTC_CheckStackVars2@12 ; _RTC_CheckStackVars2(x,x,x)
                pop     eax
                pop     edx
                lea     esp, [ebp-130h]
                mov     ecx, [ebp+ms_exc.registration.Next]
                mov     large fs:0, ecx
                pop     ecx
                pop     edi
                pop     esi
                pop     ebx
                mov     ecx, [ebp+var_1C]
                xor     ecx, ebp        ; cookie
                call    j_@__security_check_cookie@4 ; __security_check_cookie(x)
                mov     esp, ebp
                pop     ebp
                retn

                mov     eax, [ebp+size]
                add     eax, 24h
                mov     [ebp+cbSize], eax
                mov     eax, [ebp+cbSize]
                call    j___alloca_probe_16
                mov     [ebp+pAllocaBase], esp
                mov     [ebp+ms_exc.old_esp], esp
                lea     ecx, [ebp+allocaList]
                push    ecx             ; pAllocaInfoList
                mov     edx, [ebp+cbSize] ; cbSize
                mov     ecx, [ebp+pAllocaBase] ; pAllocaBase
                call    j_@_RTC_AllocaHelper@12 ; _RTC_AllocaHelper(x,x,x)
                add     [ebp+pAllocaBase], 20h
                mov     edx, [ebp+pAllocaBase]
                mov     [ebp+pData], edx

  很疑惑地,在__alloca_probe_16调用之前,发生了add eax,24h和mov eax,[ebp+cbSize],而在之后发生了mov [ebp+pAllocaBase], esp,那么大胆做出猜测:

  • 1.add eax,24h,说明这24h字节用来实现内存管理或字节对齐之类功能
  • 2.__alloca_probe_16为接受一个参数的函数,该参数通过eax传递,进行的操作是修改esp,因此esp可以看做执行结果
  • 3.另一个函数原型为void __fastcall _RTC_AllocaHelper(_RTC_ALLOCA_NODE *pAllocaBase, unsigned int cbSize, _RTC_ALLOCA_NODE **pAllocaInfoList),反汇编得到
; void __fastcall _RTC_AllocaHelper(_RTC_ALLOCA_NODE *pAllocaBase, unsigned int cbSize, _RTC_ALLOCA_NODE **pAllocaInfoList)
@_RTC_AllocaHelper@12 proc near         ; CODE XREF: _RTC_AllocaHelper(x,x,x) j
pAllocaInfoList = dword ptr  8
pAllocaBase = ecx
cbSize = edx
                push    ebp
                mov     ebp, esp
                push    ebx
                push    esi
                mov     esi, pAllocaBase
                mov     ebx, cbSize
                test    esi, esi
                jz      short loc_4116CC
                test    ebx, ebx
                jz      short loc_4116CC
                mov     cbSize, [ebp+pAllocaInfoList]
                test    cbSize, cbSize
                jz      short loc_4116CC
                push    edi
                mov     al, 0CCh
                mov     edi, esi
                mov     pAllocaBase, ebx
                rep stosb
                mov     eax, [cbSize]
                mov     [esi+4], eax
                mov     [esi+0Ch], ebx
                mov     [cbSize], esi
                pop     edi
loc_4116CC:                             ; CODE XREF: _RTC_AllocaHelper(x,x,x)+B j
                                        ; _RTC_AllocaHelper(x,x,x)+F j ...
                pop     esi
                pop     ebx
                pop     ebp
                retn    4
@_RTC_AllocaHelper@12 endp

逆向分析得道C++代码:

void main()
{
    ......
    int size=1024,cbsize=size+sizeof(_RTC_ALLOCA_NODE)+4;
    _RTC_ALLOCA_NODE* pAllocaBase=__alloca_probe_16(cbsize);
    _RTC_AllocaHelper(pAllocaBase,cbsize,NULL);
    void* pData=(void*)(pAllocaBase+1);
    ......
}
 
//这个结构体来自于reactos
#pragma pack(push,1)
typedef struct _RTC_ALLOCA_NODE
{
     __int32 guard1;
    struct _RTC_ALLOCA_NODE *next;
    #if (defined(_X86_) && !defined(__x86_64))
        __int32 dummypad;
    #endif
        size_t allocaSize;
    #if (defined(_X86_) && !defined(__x86_64))
        __int32 dummypad2;
    #endif
        __int32 guard2[3];
}_RTC_ALLOCA_NODE;
#pragma pack(pop)
 
void __fastcall _RTC_AllocaHelper(_RTC_ALLOCA_NODE *pAllocaBase, unsigned int cbSize, _RTC_ALLOCA_NODE **pAllocaInfoList)
{//初始化已分配空间,可以用于维护调试版函数栈
  if(pAllocaBase && cbSize && pAllocaInfoList)//由于最后一个参数在本例中为0,这个函数实际相当于没有执行
  {
     memset(pAllocaBase,0xCC,cbSize);//经常在调试版程序栈空间看到0xCC  "烫烫烫烫烫"  对吧,就是这样的。。。
     pAllocaBase->next=*pAllocaInfoList;//链接到前一个结构;
     pAllocaBase->allocaSize=cbSize;
     *pAllocaInfoList=pAllocaBase;//自此可知,上述结构形成链表,pAllocaInfoList指向当前结构
  }
}
//__alloca_probe_16代码下面会进行分析

  以上是debug版的情况,如果尝试用release版查看反汇编代码,会发现只有push和call __alloca_probe_16部分,可知add eax,24和AllocaHelper只是调试版本用于内存管理的。所以重点落在该函数的解析上。进入源代码查看,__alloca_probe_16用来按16字节对齐内存,而chkstk子例程进行实际分配操作:

// alloca16.asm

; _alloca_probe_16, _alloca_probe_8 - 按8/16字节对齐例程
;输入:EAX = 栈帧大小
;输出:调整EAX,修改esp.
 
public  _alloca_probe_8
_alloca_probe_16 proc                   ; 16 byte aligned alloca
 
        push    ecx
        lea     ecx, [esp] + 8          ; 父函数栈顶(call _alloca_probe_16和push ecx)
        sub     ecx, eax                ; 
        and     ecx, (16 - 1)           ; 计算地址低4位未对齐偏移
        add     eax, ecx                ; 增加cbSize使其对齐
        sbb     ecx, ecx                ; 如果cbSize溢出,ecx = 0xFFFFFFFF,否则ecx = 0
        or      eax, ecx                ; 如果溢出,则eax = 0xFFFFFFFF
        pop     ecx                     ; 还原ecx
        jmp     _chkstk         ; eax存储修正cbSize,并交给_chkstk处理
_alloca_probe_16 endp
 
        end
 
 
public  _alloca_probe
_chkstk proc
_alloca_probe    =  _chkstk
        push    ecx
        lea     ecx, [esp] + 8 - 4      ; 考虑到之后的ret指令对未来esp的修改
        sub     ecx, eax                ; 分配栈空间,ecx存储更新后的栈位置
        sbb     eax, eax                ; 如果申请空间过大,eax = 0xFFFFFFFF,否则eax = 0
        not     eax                     ; 
        and     ecx, eax                ; ecx = 0 | ecx = ecx
        mov     eax, esp                ;
        and     eax, not ( _PAGESIZE_ - 1) ; 得到当前栈位置所处页面地址
cs10:
        cmp     ecx, eax                ; 
        jb      short cs20              ; 如果新的栈位置小于页面地址
        mov     eax, ecx                ; 
        pop     ecx
        xchg    esp, eax                ; 更新esp,原始esp存储在eax中
        mov     eax, dword ptr [eax]    ; 当前esp指向返回地址
        mov     dword ptr [esp], eax    ; 修正函数栈帧,使其可以正确返回
        ret
cs20:
        sub     eax, _PAGESIZE_         ; 获取上一个页面
        test    dword ptr [eax],eax     ; 探测页面权限
        jmp     short cs10      ; 如果没有产生异常则跳转,如果出现异常,则直接进入父函数的异常处理中
 
_chkstk endp
        end

calloc

void* calloc(size_t num,size_t size);
  calloc用来分配数组空间,同样返回指针是根据对象类型对齐的,每个对象都被初始化为0,如果待分配内存超过_HEAP_MAXREQ或分配失败则设置errno为ENOMEM,calloc内部调用了malloc函数使用_set_new_mode函数设置回调模式,该回调用于处理分配失败情况,默认情况下,分配失败后malloc不会调用新回调分配内存,然后我们可以通过提前调用_set_new_mode(1)或者链接NEWMODE.OBJ修改这种默认行为.calloc用法如下:

#include <stdio.h>
#include <malloc.h>
 
int main( void )
{
   long *buffer;
 
   buffer = (long *)calloc( 40, sizeof( long ) );
   if( buffer != NULL )
      printf( "Allocated 40 long integers\n" );
   else
      printf( "Can't allocate memory\n" );
   free( buffer );
}

Calloc源码:

// calloc.c和calloc_impl.c
void * __cdecl _calloc_base (size_t num, size_t size)
{
    int errno_tmp = 0;
    void * pv = _calloc_impl(num, size, &errno_tmp);
 
    if ( pv == NULL && errno_tmp != 0 && _errno())
    {
        errno = errno_tmp; // recall, #define errno *_errno()
    }
    return pv;
}
void * __cdecl _calloc_impl (size_t num, size_t size, int * errno_tmp)
{
        size_t  size_orig;
        void *  pvReturn;
 
        /* ensure that (size * num) does not overflow */
        if (num > 0)
        {
            _VALIDATE_RETURN_NOEXC((_HEAP_MAXREQ / num) >= size, ENOMEM, NULL);
        }
        size_orig = size = size * num;
 
 
        /* force nonzero size */
        if (size == 0)
            size = 1;
 
        for (;;)
        {
            pvReturn = NULL;
 
            if (size <= _HEAP_MAXREQ)
            {
                if (pvReturn == NULL)
                    pvReturn = HeapAlloc(_crtheap, HEAP_ZERO_MEMORY, size);
            }
 
            if (pvReturn || _newmode == 0)
            {
                RTCCALLBACK(_RTC_Allocate_hook, (pvReturn, size_orig, 0));
                if (pvReturn == NULL)
                {
                    if ( errno_tmp )
                        *errno_tmp = ENOMEM;
                }
                return pvReturn;
            }
 
            /* call installed new handler */
            if (!_callnewh(size))
            {
                if ( errno_tmp )
                    *errno_tmp = ENOMEM;
                return NULL;
            }
 
            /* new handler was successful -- try to allocate again */
        }
}

  现在来分析_calloc_impl执行流程:

  • 1.先检查申请大小是否超出门限,若申请大小为0则强制为1
  • 2.使用HeapAlloc分配内存并清零。如果成功则返回,否则执行_callnewh,即定义的失败处理函数,如果该回调函数返回0则原函数返回0退出,如果该回调函数返回非0,则原函数重复执行2直到成功。(_callnewh最终调用了NtQueryInformationProcess 0x24)

  可见calloc并没有像MSDN说的那样调用了malloc。。。另外,没看到有异常处理机制。

_expand

  用于扩展或缩小已分配内存,用于改变已分配内存区大小。void* _expand(void* memblock,size_t newsize);
  该函数会检测地址的内存权限,如果不通过移动内存无法得到足够的空间,该函数会返回空,该函数不会分配小于请求大小的内存区。该函数不通过移动内存块的方式增缩已分配堆内存,在64位下该函数不会缩小内存区,大小小于16k的内存块都是在低碎片堆中分配的,在这种情况下_expand不会对内存块做任何变动直接返回memblock。同样该函数会检测参数合法性,且size不能超过门限值_HEAP_MAXREQ。

#include <stdio.h>
#include <malloc.h>
#include <stdlib.h>
 
int main( void )
{
   char *bufchar;
   printf( "Allocate a 512 element buffer\n" );
   if( (bufchar = (char *)calloc( 512, sizeof( char ) )) == NULL )
      exit( 1 );
   printf( "Allocated %d bytes at %Fp\n", 
         _msize( bufchar ), (void *)bufchar );
   if( (bufchar = (char *)_expand( bufchar, 1024 )) == NULL )
      printf( "Can't expand" );
   else
      printf( "Expanded block to %d bytes at %Fp\n", 
            _msize( bufchar ), (void *)bufchar );
   // Free memory 
   free( bufchar );
   exit( 0 );
}

expand源码为:

void * __cdecl _expand_base (void * pBlock, size_t newsize)
{
        void *      pvReturn;
 
        size_t oldsize;
 
        /* validation section */
        _VALIDATE_RETURN(pBlock != NULL, EINVAL, NULL);
        if (newsize > _HEAP_MAXREQ) {
            errno = ENOMEM;
            return NULL;
        }
 
        if (newsize == 0)
        {
            newsize = 1;
        }
 
        oldsize = (size_t)HeapSize(_crtheap, 0, pBlock);
 
        pvReturn = HeapReAlloc(_crtheap, HEAP_REALLOC_IN_PLACE_ONLY, pBlock, newsize);
 
        if (pvReturn == NULL)
        {
            /* 如果使用了低碎片堆则返回原指针. */
            if (oldsize <= 0x4000 /* 低碎片堆最多申请16KB内存 */
                    && newsize <= oldsize && _is_LFH_enabled())
                pvReturn = pBlock;
            else
                errno = _get_errno_from_oserr(GetLastError());
        }
 
        if (pvReturn)
        {
            RTCCALLBACK(_RTC_Free_hook, (pBlock, 0));
            RTCCALLBACK(_RTC_Allocate_hook, (pvReturn, newsize, 0));
        }
 
        return pvReturn;
}

  可见_expand函数最终调用了HeapReAlloc函数。
  一个小插曲:其实这些内存管理的库函数普遍使用debug和release两个版本,debug源码在dbg*.c(pp)中可以找到,同时调试的时候是可以直接定位进去的,而release版函数通常在函数名所在文件,比如delete在delete.cpp中;而debug版源码内存管理函数通常会有一种_CrtMemBlockHeader的结构体,这些都需要自己摸索。

realloc

源码:

void * __cdecl _realloc_base (void * pBlock, size_t newsize)
{
        void *      pvReturn;
        size_t      origSize = newsize;
 
        //  if ptr is NULL, call malloc
        if (pBlock == NULL)
            return(_malloc_base(newsize));
 
        //  if ptr is nonNULL and size is zero, call free and return NULL
        if (newsize == 0)
        {
            _free_base(pBlock);
            return(NULL);
        }
 
 
        for (;;) {
 
            pvReturn = NULL;
            if (newsize <= _HEAP_MAXREQ)
            {
                if (newsize == 0)
                    newsize = 1;
                pvReturn = HeapReAlloc(_crtheap, 0, pBlock, newsize);
            }
            else
            {
                _callnewh(newsize);
                errno = ENOMEM;
                return NULL;
            }
 
            if ( pvReturn || _newmode == 0)
            {
                if (pvReturn)
                {
                    RTCCALLBACK(_RTC_Free_hook, (pBlock, 0));
                    RTCCALLBACK(_RTC_Allocate_hook, (pvReturn, newsize, 0));
                }
                else
                {
                    errno = _get_errno_from_oserr(GetLastError());
                }
                return pvReturn;
            }
 
            //  call installed new handler
            if (!_callnewh(newsize))
            {
                errno = _get_errno_from_oserr(GetLastError());
                return NULL;
            }
 
            //  new handler was successful -- try to allocate again
        }
}

得到realloc->HeapReAlloc

malloc

源码:

void * __cdecl _malloc_base (size_t size)
{
    void *res = NULL;
    //  validate size
    if (size <= _HEAP_MAXREQ) {
        for (;;) {
            //  allocate memory block
            res = _heap_alloc(size);
            //  if successful allocation, return pointer to memory
            //  if new handling turned off altogether, return NULL
            if (res != NULL)
            {
                break;
            }
            if (_newmode == 0)
            {
                errno = ENOMEM;
                break;
            }
            //  call installed new handler
            if (!_callnewh(size))
                break;
            //  new handler was successful -- try to allocate again
        }
    } else {
        _callnewh(size);
        errno = ENOMEM;
        return NULL;
    }
    RTCCALLBACK(_RTC_Allocate_hook, (res, size, 0));
    if (res == NULL)
    {
        errno = ENOMEM;
    }
    return res;
}
__forceinline void * __cdecl _heap_alloc (size_t size)
{
    if (_crtheap == 0) {
        _FF_MSGBANNER();    /* write run-time error banner */
        _NMSG_WRITE(_RT_CRT_NOTINIT);  /* write message */
        __crtExitProcess(255);  /* normally _exit(255) */
    }
    return HeapAlloc(_crtheap, 0, size ? size : 1);
}

得到malloc->_heap_alloc->HeapAlloc

free

源码:

void __cdecl _free_base (void * pBlock)
{
        int retval = 0;
        if (pBlock == NULL)
            return;
        RTCCALLBACK(_RTC_Free_hook, (pBlock, 0));
        retval = HeapFree(_crtheap, 0, pBlock);
        if (retval == 0)
        {
            errno = _get_errno_from_oserr(GetLastError());
        }
}

得到free->HeapFree

现在所有问题都集中在了HeapAlloc HeapFree HeapReAlloc上

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350