VC 运行时库中的 new/delete 函数

晨曦之光 发布于 2012/05/23 11:02
阅读 715
收藏 1

原文:VC 运行时库中的 new/delete 函数
作者:Breaker <breaker.zy_AT_gmail>


Windows VC CRT 运行时库中导出的 new/delete 二进制接口

目录


缘起^

用 dependency walker (depends) 跟了一下,发现 operator new/delete 函数是从 msvcr[ver].dll 中导出的(如图),其中 ver 是 VC 运行时库 (CRT) 的版本,例如:VC 2005 (VC8) 环境下,Release 版本为 80,Debug 版本为 80d。本以为 operator new/delete 是从另一个 msvcp[ver].dll 导出的,其实不是,msvcp[ver].dll 有自己导出的 operator new/delete,但并不是我们编程常规用的 new/delete 操作符

CRT 的动态链接模块^

VC 的运行时库,通常简称 CRT。特指时,它表示 msvcr[ver].dll 这个动态链接库,但在泛义上它和其它几个动态链接库有着紧密的联系,概念上的划分也有共享的部分。这几个都含 Runtime Library 语义的 dll 分别是:msvcr[ver].dll、msvcp[ver].dll、msvcm[ver].dll 和 msvcrt.dll

看看这些 dll 名字的都代表了什么意思:

  • msvcr[ver].dll

    全称为 Microsoft C Runtime Library。msvcr[ver].dll 导出所有的标准 C 库 API,和微软的标准 C 库扩展 API,以及一些 C++ 基本语言特性需要的 API

  • msvcp[ver].dll

    全称 Microsoft C++ Runtime Library。msvcp[ver].dll 导出标准 C++ 库中的 STL 和 iostream 类

  • msvcm[ver].dll

    它也叫做 Microsoft C Runtime Library,不过是给托管代码用的 CRT,所以后缀 m 的含义可以理解为 managed(托管)。msvcm 没有任何对应的静态库,也就是说要使用托管 CRT,只能动态链接到 msvcm[ver].dll。有两种方式指定使用托管 CRT:

    1. 使用 /clr 编译选项,这时客户程序可以使用 managed/native 混合代码,并且使用 msvcmrt.lib 导入库

    2. 使用 /clr:pure 编译选项,这时客户程序使用纯粹的 MSIL 托管代码

    由于 CLR 的 COM 实质,msvcm[ver].dll 依赖这些 dll:Native CRT:msvcr[ver].dll,OLE Library:ole32.dll,以及 .NET Runtime Execution Engine:mscoree.dll

上面的 msvcr、msvcp、msvcm 是 VC 引入的 dll,在部署应用程序时可以使用微软提供的 VC Redistributable Package 包来安装这些 dll,安装使用 side by side 方式,dll 拷贝到 %SystemRoot%winsxs 目录下,并且这些 dll 都是以 Release 方式编译的

另外,还有一种直接拷贝这些 dll 到目标系统的部署方法,参考 MSDN 的 How to: Deploy using XCopy (VC8) 的 Deploying Visual C++ library DLLs as private assemblies 章节。这些要拷贝的 dll 保存在:Path-to-VSVCredist(Release 版)和 Path-to-VSVCredistDebug_NonRedist(Debug 版)

  • msvcrt.dll

    全称 Windows NT CRT DLL,它的名字是固定不带版本号的,位置总在 %SystemRoot%system32 下。msvcr[ver].dll 和 msvcrt.dll 的作用区别借用 MSDN 的话说为:

    from: C Run-Time Libraries (VC8)

    What is the difference between msvcrt.dll and msvcr80.dll?

    The msvcrt.dll is now a "known DLL", meaning that it is a system component owned and built by Windows. It is intended for future use only by system-level components.

    msvcr[ver].dll 依赖 msvcrt.dll

使用 msvcr[ver].dll 导出的 new^

下面说下用 VC 编译的 C++ 程序中,常规使用的 new/delete 操作符,也就是 msvcr[ver].dll 导出的 operator new/delete 函数,它们的调用和二进制模块 msvcr[ver].dll 的导出函数的对应关系,以及申请内存失败的处理机制

声明:下面程序的编译、测试环境均为 VC8,用模块名 msvcr80[d].dll 表示无差别情况下的 Debug 或 Release 版 CRT 动态链接库;当提到 VC 的头文件、源文件、静态库、导入库、对象文件等路径时,均是相对于 Visual Studio 2005 的安装目录;给出的 MSDN 参考,若非指明,也是适用于 VC8 版本

工具:用 depends 查找模块导出符号时,有些不方便。可以使用 VC 的附带工具 dumpbin,用法参考 DUMPBIN Reference (VC9),例如:dumpbin /exports msvcp80d.dll > res.txt

头文件和模块 msvcr80[d].dll 中 new/delete 的对应^

有两个关于 new/delete 的头文件:标准 C++ 库的头文件 <new>(没有 .h 后缀),和 CRT 的头文件 <new.h>。<new.h> 对应的模块是 msvcr80[d].dll;而 <new> 中声明的大部分函数对应于 msvcr80[d].dll,如常规的 new/delete 函数,而有些函数,例如下面会讲的 set_new_handler(),则对应 msvcp80[d].dll

msvcr80[d].dll 导出的 new/delete 有:

// 这是模块 msvcr80[d].dll 导出的函数/二进制接口
void* operator new(unsigned int);
void* operator new(unsigned int, int, char const*, int);
void* operator new[](unsigned int);
void* operator new[](unsigned int, int, char const*, int);

void operator delete(void*);
void operator delete[](void*);

这些就是编程中常规用的 new/delete 的二进制接口

在 C++ 源码级别,总共有 6 个 new 函数声明,上面的第一个导出函数 void* operator new(unsigned int) 对应其中的 4 个:

// 这是源文件中声明的函数原型
void* operator new(size_t count) throw(std::bad_alloc);
void* operator new(size_t count, const std::nothrow_t&) throw();

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

为什么二进制的 void* operator new(unsigned int) 也会对应声明的 operator new[] 原型?后面会慢慢道来

另外 2 个 new 函数的声明是:

void* operator new(size_t count, void* object) throw();
void* operator new[](size_t count, void* object) throw();

这两个被称为放置式的 new,不对应任何运行时库的动态链接库,它们的二进制代码会编译进客户程序中

上面 6 个 new 的使用语法参考:operator new (CRT)operator new[] (CRT)

下面就说说这几个 new 的区别

标量 new 与矢量 new[]^

标量 new(scalar new),即 operator new;矢量 new(vector new),operator new[]

按照 MSDN operator new[] (CRT) 的说法,当使用 new 申请数组块内存时,就调用矢量的 operator new[]

在实际的 VC8 环境测试中,发现下列客户代码均会调用 msvcr80[d].dll 导出的 void* operator new(unsigned int),而非导出的 void* operator new[](unsigned int):

// 基本类型
char* s1 = new char[10];
int* s2 = new int[10];
// POD 的平坦结构
TestStruct* s3 = new TestStruct[10];
// non-POD 的类
TestObj* s4 = new TestObj[10];

反汇编调试后发现,在客户程序中 VC8 编译器已经将申请数组块的整体大小计算出来了,比如 sizeof(TestObj) = 40,则编译器就会计算出 new TestObj[10] 申请的大小为 40 * 10 = 400

  • 如果此时仅包含 <new.h>,或者不包含 <new> 和 <new.h> 任一个,则会调用 msvcr80[d].dll 中导出的 void * operator new(unsigned int)

  • 如果仅包含 <new>,或者 <new>、<new.h> 两个都包含(顺序无关),则会调用 VCcrtsrcnewaop.cpp 中定义的 operator new[](aop 的含义是 array operator)。那么 newaop.cpp 中的 operator new[] 是否是 msvcr80[d].dll 中导出的 void* operator new[](unsigned int)?答案不是,从 VC 的 Call Stack 中看出 newaop.cpp 对应的二进制模块是客户程序的模块而非 msvcr80[d].dll。newaop.cpp 定义的 operator new[] 是 msvcr80[d].dll 导出的 void* operator new(unsigned int) 的简单包裹,最后将调用传给它

delete/delete[] 的调用也有这种受包含 <new> 还是 <new.h> 影响的问题,见下面对 delete 的讨论

所以问题是,虽然在 msvcr80[d].dll 中导出了 operator new[],但似乎不能通过标准的方法使用它

放置式 new^

按 new 的行为是申请存储位置,还是将对象放置到某个存储位置,可以分为:非放置式(nonplacement)new,和 放置式(placement)new

放置式 new 语法参考:《C++ 程序设计语言》特别版(Bjarne)章节 10.4.11 对象的放置。有两种惯用的放置手法:1. 放置到已有存储位置。2. 使用 Arena(场地)分配存储位置(自定义放置式 new)

VC CRT 中提供了第一种放置式 new,在 <new> 和 <new.h> 中都有声明:

// <new.h> 中声明:
inline void *__CRTDECL operator new(size_t, void *_Where);

// <new> 中声明:
inline void *__CRTDECL operator new(size_t, void *_Where) _THROW0();
inline void *__CRTDECL operator new[](size_t, void *_Where) _THROW0();

这些放置 new 都是 inline 函数,声明的同时给出定义,函数体都是一句简单的 return (_Where),所以不存在 msvcr80[d].dll 中导出的放置式 new

测试放置式 new 的代码:

#include <new.h>

class TestObj
{
public:
    int a[10];
    TestObj(int _a = 1)
    {
        for (int i = 0; i < 10; i++)
            a[i] = _a;
    }
};

int main()
{
    TestObj* place = (TestObj*) malloc(sizeof(TestObj));

    // 用 TestObj(2) 初始化放置位置,注意:这种初始化是直接的,即
    // 调用直接构造函数 TestObj() 完成,而没有拷贝构造 TestObj(const TestObj& obj)
    // 或赋值 TestObj& operator=(const TestObj& obj) 语义

    TestObj* s1 = new(place) TestObj(2);

    // s1 和 place 指向同一块分配内存,下句会将这块内存释放
    delete s1;
    return 0;
}

no-throw 的 new^

还有一种 MSDN 上称为 placement, no-throw 的 operator new/new[],在 <new.h> 和 <new> 中都有声明:

// <new.h> 中的声明:
__bcount_opt(_Size) void *__CRTDECL operator new(size_t _Size, const std::nothrow_t&) throw();
__bcount_opt(_Size) void *__CRTDECL operator new[](size_t _Size, const std::nothrow_t&) throw();

// <new> 中的声明:
__bcount_opt(_Size) void *__CRTDECL operator new(size_t _Size, const std::nothrow_t&) _THROW0();
__bcount_opt(_Size) void *__CRTDECL operator new[](size_t _Size, const std::nothrow_t&) _THROW0();

不过它和放置位置没有关系,而是影响内存申请失败时的报告机制,如果使用这种 new,则申请失败时将以 operator new 返回 0 值表示失败,否则使用默认抛出异常的方式表示申请失败

以调用 new(std::nothrow) char[BIG_SIZE] 为例,调用顺序如下:

  1. 客户程序模块: newaopnt.cpp: void* __CRTDECL operator new[](::size_t count, const std::nothrow_t& x)

  2. 客户程序模块: newopnt.cpp: void* __CRTDECL operator new(size_t count, const std::nothrow_t&)

  3. msvcr80[d].dll: 导出的 void* operator new(unsigned int)

newaopnt.cpp 和 newopnt.cpp 在 VCcrtsrc 目录下,后缀 nt 表示 no-throw

更详细的 new 申请失败报告机制在后面叙述

调试版的 new^

在 msvcr80[d].dll 中有两个 4 个参数的 operator new 函数的导出:

void* operator new(unsigned int, int, char const*, int);
void* operator new[](unsigned int, int, char const*, int);

这两个函数的声明在 <crtdbg.h> 中,实现源码在 <dbgnew.cpp>,用法参考 The Debug Heap from C++。示例:

在工程公共头文件中:

// common.h

#include <crtdbg.h>

// Defines global operator new to allocate from client blocks

#ifdef _DEBUG
    #define DEBUG_CLIENTBLOCK   new(_CLIENT_BLOCK, __FILE__, __LINE__)
#else
    #define DEBUG_CLIENTBLOCK
#endif // _DEBUG

在源文件中:

#include "common.h"

#ifdef _DEBUG
#define new DEBUG_CLIENTBLOCK
#endif

int main()
{
    char* p1;
    p1 = new char[40];
    _CrtMemDumpAllObjectsSince(NULL);
}

上面申请数组块内存时,会调用 msvcr80d.dll 导出的 void* operator new[](unsigned int, int, char const*, int)

_CrtMemDumpAllObjectsSince(NULL) 将从程序开始到其调用点的所有堆对象调试信息,转储到调试输出,比如上面用 operator new[](unsigned int, int, char const*, int) 申请内存时,就会产生调试信息。_CrtMemDumpAllObjectsSince() 只在调试版起作用(有 _DEBUG 定义),当没有 _DEBUG 定义时,_CrtMemDumpAllObjectsSince() 就被替换成一个空操作 ((void)0),如同 _ASSERT() 的实现一样(_ASSERT() 也在 <crtdbg.h> 中定义)。可以用 VC 的调试 Output 窗口,或 DebugView 工具查看 _CrtMemDumpAllObjectsSince() 的输出

new 申请内存失败^

参考:The new and delete Operators

msvcr80[d].dll 中导出的 new 的报告申请失败的默认方式是抛出 std::bad_alloc 异常。测试例子:

#include <stdio.h>
#include <typeinfo>
#include <new>

#define BIG_SIZE    0x7fffffff

char* s1 = NULL;

try
{
    s1 = new char[BIG_SIZE];
}
catch (std::bad_alloc& e)
{
    printf("Caught: %sn", e.what());
    printf("Type: %sn", typeid(e).name());
}

上面的代码编译后不会链接到 msvcp80[d].dll,因为标准 C++ 异常类(即 std::bad_alloc)和 C++ RTTI 类(type_info),均在 msvcr80[d].dll 中有导出

如果想使用返回 0 来表示 new 申请内存失败,除了使用上面提到的 placement, no-throw 的 new 外,调试版的 operator new/new[](unsigned int, int, char const*, int) 也是以返回 0 表示失败,并不抛出异常

另外,还有一种用返回 0 来表示失败的方法,就是和 VClibnothrownew.obj 链接,此时便不会调用 msvcr80[d].dll 中导出的 new(用 depends 可以观察到),而调用 nothrownew.obj 中包含的 new

VC 编译出代码的异常处理方式(Exception Handling Model)和编译选项 /EH 有关系,它会影响 C 和 C++ 两种语言中的异常处理,以及标准 C++ 规范中的异常(try-catch 结构)和 Windows 特有的异常处理方式 SEH (Structured Exception Handling)(__try-__except-__finally 结构)

关于 VC 中 C/C++ 的异常处理,和更多 new/delete 操作的内容,请查阅文章最后的参考

std::set_new_handler 和 _set_new_handler^

  • std::set_new_handler

    参考:

    set_new_handler() 是 C++ 标准中定义的 new 申请失败处理设定函数,使用的失败处理函数类型为:typedef void (*new_handler)()。相关函数、类型在 <new> 中声明,set_new_handler() 在 msvcp80[d].dll 中导出

    例子:

    #include <new>
    #include <typeinfo>
    
    using namespace std;
    
    #define BIG_SIZE    0x7fffffff
    
    void my_handler()
    {
        printf("allocation failed.");
    
        // 除非你产生更多可用的内存,否则应该抛出一个异常
        throw bad_alloc();
    }
    
    int main()
    {
        new_handler old_handler = set_new_handler(my_handler);
        char* s1 = NULL;
    
        try
        {
            s1 = new char[BIG_SIZE];
        }
        catch (exception& e)
        {
            printf("Caught: %sn", e.what());
            printf("Type: %sn", typeid(e).name());
        }
    
        delete[] s1;
        s1 = NULL;
    
        set_new_handler(old_handler);
    
        return 0;
    }
    

    上面代码会链接到 msvcp80[d].dll

    自定义的错误处理函数 new_handler,必需完成 3 种功能之一:

    1. 产生更多可用的内存以供申请,例如使用垃圾回收、不常用对象交换到文件等手段,具体方式和应用有关,此时该函数可以用直接返回的方式离开,随后控制返回给 operator new(),并再次尝试申请内存

    2. 调用 abort 或 exit 函数,让 CRT 负责并终止程序执行

    3. 抛出一个异常,通常是 std::bad_alloc,该异常会穿过 operator new() 一直上抛到客户程序,此时 new_handler 通过异常方式离开 operator new()

    所以用户的 new_handler 实现中,如果即没有产生更多可用内存的工作,又不通过异常或 abort/exit 方式离开 operator new(),则 operator new() 就会陷入一直调用 new_handler 的死循环

  • _set_new_handler

    参考:_set_new_handler

    _set_new_handler() 是 CRT 提供的 new 申请失败处理设定函数,使用的失败处理函数类型为:typedef int (*_PNH)(size_t)。相关函数、类型在 <new.h> 中声明,_set_new_handler() 在 msvcr80[d].dll 中导出

    例子:

    #include <new.h>
    #include <typeinfo>
    
    using namespace std;
    
    #define BIG_SIZE    0x7fffffff
    
    int my_handler(size_t sz)
    {
        printf("allocation failed, request size: %dn", sz);
    
        // 除非你产生更多可用的内存,否则应该返回 0
        return 0;
    }
    
    int main()
    {
        _PNH old_handler = _set_new_handler(my_handler);
        char* s1 = NULL;
    
        try
        {
            s1 = new char[BIG_SIZE];
        }
        catch (exception& e)
        {
            printf("Caught: %sn", e.what());
            printf("Type: %sn", typeid(e).name());
        }
    
        delete[] s1;
        s1 = NULL;
    
        _set_new_handler(old_handler);
    
        return 0;
    }
    

    上面代码会仅会链接到 msvcr80[d].dll,而不会链接 msvcp80[d].dll

    和标准 C++ 库中的 new 失败处理函数不同,CRT 中规定的失败处理函数 int (*_PNH)(size_t),有返回值和参数。_PNH 的参数 size_t,表示请求申请但失败的内存大小,而 int 型返回值表示:

    1. 返回非 0 值,表示 _PNH 做过一些产生更多可用内存的工作,控制返回给 operator new() 后,会再次尝试申请内存。该情况和 C++ new_handler 的直接返回类似

    2. 返回 0,表示无需让 operator new() 再次尝试申请内存,申请操作已经彻底失败,最终控制会以默认抛出异常方式回到客户程序。该情况和 C++ new_handler 的以抛出异常方式结束申请类似

    所以类似标准 C++ 库的 new_handler,_PNH 如果即没有产生更多可用内存的工作,又返回了非 0 值,则 operator new() 就会陷入一直调用 _PNH 的死循环

    _PNH 是作用于 CRT 提供的全局 operator new() 的,要想 _PNH 也作用于 malloc(),使得 malloc() 申请失败时也调用 _PNH 处理,可以调用 _set_new_mode(1) 激活 malloc() 的失败处理机制

    在二进制层次,_PNH 函数是在所有模块间共享的,在 dll 中设定的 _PNH 会影响到主 exe 和其它 dll 中的 operator new() 行为

使用 msvcr[ver].dll 导出的 delete^

delete 和 delete[]^

在客户代码中使用 delete[] 时,如果仅包含 <new>,或者 <new>、<new.h> 两个都包含(顺序无关),则调用 msvcr80[d].dll 导出的 void operator delete[](void*)

如果仅包含 <new.h>,或者不包含 <new> 和 <new.h> 任一个,则调用 msvcr80[d].dll 导出的 void operator delete(void*)

实际上 msvcr80[d].dll 的 void operator delete[](void*) 实现,仅仅是对 void operator delete(void*) 做了一个简单的包裹。delete[] 的源码在 VCcrtsrcdelete2.cpp,Release 版的 delete 的源码在 VCcrtsrcdelete.cpp,Debug 版的 delete 的源码在 VCcrtsrcdbgdel.cpp

两次重复 delete^

示例:

char* s1 = new char[10];
delete[] s1;
delete[] s1;

在 Debug 配置下,即链接到 msvcr80d.dll,dbgdel.cpp 中定义的 delete 在程序运行时会检查到这种情况,并报 assert 诊断错误

在 Release 配置下,即链接到 msvcr80.dll,如果程序运行时 attach 到 VC 的调试器,则在重复 delete 的位置会给出警告,中止运行并报错 "This may be due to a corruption of the heap, and indicates a bug in [program-name.exe] or any of the DLLs it has loaded.",可以让调试器继续运行程序。如果没有 attach 到调试器,而是独立运行程序,则不会看到任何警告提示,程序运行直到结束,这可能会造成复杂程序中 bug 的潜藏点

delete 空指针^

在上面重复 delete 的代码中加上一句,如下:

char* s1 = new char[10];
delete[] s1;
s1 = NULL;
delete[] s1;

和重复 delete 不同,上述 delete 空指针无论是从 C++ 语法标准的角度(参考《C++ 程序设计语言》章节 6.2.6 自由存储),还是在实际的 VC8 环境中都是正确的,Debug 和 Release 版的 CRT 库均会正常运行,不会报出错误或警告

调试版的 delete^

参考:The Debug Heap from C++

和使用调试版的 void* operator new(unsigned int, int, char const*, int) 不同,不需用户写 delete 操作的替换宏和更改任何代码,只需用 Debug 配置方式编译程序即可使用调试版的 delete,而换到 Release 方式编译就可使用一般的 delete

msvcp80[d].dll 导出的 new/delete^

最后,不妨看看 msvcp80.dll 导出的 operator new/delete 函数,大家想想这些接口在何种情况下被调用呢?

void* operator new(unsigned int, struct std::_DebugHeapTag_t const &, char*, int);
void* operator new[](unsigned int, struct std::_DebugHeapTag_t const &, char*, int);

void operator delete(void *, struct std::_DebugHeapTag_t const &, char*, int);
void operator delete[](void *, struct std::_DebugHeapTag_t const &, char*, int);

除了上述全局的 new/delete 外,msvcp80[d].dll 中还导出了 std::locale::facet 类定义的 new/delete,不过这些跟用户常规的 new/delete 操作都没有关系

总结^

CRT 的模块 msvcr80[d].dll 中并非只含 C API,下面标准 C++ 库的 API 也在其中:

  • 常规使用的 operator new/delete,包括调试版 void* operator new(unsigned int, int, char const*, int),对应头文件 <new> 和 <new.h>

  • 标准 C++ 异常类,对应头文件 <exception>

  • 标准 C++ RTTI 类,对应头文件 <type_info> 和 <type_info.h>

而 msvcp80[d].dll 则侧重于标准 C++ 库中的 STL 和 iostream 类的实现,basic_string、vector、basic_ostream 等均在这里导出,另外还包括 <new> 中声明的 std::set_new_handler()

VC 的这种混合 C API 和标准 C++ 库的模块管理方式,让人感觉很混乱并摸不着头脑。其实,微软这么做是有考虑的,试想一下,如果你即想使用 C++ 的基本语言特性,如 new/delete、RTTI 等,又不想依赖额外的标准 C++ 库,如一大堆 STL 模板类和 iostream 类,此时就可以仅链接 msvcr80[d].dll。总之,只要记住 msvcr80[d].dll 包括所有 VC 基本的 C/C++ 语言支持就好了,这也是 CRT“运行时”库的内涵所在

参考^

  • C Run-Time Libraries: 说明 VC 中运行时库 (CRT)、托管代码运行时库、标准 C++ 库,对应的静态、动态、导入库文件,以及编译选项
  • CRT Debugging Techniques: CRT 中的调试技术,包括调试用到的宏、函数,调试版的堆管理函数 malloc、new/delete 等
  • /EH (Exception Handling Model): VC 用来控制异常处理方式的编译选项 /EH,默认的 VC 工程通常使用 /EHsc 选项编译
  • Exception Handling in Visual C++: 讲述 VC 支持的两种异常处理方式:C++ 异常 和 SEH (Structured Exception Handling) 的工作机制和语法。建议先看其中的Exception Specifications,明白函数的 throw() 修饰词怎样和编译选项 /EH 相互作用

原文链接:http://blog.csdn.net/breakerzy/article/details/6925957
加载中
返回顶部
顶部