最近在开发的时候,特别是遇到关于跨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. 【总结】IntelliJ IDEA 插件

    1..iBATIS/MyBatis plugin轻松通过快捷键找到MyBatis中对应的Mapper和XML,CTRL+ALT+B 2.iBATIS/MyBatis plugin轻松通过快捷键找到My ...

  2. nacos 安装和使用

    Nacos 是阿里巴巴开源项目,用于构建微服务应用的服务发现.配置管理和服务管理. 在微服务项目中不同模块之间服务调用时,实现服务注册与发现. Nacos 使用: Nacos 是java开发的,依赖 ...

  3. SpringBoot整合Swagger2一直弹窗的坑

    问题现象: 我的Swagger配置信息文件如下 package com.qbb.qmall.service.config; import com.google.common.base.Predicat ...

  4. 华企盾防泄密软件关于U盘无法注册问题

    1.以管理员权限运行控制台注册 2.如果是非加密注册可在USB拔插日志中右键日志远程注册 3.检查USB的驱动程序注册表是否都有 4.换一台电脑安装控制台注册 5.检查是否有与驱动有关的程序卸载试试 ...

  5. 一篇可供参考的 K8S 落地实践经验

    前言 k8s 即 Kubernetes,是一个开源的容器编排引擎,用来对容器化应用进行自动化部署. 扩缩和管理 本篇文章将分享 k8s v1.18.8 的安装,以及其面板,监控,部署服务,使用Ingr ...

  6. 从零玩转设计模式之原型模式-yuanxingmoshi

    title: 从零玩转设计模式之原型模式 date: 2022-12-11 20:05:35.488 updated: 2022-12-23 15:35:44.159 url: https://www ...

  7. redis + AOP + 自定义注解实现接口限流

    限流介绍 限流(rate limiting) ​ 是指在一定时间内,对某些资源的访问次数进行限制,以避免资源被滥用或过度消耗.限流可以防止服务器崩溃.保证用户体验.提高系统可用性. 限流的方法有很多种 ...

  8. Ubuntu安装Maridb 10.5版本

    以20.04版本为例 Ubutun20.04自带源默认安装的mariadb版本为10.3不符合安装zabbix6.0的要求 打开MariaDB的官方网站:https://mariadb.org/mar ...

  9. ubuntu 之 go+/goplus 安装

    目前情况是要安装 goplus/go+ 之前 必须先安装 golang golang下载地址:https://golang.google.cn/dl/ 或者 https://studygolang.c ...

  10. 【manim动画教程】--目录(完结)

    manim是一个生成数学教学视频的动画引擎. 它用编程的方式创建精美的数学动画,让数学更加易懂. 本教程简单介绍了 manim 的基本使用方式,基于 v0.17.2 版本 manim 安装 manim ...