场景描述
在Windows平台上使用C++开发了一个服务,其中组合了各种各样的第三方组件,一般以lib/dll和头文件的形式使用。有这样一种场景,如下图所示,应用程序申请了一段内存ptr
, 但在调用lib.dll的函数接口中其调用了free(ptr)
。一般来说我们也尽量避免在一个组件中申请内存,而在另一个组件中释放,这里恰巧是一个bug导致了跨组件的内存申请和释放。
那么请各位读者思考一下,这样会有问题吗?如果你是一个老司机,也许已经发现,在某些情况下会在调用free(ptr)
的时候导致程序crash。
为何crash
熟悉Windows编程的读者应该了解如下图所示,操作内存的方式有如下几种:
-
直接
VirtualAlloc
之类的函数,可以申请一段虚拟地址空间,并且使用这段空间 -
直接使用
HeapCreate
,HeapAlloc
之类的函数,来创建堆,并且使用堆。也就是说Windows的进程可以有多个堆,一般进程启动有一个默认的堆。而Heap
的底层实际是采用VirtualXXX
之类的函数进行控制的。 -
Local或者Global Memory API,主要是从进程默认堆中申请或者释放内存
-
CRT库中调用
malloc
去申请内存,而这里是本文的重点。其底层实际是使用的Windows Heap API实现。 -
最后一个就是Memory Mapped File。
在第一个章节我们描述的内存申请释放场景,是采用了CRT库的方式。CRT库的链接方式有四种:
-
/MT
静态链接进你的组件。也就是说当你采用这个编译选项的时候CRT的的代码也被链接进了你的DLL或者Exe。 -
/MD
这种链接方式,实际上在应用程序运行的时候,才会加载对应的CRT库的DLL。 -
/MTd
和/MDd
主要针对Debug Build,链接的方式和上面两种一一对应,不再赘述。
那么我们再来看下第一章节的场景,他们分别采用如下编译方式:
-
APP.exe
采用/MD
编译,也就是会在运行的时候,装载CRT库的DLL,调用的malloc
也是在这个DLL里面。 -
lib.dll
采用的/MT
编译,那么在调用free
的时候会调用链接在lib.dll
中的CRT库中的free
留给读者一点时间,这样的场景调用会有什么问题呢?
这个问题涉及到的是CRT库malloc
的实现问题,之前我们已经了解到了CRT库采用的是HeapXXX
相关的API。那么CRT库是采用HeapCreate
创建了新的堆,还是使用进程默认堆呢?
微软的CRT库是开源的,lib.dll采用的是VS2010编译的,CRT库会使用HeapCreate
创建新的堆。而APP.exe采用的是VS2015编译的 (因为App.exe和lib.dll不是同一个团队做的,有可能编译器版本不同),其对应版本的CRT库是使用的系统默认堆。那么APP.exe中malloc
的内存是系统默认堆里申请并且管理的,而在lib.dll
中free却会从自己创建的堆中去寻找,寻找不到对应的分配的地址,从而导致了程序Crash。
那么这个章节留两个问题给大家,如果APP.exe和lib.dll继续使用原先的链接CRT库的方式:
-
APP.exe和lib.dll均采用VS2010编译,第一章节的场景还会Crash吗?
-
APP.exe和lib.dll均采用VS2015编译,第一章节的场景还会Crash吗?
如果这两个问题能够回答正确,说明你已经理解这个问题啦。如果没有答出来,欢迎我们一起讨论。
建议
在编程的道路上,到处都是坑,有新的挖的也有前人留下的坑。那么就尽量用自己的经验去防御性编程,减少可能存在的坑:
-
在一个应用程序中,所有自己可控的组件均采用
/MD
的方式去链接CRT库 -
尽量不要在一个模块中申请内存,在另外一个模块中释放。比如你实现一个动态链接库(DLL),提供一个接口
FuncA
申请并返回内存地址,那么最好提供一个接口FreeXX
去释放FuncA
申请的内存。