最近在开发的时候,特别是遇到关于跨DLL申请对象、指针、内存等问题的时候遇到了这么一个问题。

问题 跨DLL能不能调用到DLL中提供的单例?

问题比较简单,就是我现在有一个进程A,有DLL B DLL C,这两个DLL都依赖DLL D的单例,这个时候如果A调用了DLLB 和 DLL C,那么B和C能否正确引用到这个指定的单例?

其实这个问题我心中想的是可以使用这个进程的,但是在实际开发中我们却有了争议,有的人认为可以,有的人认为不可以,但是我对这个内存的所有权一直不太清楚,所以这也算是给自己一个解惑,一个交代。这篇文章也是我边查资料边写的,有引用很多文章,主要是为了引出结论和一些别的问题。

聊C/C++的动态内存管理的内容之前,我们先来了解一下进程中的内存分区,如上所示。我们可以通过查看程序的完整启动过程去看这些内存分区。

内存分区

在C++中,将操作系统分配给程序的内存空间按照用途划分了代码段、数据段、栈、堆几个不同的区域,每个区域都有其独特的内存管理机制。

代码区

只存代码本身,几乎不需要考虑

代码区是用于存储程序代码的区域,代码段在程序真正执行前就被加载到内存中,在程序执行期间,代码区内存不会被修改和释放。由于代码区是只读的,所以会被多个进程共享。在多个进程同时执行同一个程序时,操作系统只需要将代码段加载到内存中一次,然后让多个进程共享这个内存区域即可。

数据段

纯粹的数据,几乎不用考虑

数据段用于存储静态全局变量、静态局部变量和静态常量等静态数据。在程序运行期间,数据段的大小固定不变,但其内容可以被修改。按照变量是否被初始化。数据段可分为已初始化数据段和未初始化数据段。

存放局部变量和函数调用。局部变量访问出错和函数死循环往往会导致stacks overflow。

C++中函数调用以及函数内的局部变量的使用,都是通过栈这个内存分区实现的。栈分区由操作系统自动分配和释放,是一种"后进先出"的一种内存分区。每个栈的大小是固定的,一般只有几MB,所以如果栈变量太大,或者函数调用嵌套太深,容易发生栈溢出(stack overflow)。

一般就是我们new出来的指针,是一些内存空间。当内存地址所有权不明或者内存出错就容易出现堆损坏问题。

用于存放在程序运行时被动态分配的内存段。堆的大小不固定,可以动态增加和减少。使用malloc()等函数动态分配内存到堆上,使用free()等函数释放对应的动态分配内存。堆的最大容量受限于系统中有效的虚拟内存。

栈和堆?

区别:

1、申请方式:栈的空间由操作系统自动分配和释放,堆上的空间需要程序员使用malloc\free手动分配和释放。如果不释放会造成内存泄漏。

2、申请大小限制和效率:栈的空间时有限的,在linux中,使用 ulimit -s 指令 ,可以看到看到栈的容量为8M。栈区以先进后出的方式自动分配时连续的内存单元,效率高。

堆的大小受限与系统中有效的虚拟内存大小,系统是用链表来存储空闲的内存块,是不连续的。因此,堆的空间分配比较灵活,但容易产生内存碎片,相对来讲对效率低。

对于一个程序而言,大概的模型是这样的

以启动Windows系统中的exe程序为例

当我们双击某个exe程序或者通过桌面等快捷方式去启动一个exe程序时,就进入exe程序的启动过程。

程序启动时,系统首先会将exe主程序依赖的所有的dll库文件(包括exe程序自带的dll以及exe程序依赖的系统dll)都加载到进程空间中,这些dll二进制文件中存放的是可执行的二进制机器代码(即汇编代码,机器码与汇编代码等价的,汇编代码是机器码的助记符),都加载到进程的代码段的内存区中。

待所有依赖的dll模块都加载到进程空间后,最后才会将exe主程序加载到进程空间中。然后去启动C/C++的运行时库,紧接着去给全局变量分配内存并执行全局变量的初始化操作,此处对应的就是全局内存区。然后才会进入到main函数,程序才能真正的启动并运行起来。

进入到函数中,就会从所在线程的栈内存上给函数的局部变量分配栈内存,这就是我们讲的栈内存。当执行到malloc或new等代码时,申请的内存就是堆内存。

回到问题

这个问题其实实际上还没有涉及到内存所有权问题,但是我觉得可以当作一个引子:

static变量的唯一性是动态库级别的,不同库包含同一截代码的话就会有多份static实例。出现问题的情况往往是,这截代码是实现在头文件里的,所以被不同模块引用时就生成了多份代码。解决办法就是按照一般DLL导出接口的规则,在获取static变量的地方:

1.对于实现单例的dll,使用dllexport。对于MSVC来说就是__declspec(dllexport),对于GCC/clang就是__attribute__ ((dllexport))

2.对于引用单例的dll,使用dllimport。对于MSVC/clang来说就是__declspec(dllimport),对于GCC/clang就是__attribute__ ((dllimport))这样就只有一份了。

所以跨DLL的单例模式是可行的,这个静态变量(单例对象)必须导出,否则这个静态对象的实现会在不同的代码头文件引用的过程中一同被申请出来。

解决方案:

1.设计代码时选好单例的内存该放在哪,然后通过导入导出一解决(MSVC上导入导出使_declspec(dllimport)和_declspec(dllexport),类比extern变量一)。比如单例打算放在动态链接库A中,那就在生成动态库A的地方的这个static变量导出,其他动态链接库°或可执行文件使用的地方导入。

2.搞个统一的地方大家的单例共同注册到一起,用的时候拿出来。

tips:

Windows使用注意事项:

  1. 要求使用MD/MDd,而不是MT/MTd,反正至少保证要保证exe和所有dll使用同一间使用同一个crtheap堆,否则exe和不同dll间都有各自的crtheap堆,而new和delete需要在同一堆中配套执行。
  2. 慎用virtual,析构函数不能为virtual函数。因为当析构函数为virtual函数时,如果单例由dll1加载,而dll2在dll1卸载之后如果还在使用,那析构时调virtual的析构函数会去查虚函数表,而虚函数表是由dll1创建的,会引发崩溃。其他虚函数也涉及虚函数表,因此若要使用虚函数,那么除非能保证dll的卸载顺序,否则不要使用热卸载。事实上,全局单例的管理交由单例框架来实现后,析构函数是否使用virtual都不会产生泄漏,因为单例框架构造和析构时使用的都是具体的全局单例类,而不会是它们的基类。
  3. 单例全为懒加载,直到GetReference的时候才真正实例化单例对象,需要注意全局单例没有保证多线程间安全,因此在单例实例化/动态库首次获取单例时都是线程不安全的。若在SingletonManager.cpp的Count/Obtain/Release函数中使用std::mutex加锁能够实现单例获取的安全,但实例化过程(创建过程)仍是线程不安全的。

下一期来聊聊关于跨DLL引用DLL的时候导致的所有权问题

C++跨DLL内存所有权问题探幽(一)DLL提供的全局单例模式的更多相关文章

  1. [转]System.DllNotFoundException: 无法加载 DLL“*.dll”: 内存位置访问无效。 (异常来自 HRESULT:0x800703E6)

    我在使用地税发票控件进行开票的测试的时候,在xp上测试时正常的,在别人的win7系统测试也是正常,但我在我本机确不正常.我本机装的是msdn版本win7系统,这个系统比较原装. 错误信息如下: -- ...

  2. DLL内存分配与共享

    一旦DLL的文件映像被映射到调用进程的地址空间中,DLL的函数就可以供进程中运行的所有线程使用.实际上,DLL几乎将失去它作为DLL的全部特征.对于进程中的线程来说,DLL的代码和数据看上去就像恰巧是 ...

  3. Delphi之DLL知识学习1---什么是DLL

    DLL(动态链接库)是程序模块,它包括代码.数据或资源,能够被其他的Windows 应用程序共享.DLL的主要特点之一是应用程序可以在运行时调入代码执行,而不是在编译时链接代码,因此,多个应用程序可以 ...

  4. DLL入门浅析(2)——如何使用DLL

    转载自:http://www.cppblog.com/suiaiguo/archive/2009/07/20/90621.html 上文我简单的介绍了如何建立一个简单DLL,下面再我简单的介绍一下如何 ...

  5. DLL入门浅析(1)——如何建立DLL

    转载自:http://www.cppblog.com/suiaiguo/archive/2009/07/20/90619.html 初学DLL,结合教程,总结一下自己的所得,希望对DLL初学者们有所帮 ...

  6. [转载]解析WINDOWS中的DLL文件---经典DLL解读

    [转载]解析WINDOWS中的DLL文件---经典DLL解读 在Windows世界中,有无数块活动的大陆,它们都有一个共同的名字——动态链接库.现在就走进这些神奇的活动大陆,找出它们隐藏已久的秘密吧! ...

  7. DLL注入新姿势:反射式DLL注入研究

    在分析koadic渗透利器时,发现它有一个注入模块,其DLL注入实现方式和一般的注入方式不一样.搜索了一下发现是由HarmanySecurity的Stephen Fewer提出的ReflectiveD ...

  8. .lib .dll 区别介绍、使用(dll的两种引入方式)

    .lib .dll文件都是程序可直接引用的文件,前者就是所谓的库文件,后者是动态链接库(Dynamic Link Library)也是一个库文件.而.pdb则可以理解为符号表文件.DLL(Dynami ...

  9. 关于Dll、Com组件、托管dll和非托管dll

    转自:https://blog.csdn.net/black_bad1993/article/details/53906252 Com组件 1.线程模型是干嘛用的?解决"多个线程" ...

  10. java.lang.UnsatisfiedLinkError: C:\apache-tomcat-8.0.21\bin\tcnative-1.dll: Can't load IA 32-bit .dll on a AMD 64-bit platform

    Tomcat启动报错: 25-Mar-2016 10:40:43.478 SEVERE [main] org.apache.catalina.startup.Catalina.stopServer C ...

随机推荐

  1. C++ Qt开发:使用顺序容器类

    当我们谈论编程中的数据结构时,顺序容器是不可忽视的一个重要概念.顺序容器是一种能够按照元素添加的顺序来存储和检索数据的数据结构.它们提供了简单而直观的方式来组织和管理数据,为程序员提供了灵活性和性能的 ...

  2. [CF1824D] LuoTianyi and the Function

    题目描述 LuoTianyi gives you an array $ a $ of $ n $ integers and the index begins from $ 1 $ . Define $ ...

  3. 华企盾DSC由于半透明软件设置了需要管理员权限打开导致半透明打不开加密文件

    解决方法: 1.右键该应用程序->属性->兼容性,去掉[以管理员权限运行此程序] 2.也可以打开控制面板->系统和安全->用户账户控制设置调至最低

  4. 我的大数据之路 - 基于HANA构建实时方案的历程

    产品内部前期有一个共识,依据业务要求的时效性来选择技术平台,即: 实时类业务,时效性小于2小时,则使用HANA构建. 离线类业务,时效性大于2小时,则使用大数据平台构建. 经过五月.六月两月的努力,离 ...

  5. loader编写小记

    此项目在一些大佬的基础上进行了修改,或许能提供一些思路.还在学习中很菜很菜,不足之处还请师傅们多多指点 tips 对shellcode使用AES + Base85加密后以txt保存在远端供下载. 针对 ...

  6. java后端工具类之从0到1生成数字证书(RSA)

    开放签电子签章系统-开源版这周(2023-12-15)就要上线发布了,为了让开源版能够更好的服务广大研发工程师,接下来会详细的说说开源版从代码层面的具体实现,以便大家在工作过程中更好使用开放签电子签章 ...

  7. 【开源】EDUCN网站

    EDUCN https://scrc.rth1.link/ <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN&q ...

  8. Java 展开或折叠PDF中的书签

    PDF中的书签功能可快速定位到指定阅读位置.对多层书签可根据阅读喜好设置层级展开或折叠.本文将通过Java程序代码介绍如何来实现PDF书签展开或折叠. 程序环境 Spire.Pdf.jar( 免费版3 ...

  9. 前端面试常考题:JS垃圾回收机制

    摘要:众所周知,应用程序在运行过程中需要占用一定的内存空间,且在运行过后就必须将不再用到的内存释放掉,否则就会出现下图中内存的占用持续升高的情况,一方面会影响程序的运行速度,另一方面严重的话则会导致整 ...

  10. 云小课 | 守护网络安全不是问题,iptables的四表五链为你开启“八卦阵”

    摘要:担心网络基本安全?iptables八卦阵为您守护!本文带您一起了解iptables的相关知识. 网络世界就和现实世界一样,总是会有些不怀好意的"人"出现,扫扫你的端口啊,探测 ...