跨模块内存管理的陷阱

跨模块内存管理的陷阱

A.exe中加载B.dll.  在A.exe中用new申请了一片内存,在B.dll中执行delete导致程序崩溃。

原因:

跨模块内存管理不一致导致。A.exe是MD链接,B.dll是MT链接,有2个C++运行库运行在同一个地址空间上,有可能读写同样内存区域,导致程序崩溃。

解决办法:

        每个模块自己管理内存。不要跨模块申请/释放内存.

1. A.exe中调用B.dll中的对应的函数,由其分配内存,然后再调用B.dll中的函数去释放。

2. A申请的内存,由A来释放。

3.B.dll使用MD链接,而且B编译时使用和A编译时相同版本的运行时库。

详解:

在Windows系统中,进程和dll的内存管理是由Runtime Library实现的,而MT和MD影响了Runtim Library的链接方式,从而导致了此问题。

l MT 是多线程静态链接运行时库。

l MD是多线程动态链接运行时库。

在《》中专门讲述了该问题。我这里简单摘录一下:

If you choose to link with the static runtime library, then your module has its own private copy of the C/C++ runtime. When your module calls new or malloc, the memory can only be freed by your module calling delete or free. If another module calls delete or free, that will use the C/C++ runtime of that other module which is not the same as yours. Indeed, even if you choose to link with the DLL version of the C/C++ runtime library, you still have to agree which version of the C/C++ runtime to use. If your DLL uses MSVCRT20.DLL to allocate memory, then anybody who wants to free that memory must also use MSVCRT20.DLL.

另外在MSDN《》也提到:

If you do choose to mix CRT libraries, remember that you have two separate copies of the CRT, with separate and distinct states, so you must be careful about what you try to do across a CRT-boundary. There are many ways to get into trouble with two CRTs. Here are just a few:

  • There are two separate heaps. You cannot allocate (explicitly with new, malloc, or so on -- or implicitly with strdup, strstreambuf::str, or so on), and then pass the pointer across a CRT-boundary to be freed.
  • You cannot pass a FILE* or file handle across a CRT-boundary and expect the "stdio low-level IO" to work.
  • You cannot set the locale in one and expect the other's locale to be set.

简单点说,当使用静态库链接时,会有多份运行时库,而且每份库都拷贝一份自己的内存管理。而使用动态链接后,由于都是链接的同一个运行时库,这样就保证内存管理只用一份了。但是动态链接时必须两个模块使用同样版本的运行时库,不同版本的依旧会有多份。

在该案例中,由于A.exe是MD动态链接,B.dll是MT静态链接,导致有2份运行时库存在,在进行内存管理时就出错了,从而导致程序崩溃。

所以跨模块的内存管理,最好是由每个模块提供自己的分配和销毁接口函数,然后在模块外部通过这些接口的调用来控制对象的生命期,而不是在外部 new/delete。如果实在要用,可以使用微软提供了GlobalAlloc/GlobalFree这样的全局内存API,用它们的话跨模块也没有问题的。



模块连接C/C++运行库的静态版本和动态版本不同的疑问

2009-04-05 21:51:48 来自:xinyusuiyuan 浏览数:106
windows核心编程中提到,模块中存在如下代码C/C++ codeVoid EXEFunc(){PVOID pv = DLLFunc();free(pv);}PVOID DLLFunc(){return (malloc(100));}
在连接到C/C++运行库的静态版本时,free调用就会失败。为什么调用 malloc就不会失败呢?
 
changyue314回复于06日06点19分 
堆的范围出界,无权直接释放其他MODULE的动态分配指针,可以由其他MODULE提供释放函数即可。
 
走向世界回复于06日10点10分 
exe和dll都静态链接,则各有一个C运行库heap
C运行库利用一个链表来管理动态分配的内存,每malloc一笔,都会在local heap中登记信息修改链表,同样,free也相应会修改链表
跨模块边界传递的由malloc而来的指针地址再另一个模块中是不能被正常识别的,

==================
老外说了一堆,就是这意思,该仁兄言简意赅,只可惜很少人能够读懂!
 
快乐学习回复于06日13点16分 
看了各位的解释,我还是有个问题:
当模块连接C/C++运行时库的静态版时, free和malloc由连接器把这两个函数的代码复制到模块中(我的理解),那么,free和malloc不是都在一个模块中吗?
 
shandian回复于06日15点52分 
每个crt copy都有自己的堆
 
XESFANS回复于06日18点45分 
昨天与一老外也在讨论这个问题,这是他的答复,不一定正确,但是有他的经验:

And to answer your question about DLL and local heap. This time I mean the heap

Win16 dll could and usually had a local heap in local sense and that one was mapped into the global memory space and then dll was used system wide. Memory management simply required to create and destroy object from dll because memory management was such that exe did not know about the position of a dll memory in the global space. It was just using it. The memory management was never constructed to support sharing a heap unless specified that memory block is such.

Win32 is mapping each dll into one and the same process space. So we should not have a problem with asking a memory from dll giving it to dll deleting new-ing, whatever. Right? Alas, we are sure that this is true only if we use system allocator functions properly. They do everything as it should be done and we have no problem with pointers from and to dll's as long as the same allocators are used which might not be the case if the system libraries are different. The problem is that some dll's have kept "old" or somehow adjusted memory managers that know nothing about Win32 sharing capabilities. They behave as if we are still dealing with Win16.
I would not be surprised at all, and I have met them, such compilers and C implementations of free, maloc, new and delete even today.
And the problem is of course if different dll's use different allocators.

Even under Win32, a dll still has its own local heap, it is just accessible from the main process. If you would do this: load dll, create some object on its heap, save the pointer in exe, unload dll, you would not be able to delete that object any longer, the pointer you have is invalid: no dll - no local heap.

Still this behavior asks for attention. You may have a static member pointer in a dll. You create an object and save the pointer into the static member variable. You go to another dll and ask for the static member pointer and nothing... though the space is shared and every heap accessible, the space of static members can be separated.

And this all picky details together create very difficult situation during development. If different teams have to use the same classes over several dll's and the application has a heavy pointer usage - watch out !

All in all you have to know what memory manager you use to be certain.
 
kinglion回复于06日21点26分 
exe和dll都静态链接,则各有一个C运行库heap
C运行库利用一个链表来管理动态分配的内存,每malloc一笔,都会在local heap中登记信息修改链表,同样,free也相应会修改链表
跨模块边界传递的由malloc而来的指针地址再另一个模块中是不能被正常识别的,
 
逍遥居士回复于07日06点32分 

的确没读懂,上面的代码,free和malloc不是在一个模块中吗?
 
dengpeijun回复于07日10点26分 
malloc只是申请内存,自然不会失败;free的时候由于静态库和动态库(这时候DllFunc是从dll导出的函数吧)行为的不同会导致出错。你把malloc和free放在一个模块就没问题。

这个例子应该是告诉我们,不要在模块之间进行内存的申请和释放;如果有必要,应该使用API的内存管理函数,比如GlobalAlloc、GlobalFree等。
对于OLE、COM等需要跨进程的内存管理,应该使用OLE专用的IMalloc等方式。
 
rourou回复于07日14点47分 
对于此类设计,最好还是避免,应当把这种内存的使用封装起来。
这里我就不重复,有兴趣的参考一下我在另外一帖的回复。
http://topic.csdn.net/u/20090220/12/9b07f419-f1a5-4bd4-8f67-60b8d2609d23.html
 
zhangpei1993回复于07日19点08分 
可是你的exe和dll不是一个模块。
 
jaylmj回复于08日06点06分 
up
 
潇洒无名回复于08日11点53分 
所谓静态库就是直接把中间代码.obj或.o通过link编译到可执行的二进制编码中,你可以把静态库看作为你自己代码的一部分,所以free和malloc当然是属于同一模块中了。
 
hanshiyu回复于08日17点56分 
这是我的原贴:

fifth Edition
in Chapter 19, there is one note:

Note: It is important to realize that a single address space consists of one executable module and several DLL modules. Some of these modules can link to a static version of the C/ C++ run-time library, some of these modules might link to a DLL version of the C/C++ run-time library, and some of these modules (if not written in C/C++) might not require the C/ C++ run-time library at all. Many developers make a common mistake because they forget that several C/C++ run-time libraries can be present in a single address space. Examine the following code:

VOID EXEFunc() {
PVOID pv = DLLFunc();
// Access the storage pointed to by pv...
// Assumes that pv is in EXE's C/C++ run-time heap
free(pv);
}

PVOID DLLFunc() {
// Allocate block from DLL's C/C++ run-time heap
return(malloc(100));
}

So, what do you think? Does the preceding code work correctly? Is the block allocated by the DLL's function freed by the EXE's function? The answer is: maybe. The code shown does not give you enough information. If both the EXE and the DLL link to the DLL C/C++ run-time library, the code works just fine. However, if one or both of the modules link to the static C/C++ run-time library, the call to free fails. I have seen developers write code similar to this too many times, and it has burned them all.


===========================================================================

At first one thing the starup() function do is to initialize a heap used by malloc and free and low-level IO manipulation use in CRT, so when there is two copies CRT liberary in one process adress and there will be two heaps for two groups of malloc and free, and that one block memory grabed by malloc can only be freed by the very free() function in the same copy.

but what free() is?

in fact free()'s source code is like this:

void free(void *ptr)
{
struct mem_control_block *free;
free = ptr - sizeof(struct mem_control_block);
free->is_available = 1;
return;
}


every malloc()function return pointer to a block,however there are some bytes ahead the block conserved for system information, and this information is stored in a struct mem_control_block, so indeed the pointer malloc()function returned points to the byte after the mem_control_block.

struct mem_control_block {
int is_available; //a flag that specify whether this block can be used by any other application
int size; //the size of the block
};


finnally, it seems no error in these code:

VOID EXEFunc() {
PVOID pv = DLLFunc();
// Access the storage pointed to by pv...
// Assumes that pv is in EXE's C/C++ run-time heap
free(pv);
}

PVOID DLLFunc() {
// Allocate block from DLL's C/C++ run-time heap
return(malloc(100));
}

if the system doesn't allow a free() to free a block in heap of another copy of CRT, then how the system is able to forbid?????

Thanks for your help.
I'm struggle reading win via c/c++
 
hxf妈妈回复于09日06点10分 
一般在dll中分配的内存,都需要通过dll释放。




许式伟
2004年6月21日

由于编译器、编译模式的不同,不同模块的内存结构与管理程序往往并不相同。因此,如果我们在一个模块申请内存,而在另一个模块中释放,这是一个不安全的做法。因为模块在释放内存的时候,并不会预料到需要释放的内存并非是自己管理的。
 
直观的说,假设我们有两个模块:Module1,Module2。它们有函数Module1.alloc,Module1.free;Module2.alloc, Module2.free。虽然同为alloc和free,但是你不能假设Module1.alloc/free和Module2.alloc/free是同一份代码。因此,Module1.alloc申请的资源,只有由Module1.free去释放才是确保安全的。
  
展开来讲,我们通常有以下跨模块调用约定: 

1)    不能够在一个模块的引出函数、接口中申请内存并且返回出去,让另一模块释放它。

如果有此需求,尝试用以下几种解决方案:
  1. 考虑提供一个函数,让外部模块获得所需的内存大小,让外部模块申请内存并传入。这是一个比较典型的解决方案,Windows的API均采用此解决方案。
     
  2. 考虑使用一个接口包装此内存的访问,让外部模块获得接口指针,并以此访问内存。内存是通过接口的Release()函数释放的。这样就保证了内存释放的正确性。
     
  3. 考虑使用CoTaskMemAlloc/CoTaskMemFree申请、释放内存。因为内存的申请、释放由系统完成,故可以保证其一致性。
     
  4. 作为条目3. 的特殊情形,如果返回的是字符串,可考虑用BSTR。此时资源管理由系统调用SysAllocString/SysFreeString 实现。
     
  5. 仍然在内部申请内存并返回出去,同时将该内存的释放函数也作为引出函数引出去。外部模块使用完该内存后,用我们引出的释放函数释放它。这是可行的方案,虽然比较少见。你可以认为其实CoTaskMemAlloc/CoTaskMemFree、SysAllocString/SysFreeString也是基于这条规则提供的,只不过它没有特定目的而已。

◆注意◆

有时候出于某种考虑(例如检测内存资源泄漏),我们可能提供一个自己实现的Win32 API版本来取代Windows的系统调用。

我们知道,如果你使用CoTaskMemAlloc/CoTaskMemFree、SysAllocString/SysFreeString来申请、释放内存,那么哪怕存在内存泄漏,我们在《最快速度找到内存泄漏》中介绍的方法并不能检测出来。

除了使用一些系统资源泄漏的检测工具(其实它们的方法和我们这里介绍的肯定也类似)外,一种方法,就是提供这些API的替换版本。这些替换版本中,我们提供了泄漏检测的能力。

我们这里并不准备详细讨论这个技巧。但是请注意,这里存在的潜在危险是,有可能出现这样的情形:设想我们的某DLL使用了替换版本的SysAllocString,其中申请了一个BSTR返回给另一DLL,而该DLL并不使用替换版本的SysFreeString,而是调用系统的SysFreeString释放这个BSTR。这里存在的问题是显然的,因为系统并没有分配过这样一个BSTR。

2)    不能够在参数列表或返回值中用到类。

这是因为:

  1. 同一个类,相同的声明,在不同的编译器、甚至不同的编译模式下,会有不同的内存布局。也就是说,看似是同一个类,但是其实在不同的模块中,理解上根本不同。例如,你用VC++写一个DLL,该DLL返回一个std::string,而DLL的客户程序是C++ Builder写的。你能够保证C++ Builder的std::string与VC++的内存布局一致吗?
     
  2. 类存在成员函数(特别是构造、析构),这些成员函数对我们来说是个黑箱操作。对他们的调用同样容易产生这样的情况,就是在我们的模块中申请了内存,而在外部模块中(由析构函数)释放。仍然以std::string为例。我们往往为了方便返回一个字符串,而将函数声明为:
        ⑴ std::string getXXX();
    或者:
        ⑵ getXXX(std::string& str);
    这种方式在同一模块中是可行的,而且是相对比较高效的方式。但是如果用于跨模块的字符串传递,则存在风险(并不一定会出问题,关于什么时候不出问题,我们下一回讨论)。 

遇到这种要用类的情形。请尝试采用以下方案:

  1.  考虑采用纯结构体
  2. 考虑使用一个接口包装该类,将该类实现为COM组件。
  3. 如果返回的是字符串,考虑用BSTR。 
     

纯结构体

所谓“纯结构体”,是指该结构体:
  1. 没有任何虚拟的成分。如虚函数、虚拟继承等。
     
  2. 它的所有成员变量,均为简单数据类型(C标准数据类型,不包括指针),或者是另一个“纯结构体”。
     
  3. 如果成员变量是一个指针,那么要么作为输入参数,指向的内容是一个纯结构体或C标准字符串;要么作为输出参数,指向的内容是一系统分配的资源。
总的说来一句话,就是“纯结构体”成员的类型要求,完全等同模块的引出函数参数类型的要求。
 
纯结构体在接口定义中比较广泛,往往用于取代在接口使用类的需求。对于我们规范中的“不允许使用类”,有一个误区是,使用了一个struct关键字定义的,本质上还是类的东西。例如:
struct  AStruct
{
   std::
string  strA;
   std::
string  strB;
};
这个struct有构造、析构(尽管没有显式写出,编译器帮你生成的),析构中有内存释放操作,是一个标标准准的“类”。
 
另外,结构体需要显式指定字节对齐方式。例如:
#pragma  pack(push, 1)
struct  XXXX
{
    ...
};
#pragam pack(pop)

附加说明:

对“内存管理”相关的技术感兴趣?这里可以看到我的所有关于内存管理的文章

查看评论
3楼 eXile_2006-12-27 12:12发表 [回复]
对于C++, 在同一个项目中不同的dll之间,可以使用boost::shared_ptr 进行资源管理
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值