Heap allocation failing in user DLL/EXE

2019-08-23 07:58发布

Properly linked DLLs and EXEs are supposed to have one freestore from which they all can allocate heap-based objects. Here is Chis Becke’s Answer in Who allocates heap to a DLL?:

… it is the C++ runtime that is responsible for creating its freestore and deciding how to allocate it. Specifically, if you use the Dll runtime option, then a single dll - msvcrtxx.dll - manages a single freestore that is shared between all dll's, and the exe, that are linked against that dll

Since this is true, then I should be able to new objects in DLL/EXEs defined in other DLL/EXEs. According to Chris, the msvcrtxx.dll and compile-time/runtime linker take care of where the joint freestore for all the DLL/EXEs can be obtained.

That is not working for me.

To test this, I have generated two MFC dialog programs: NewFailMfc1 and NewFailMfc2. Running NewFailMfc2 which accesses NewFailMfc1’s Www function fails when doing the new.

// Code in NewFailMfc1.
void Www()
{
  char* ch { nullptr };
  ch = new char[ 100 ]; // error: attempts to allocate memory somewhere else than in the prescribed joint DLL/EXE freestore
  ch[ 0 ] = '\0';
}

// Calling code in NewFailMfc2.
Www();

Does someone with a better knowledge of how DLL/EXE freestore works than me know what the problem is?

(I attempted to ask this question once before in "Global function ::operator new fails when compiled in MyApp1 and MyApp2. During the asking process, I discovered that the problem was occurring more generally than in the <random> std lib.)

EDIT1:

In MSDN a nice virtual agent found Potential Errors Passing CRT Objects Across DLL Boundaries for me. Unfortunately, the only solution it recommends is compiling all your programs with the /MD compiler option, and not /MT which uses multiple copies of the CRT which automatically leads to crossing boundaries and memory access violations.

This is not good news for an app developer like me. What I need is a Best Practice so I can apply it and meet my delivery deadlines without having to deal with arcane low-level memory problems. How would I fx know there is a hidden call to the global ::operator new in the std:random_device type? I wouldn’t until it access-violated. Only now after all this research do I realize that by it calling the global new, it was crossing a boundary which gave my DLL/EXE an access violation. Very obscure.

EDIT2:

I have submitted a bug report in Visual Studio regarding the std::random_device implementation. See "std::random_device instantiation causes an access-violation in certain cases".

4条回答
何必那么认真
2楼-- · 2019-08-23 08:34

Explicit Instantiation

Explicit Instantiation forces the compiler to generate code for a specific parameter list of a templated class or function. Without that code my imported DLL/EXE binaries were failing on runtime instantiations like ch = new char[ 100 ] and std::random_device rd; which implicitly does a global ::operator new. I have found no useful explanation for why this occurs. IPC discussions unfortunately don’t clearly distinguish between client-server runtime involving more than one running process, and runtime code that imports binary code (DLL/EXE) that was compiled and exported elsewhere.

The solution was to add a template parameter to the class that was failing and add a .cpp file to each module that explicitly instantiated the class, like MyClsModule1.cpp, MyClsModule2.cpp, etc. In these files I explicitly instantiate the class for that module. I also add the .h files, like MyClsModule1.h, MyClsModule2.h, for each module which contain externs so duplicate code generations don’t occur within a particular module. With this approach each module generates module-specific class code at compile time, which forces the module’s threads to allow heap instantiations which access that module’s process heap.

This modern C++ solution is elegant for me in that it keeps me from having to re-introduce complex IPC solutions like COM into my app code.

From an app developer’s perspective I think that my original code ought to have worked or at least have generated errors that were more informative as to what the problem was, so I am leaving my bug report mentioned in EDIT2 in effect.

查看更多
Luminary・发光体
3楼-- · 2019-08-23 08:35

The Multi-threaded Debug DLL heap would be per process and hence NewFailMfc1 and NewFailMfc2 would be having their own private heaps even when both the applications are linked with the Multi-threaded Debug DLL. Using the Multi-threaded Debug DLL heap only solves the problem of crossing boundaries across multiple heaps within the same process address space and is not a mechanism that can be used to share heap across process boundaries.

查看更多
Lonely孤独者°
4楼-- · 2019-08-23 08:53

forget this all.if this works somehow this just your luck.

Way better is to run COM for memory sharing.Just look at IMalloc sample here. ftp://210.212.172.242/Digital_Library/CSE/Computer,%20Technology%20and%20Engineering%20eBooks/Books7/petzold_rus.part1%20(2)/DISK/CODE/CHAP20/

Not that easy yea.

As was articulated earlier in this specification, when ownership of allocated memory is passed through an interface, COM requires that the memory be allocated with a specific.... http://www.opengroup.org/comsource/techref2/CHP05GDC.HTM

wanna mention that in posted sample(win95 times) you're gathering IMalloc ole interface from ground stage . It may considered as MS Windows source code's piece. Idk anything about today's built-n IMalloc,not sure it's same.

查看更多
放我归山
5楼-- · 2019-08-23 08:56

It's possible to cross boundaries whatever that means :) First of all, you need to understand what's going on.

When you allocate memory, actually CRT can allocate a little bit more than you asked. E.g. popular practice (at least in the past) was to allocate 4 bytes more (substitute by your system bitness), write the size of allocated memory at the beginning and return ptr + 4 to you. So when you release memory the system knows how much it should release.

It's a bit simplified picture. Different compilers, different versions of same compiler and different configs of same compiler same version can do this differently. E.g. debug config can use some padding to detect buffer overruns, and other tricks. So when you allocate memory in one binary and deallocate in another, if different compilers where used this can lead to a corrupted memory (immediate crash in the best case).

This and many other reasons lead to a common suggestion: release memory in the binary where you allocated it. This is usually achieved by providing Release member function of your API class and making your destructor private, or by unique_ptr (or shared_ptr) with custom deleter, or other techniques.

Now about /MD suggestion. /MD means dynamic CRT (= in a dll), and as it's not possible to load same dll twice in the same process this means that same CRT will be used for allocation and deallocation. This is still not a solution for a different versions or different compilers. E.g. many applications use plugins system, in this case it's not a very good idea to demand that all plugins are compiled by a specific compiler/version/config

查看更多
登录 后发表回答