dll多次实例化时,系统会为每个实例分配独立的内存空间,静态变量也不例外,要想多个dll实例共用同一静态变量,目前查到最好的方法是通过编译参数,实现这个目的。Linux和Windows下均有各自的方法。
以下是我觉得比较好的两篇文章,附上出处,由于是简单拷贝,失去了原文格式,建议去看原文。
声明:由于同时转载了两篇文章,若发布类型选择转载只能填写一个转载链接,故发布类型选择原创,本文章实为转载,请支持原创。
Windows下实现:
版权声明:本文为CSDN博主「zslInSz」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/wyyzsl212328/article/details/8054047
为防止原文失效,以下是转载的文章内容,由于是简单拷贝,失去了原文格式,建议去看原文。
————————————————————————————————
全局数据和静态数据不能被同一个. e x e或D L L文件的多个映像共享,这是个安全的默认设置。但是,在某些情况下,让一个. e x e文件的多个映像共享一个变量的实例是非常有用和方便的。例如,Wi n d o w s没有提供任何简便的方法来确定用户是否在运行应用程序的多个实例。但是,如果能够让所有实例共享单个全局变量,那么这个全局变量就能够反映正在运行的实例的数量。当用户启动应用程序的一个实例时,新实例的线程能够简单地查看全局变量的值(它已经被另一个实例更新);如果这个数量大于1,那么第二个实例就能够通知用户,该应用程序只有一个实例可以运行,而第二个实例将终止运行。
每个. e x e或D L L文件的映像都由许多节组成。按照规定,每个标准节的名字均以圆点开头。例如,当编译你的程序时,编译器会将所有代码放入一个名叫. t e x t的节中。该编译器还将所有未经初始化的数据放入一个. b s s节,而已经初始化的所有数据则放入. d a t a节中。
每一节都拥有与其相关的一组属性,这些属性如表1 7 - 1所示。
表17-1 .exe或D L L文件各节的属性
属性 含义
R E A D 该节中的字节可以读取
W R I T E 该节中的字节可以写入
E X E C U T E 该节中的字节可以执行
S H A R E D 该节中的字节可以被多个实例共享(本属性能够有效地关闭c o p y - o n - w r i t e机制)
使用M i c r o s o f t的Visual Studio的D u m p B i n实用程序(带有/ H e a d e r s开关),可以查看. e x e或D L L映射文件中各个节的列表。下面选录的代码是在一个可执行文件上运行D u m p B i n程序而生成的:
SECTION HEADER #1
.text name
11A70 virtual size
1000 virtual address
12000 size of raw data
1000 file pointer to raw data
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
60000020 flags
Code
Execute Read
SECTION HEADER #2
.rdata name
1F6 virtual size
13000 virtual address
1000 size of raw data
13000 file pointer to raw data
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
40000040 flags
Initialized Data
Read Only
SECTION HEADER #3
.data name
560 virtual size
14000 virtual address
1000 size of raw data
14000 file pointer to raw data
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
C0000040 flags
Initialized Data
Read Write
SECTION HEADER #4
.idata name
58D virtual size
15000 virtual address
1000 size of raw data
15000 file pointer to raw data
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
C0000040 flags
Initialized Data
Read Write
SECTION HEADER #5
.didat name
7A2 virtual size
16000 virtual address
1000 size of raw data
16000 file pointer to raw data
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
C0000040 flags
Initialized Data
Read Write
SECTION HEADER #6
.reloc name
26D virtual size
17000 virtual address
1000 size of raw data
17000 file pointer to raw data
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
42000040 flags
Initialized Data
Discardable
Read Only
Summary
1000 .data
1000 .didat
1000 .idata
1000 .rdata
1000 .reloc
12000 .text
表1 7 - 2显示了比较常见的一些节的名字,并且说明了每一节的作用。
除了编译器和链接程序创建的标准节外,也可以在使用下面的命令进行编译时创建自己的节:
表17-2 常见的节名及作用
节名 作用
. b s s 未经初始化的数据
. C RT C运行期只读数据
. d a t a 已经初始化的数据
. d e b u g 调试信息
. d i d a t a 延迟输入文件名表
. e d a t a 输出文件名表
. i d a t a 输入文件名表
. r d a t a 运行期只读数据
. r e l o c 重定位表信息
. r s r c 资源
. t e x t . e x e或D L L文件的代码
. t l s 线程的本地存储器
. x d a t a 异常处理表
#pragma data_seg("sectionname")
我可以创建一个称为“S h a r e d”的节,它包含单个L O N G值,如下所示:
#pragma data_seg("Shared")
LONG g_lInstanceCount = 0;
#pragma data_seg()
当编译器对这个代码进行编译时,它创建一个新节,称为S h a r e d,并将它在编译指示后面看到的所有已经初始化(i n i t i a l i z e d)的数据变量放入这个新节中。在上面这个例子中,变量放入S h a r e d节中。该变量后面的#pragma dataseg()一行告诉编译器停止将已经初始化的变量放入S h a r e d节,并且开始将它们放回到默认数据节中。需要记住的是,编译器只将已经初始化的变量放入新节中。例如,如果我从前面的代码段中删除初始化变量(如下面的代码所示),那么编译器将把该变量放入S h a r e d节以外的节中。
#pragma data_seg("Shared")
LONG g_lInstanceCount;
#pragma data_seg()
Microsoft 的Visual C++编译器提供了一个A l l o c a t e说明符,使你可以将未经初始化的数据放入你希望的任何节中。请看下面的代码:
// Create Shared section & have compiler place initialized data in it.
#pragma data_seg("Shared")
// Initialized, in Shared section
int a = 0;
// Uninitialized, not in Shared section
int b;
// Have compiler stop placing initialized data in Shared section.
#pragma data_seg()
// Initialized, in Shared section
__declspec(allocate("Shared")) int c = 0;
// Uninitialized, in Shared section
__declspec(allocate("Shared")) int d;
// Initialized, not in Shared section
int e = 0;
// Uninitialized, not in Shared section
int f;
上面的注释清楚地指明了指定的变量将被放入哪一节。若要使A l l o c a t e声明的规则正确地起作用,那么首先必须创建节。如果删除前面这个代码中的第一行#pragma data_seg,上面的代码将不进行编译。
之所以将变量放入它们自己的节中,最常见的原因也许是要在. e x e或D L L文件的多个映像之间共享这些变量。按照默认设置, . e x e或D L L文件的每个映像都有它自己的一组变量。然而,可以将你想在该模块的所有映像之间共享的任何变量组合到它自己的节中去。当给变量分组时,系统并不为. e x e或D L L文件的每个映像创建新实例。
仅仅告诉编译器将某些变量放入它们自己的节中,是不足以实现对这些变量的共享的。还必须告诉链接程序,某个节中的变量是需要加以共享的。若要进行这项操作,可以使用链接程序的命令行上的/ S E C T I O N开关:
/SECTION:name,attributes
在冒号的后面,放入你想要改变其属性的节的名字。在我们的例子中,我们想要改变S h a r e d节的属性。因此应该创建下面的链接程序开关:
/SECTION:Shared,RWS
在逗号的后面,我们设定了需要的属性。用R代表R E A D ,W代表W E I T E,E代表E X E C U T E,S代表S H A R E D。上面的开关用于指明位于S h a r e d节中的数据是可以读取、写入和共享的数据。如果想要改变多个节的属性,必须多次设定/ S E C T I O N开关,也就是为你要改变属性的每个节设定一个/ S E C T I O N开关。
也可以使用下面的句法将链接程序开关嵌入你的源代码中:
#pragma comment(linker, "/SECTION:Shared,RWS")
这一行代码告诉编译器将上面的字符串嵌入名字为“ . d r e c t v e”的节。当链接程序将所有的. o b j模块组合在一起时,链接程序就要查看每个. o b j模块的“ . d r e c t v e”节,并且规定所有的字符串均作为命令行参数传递给该链接程序。我一直使用这种方法,因为它非常方便。如果将源代码文件移植到一个新项目中,不必记住在Visual C++的Project Settings(项目设置)对话框中设置链接程序开关。
虽然可以创建共享节,但是,由于两个原因, M i c r o s o f t并不鼓励你使用共享节。第一,用这种方法共享内存有可能破坏系统的安全。第二,共享变量意味着一个应用程序中的错误可能影响另一个应用程序的运行,因为它没有办法防止某个应用程序将数据随机写入一个数据块。
假设你编写了两个应用程序,每个应用程序都要求用户输入一个口令。然而你又决定给应用程序添加一些特性,使用户操作起来更加方便些:如果在第二个应用程序启动运行时,用户正在运行其中的一个应用程序,那么第二个应用程序就可以查看共享内存的内容,以便获得用户的口令。这样,如果程序中的某一个已经被使用,那么用户就不必重新输入他的口令。
这听起来没有什么问题。毕竟没有别的应用程序而只有你自己的应用程序加载了D L L,并且知道到什么地方去查找包含在共享节中的口令。但是,黑客正在窥视着你的行动,如果他们想要得到你的口令,只需要编写一段很短的程序,加载到你的公司的D L L文件中,然后监控共享内存块。当用户输入口令时,黑客的程序就能知道该用户的口令。
黑客精心编制的程序也可能试图反复猜测用户的口令并将它们写入共享内存。一旦该程序猜测到正确的口令,它就能够将各种命令发送给两个应用程序中的一个。如果有一种办法只为某些应用程序赋予访问权,以便加载一个特定的D L L,那么这个问题也许是可以解决的。但是目前还不行,因为任何程序都能够调用L o a d L i b r a r y函数来显式加载D L L。
————————————————————————————————
Linux下实现:
版权声明:转自博客园,bourneli(李伯韬)的技术博客
原文链接:https://www.cnblogs.com/bourneli/archive/2011/12/28/2305280.html
为防止原文失效,以下是转载的文章内容,由于是简单拷贝,失去了原文格式,建议去看原文。
————————————————————————————————
本文目的
前几天在开发中遇到一个古怪的问题,定位了两天左右的时间才发现问题。该问题正如题目所描述:单一模式在动态链接库之间出现了多个实例。由于该实例是一个配置管理器,许多配置信息都在这个实例的初始化过程中读取,一旦出错,系统的其他地方都无法正确运行,所以给问题定位带来一定难度。为了避免敏感信息的泄漏,同时为了便于大家理解,将问题简化,在此与大家分享。
问题描述
首先,编写一个简单的单一模式的类,文件singleton.h内容如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
接下来,编写一个简单的插件,调用该singleton,插件内容在hello.cpp文件中,如下:
1 2 3 4 5 6 7 8 9 |
|
最后,编写一个main函数,调用singleton和插件,如下面的example1.cpp文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
|
好了,问题重现的简化版本构建完成,接下来,我们需要编译并执行它。编写makefile,内容如下(PS: singleton.h,hellp.cpp和example1.cpp在同一个目录中):
1 2 3 4 5 6 7 |
|
make完后,运行example1文件。读到这里,你认为会输出什么?不读题目,可能,你认为会输出这些内容:
singleton.num in main : 100 singleton.num in hello.so : 100 singleton.num in hello.so after ++ : 101 singleton.num in main : 101 |
可是,小黑框中输出了如下的内容(注意红色部分):
singleton.num in main : 100 singleton.num in hello.so : -1 singleton.num in hello.so after ++ : 0 singleton.num in main : 100 |
从输出内容中,可以看出singleton出现了两个实例,一个实例在main函数中,另一个实例在插件hello.so中。
原因分析和解决方法
上面的现象,与现实中的项目一样,为单例构造了两个实例,这与我的初衷(使用单一的配置)相悖。为什么会出现这个现象呢?
究其原因,是由于插件的动态链接引起的。hello.so在动态链接过程中,没有发现example1中有singleton::instance这个实例(但是事实上,该实例已经存在了),那么就会自己创建一个实例(正如单例的逻辑实现),这样就导致了两个实例。
可以通过工具nm查看example1的符号表(symbol table),看看其中是否包含singleton::instance符号(dynamic )。
(PS: google一下”symbol table”和”nm”可以了解更多细节)
$ nm -C example1 | grep singleton 080488fa t global constructors keyed to _ZN9singleton9pInstanceE 08048ab6 W singleton::instance() 08049ff0 B singleton::pInstance 08048aa8 W singleton::singleton() $ nm –C –D example1 | grep singleton $ |
D选项用于查看动态符号(dynamic symbol),你会发现singleton::pInstance在静态表中存在,在动态表中不存在,这也就是说,动态连接器(dynamic linker)在加载hello.so的时候,无法找到singleton::pInstance的唯一实例,只能构造一个新的实例,该实例在hello.so中是唯一的,但是不能保证在example1和hello.so中唯一。
现在,解决问题的关键在于如何暴露example1中的singleton::pInstance。好在,GNU make有一个链接选项-rdynamic,可以帮我们解决这个问题,看看修改后的makefile(注意第二行末尾与原来的区别):
1 2 3 4 5 6 7 |
|
修改后,重新编译example1(make clean && make)。
此时,再看看example1的符号表(注意红色高亮部分):
$ nm -C example1 | grep singleton 08048ada t global constructors keyed to _ZN9singleton9pInstanceE 08048c96 W singleton::instance() 08049280 B singleton::pInstance 08048c88 W singleton::singleton() $ nm -C -D example1 | grep singleton 08048c96 W singleton::instance() 08049280 B singleton::pInstance 08048c88 W singleton::singleton() |
静态符号没有什么变化,但是动态符号却显示了更多的内容,其中包括我们想要的singleton::pInstance,也就是singleton的唯一实例。
OK,此时上面的问题已经解决,执行example1,输出结果如下:
singleton.num in main : 100 singleton.num in hello.so : 100 singleton.num in hello.so after ++ : 101 singleton.num in main : 101 |
此结果表明singleton在example1和hello.so之间只产生了一个实例。
插件设计建议
到目前为止,上面的问题已经解决,似乎这边文章应该结束了。但是,针对上面的问题,虽然从技术层面可以解决,但是此问题最好从设计层面上避免,养成良好的程序设计风格。
首先,我们需要明白一点,插件其实是一个独立的程序,它与主程序的不同在于他没有一个像main函数一样的入口,而是被主程序动态加载并调用相关接口。打个比喻,插件与主程序的关系好比主人(主程序)与仆人(插件)。主人通过接口向仆人发出命令,也就是调用仆人的相关函数。而仆人在执行命令的时候,不应该打扰主人,也就是不应该去调用主人的单例或其他全局变量。为了做到这一点,主人在命令中应该给出足够的信息,以便仆人能够顺利完成任务,也就是应该传入一些参数,这些参数可以由主程序统一读取,然后传给插件。这样,就不会出现类似上面两个单例实例的问题。
小结上面的比喻:主程序读取所有配置,将配置传给插件,插件在执行任务时,不要调用主程序的全局变量,而是通过局部变量,也就是参数和返回值的方式,与主程序交互。
参考资料
上面的解决方案是通过在stackOverflow中提问,得到的,本人只是将其翻译并总结,所以最后需要感谢一下stackOverflow中的热心的朋友,BourneLi是我的stackOverflow中的ID。问题链接如下: