最近打算把linux下的一个c++程序以插件的形式实现,即主程序加载so功能模块的方式,在这个过程中,遇到了一个全局符号覆盖的问题,查了一些资料有了初步的了解,这里记录一下。
遇到的问题是:程序以dlopen方式加载so文件时,如果so文件和程序存在同名的全局变量,主程序的全局变量会so文件中的全局变量覆盖。
由此,引出的问题如下
1、同名全局变量覆盖与dlopen的打开方式是否有关?
2、与主程序和so文件的编译选项是否有关?
3、以静态库.a直接连接是否也存在这个现象?
4、以动态库.so直接链接是否也存在这个问题?
5、同名全局函数是否会被覆盖?
以下多来自网上的材料,这里整理一下
1、全局变量初始化
时间
根据 C++ 标准,全局变量的初始化要在 main 函数执行前完成,既有编译时,也可能会有运行时(seriously), 其中静态初始化执行先于动态初始化!
1、static initialization: 静态初始化指的是用常量来对变量进行初始化,主要包括 zero initialization 和 const initialization,静态初始化在程序加载的过程中完成,对简单类型(内建类型,POD等)来说,从具体实现上看,zero initialization 的变量会被保存在 bss 段,const initialization 的变量则放在 data 段内,程序加载即可完成初始化,这和 c 语言里的全局变量初始化基本是一致的。
2、dynamic initialization:动态初始化主要是指需要经过函数调用才能完成的初始化,比如说:int a = foo(),或者是复杂类型(类)的初始化(需要调用构造函数)等。这些变量的初始化会在 main 函数执行前由运行时调用相应的代码从而得以进行(函数内的 static 变量除外)。--貌似gcc不支持这种方式,未验证
顺序
1、同一个编译单元内的全局变量,初始化的顺序与声明顺序一致(销毁的顺序则反过来)
2、不同编译单元间的全局变量,c++ 标准并没有明确规定它们之间的初始化(销毁)顺序,因此实现上完全由编译器自己决定,一个比较普遍的认识是:不同编译单元间的全局变量的初始化顺序是不固定的,哪怕对同一个编译器,同一份代码来说,任意两次编译的结果都有可能不一样。
以上内容主要来自 twoon c++ 全局变量初始化的一点总结 该博主的博客挺有意思的,mark一下~
2、动态库的初始化
动态在被加载时会进行内存映射,之后需要运行一些动态库的初始化函数构建动态库的运行环境,包括
(1)、动态库的构造和析构机制
void __attribute__ ((constructor)) my_init(void);
void __attribute__ ((destructor)) my_fini(void);
(2)、动态库的全局变量初始化
在C语言中,其全局变量保存在.data段,启动过程中,loader只是简单地使用mmap将数据段映射到内存中,这些全局变量只有在第一次使用到的时候才会为其分配物理内存,其在启动过程中不需要运行构造函数。在C++语言中,对于非内置类型的全局对象,系统要在main函数之前,运行其构造函数,完成全局变量的初始化。这将导致(1)减缓了进程或动态库的启动加载速度。(2)构造函数修改了类的成员变量,这会产生page fault减缓进程的启动速度;同时也会产生一些不必要的dirty page,造成内存上的浪费。
查找一个执行程序或动态库的全局变量
nm -f sysv hello | grep bss
1、在bss节,类型为OBJECT,不包含函数名,对象大小>4,基本可以认为是全局对象。
2、在bss节,类型为OBJECT,不包含函数名,对象大小=4,有可能是全局对象,需要进一步确认。
以上内容主要来自嵌入式Linux内存使用与性能优化这本书
3、动态库的符号加载问题
这里直接上结论吧
1、符号查找
如果一个动态库依赖了一些外部符号(位于其他动态库甚至应用程序中)。
(1)在直接链接这个动态库的时候,可以把依赖的其他库也直接链接上。
(2)对于动态加载的库,要保证在加载该库时,进程中加载的其他动态库里已经存在该符号。例如,可以通过LD_PRELOAD环境变量可以让一个进程先加载指定的动态库
2、符号覆盖
同名全局函数
(1)直接link动态库时,整个程序运行的时候符号会发生覆盖,只有一个符号被使用。更复杂的情况中,多个动态库和程序都有相同的符号,情况也是一样,会发生符号覆盖。如果程序里没有这个符号,而多个动态库里有相同的符号,也会覆盖。
(2)动态载入的动态库,在dlopen打开时默认使用RTLD_LOCAL参数因此,其函数符号不会出现覆盖情况;但如果采用RTLD_GLOBAL参数,则和直接link一样会出现同名函数符号的覆盖
(3)一种特殊的情况,-rdynamic,指示连接器把所有符号(不包括静态符号,比如被static修饰的函数)都添加到动态符号表.dynsym表里,以便dlopen()或backtrace()等函数使用。如果执行程序在链接时采用了该参数,则执行程序在dlopen加载动态库时(无论RTLD_LOCAL还是RTLD_GLOBAL),动态库内与执行程序同名的函数符号都将被执行程序中的函数符号覆盖。
同名全局变量
(1)与全局函数不同,执行程序和.so文件全局变量符号重复时,无论是直接链接还是动态链接,都会发生覆盖的情况,最后加载的动态库中的全局变量会冲掉之前加载的全局变量,全局变量会被初始化/释放(有可能导致double free)两次,需谨慎。
(2)对于不同的so文件链接同一个.a静态库,对于该静态库中的全局变量有如下两种情况:
- 直接link的,总是用同一个变量。
- 动态加载dlopen,依赖dlopen()的flag:(存疑,待验证)
如果是RTLD_LOCAL(默认),各个so会使用各自的.a里的变量。
如果用RTLD_GLOBAL,就跟直接link一样,用同一个变量了。
以上内容主要来自linux动态库的种种要点 和 关于动态链接库(dynamic library)里static变量和函数的问题 和 gcc选项-g与-rdynamic的异同
4、补充-C编译器对多重定义的全局变量符号的解析和链接
在编译阶段,编译器将全局符号信息隐含地编码在可重定位目标文件的符号表里。这里有个“强符号(strong)”和“弱符号(weak)”的概念——前者指的是定义并且初始化了的变量,后者指的是未定义或者定义但未初始化的变量。当符号被多重定义时,GNU链接器(ld)使用以下规则决议:
1、不允许出现多个相同强符号。
2、如果有一个强符号和多个弱符号,则选择强符号。
3、如果有多个弱符号,那么先决议到size最大的那个,如果同样大小,则按照链接顺序选择第一个。
上面所说的是在直接链接时的情况;在动态加载时,“允许”执行程序和so文件出现多个相同强符号,根据前面的结论,这种情况下会出现全局符号覆盖的情况。
以上内容来自C语言全局变量那些事儿
5、结论
最后对开始的问题进行简单整理一下(部分结论未验证,很可能出错,待验证后,具体再补充)
1、同名全局变量覆盖与dlopen的打开方式是否有关?
-》执行程序和so之间的全局变量覆盖与dlopen的参数无关,无论直接链接还是动态加载都会存在覆盖问题;so文件之间的全局变量覆盖问题(待验证)
2、与主程序和so文件的编译选项是否有关?
-》全局变量的覆盖问题和编译选项无关,覆盖前提和条件如上所述
全局函数的覆盖问题与编译选项-rdynamic有关
3、以静态库.a直接连接是否也存在这个现象?
-》当一个符号在多个目标文件(.o)里同时出现时, LD报错. 提示符号多重定义.
当一个符号在多个静态库(.a)里同时出现时, LD不报错, 以第一个遇到的为准. 并且不会有任何warning提示。
4、以动态库.so直接链接是否也存在这个问题?
-》会覆盖,并且是后面覆盖前面
5、同名全局函数是否会被覆盖?
-》(1)直接link动态库时发生覆盖;(2)动态载入的动态库,与dlopen打开时参数RTLD_LOCAL、RTLD_GLOBAL有关(3)执行程序在链接时采用了-rdynamic,则执行程序在dlopen加载动态库时,.so与执行程序同名的函数符号都将被执行程序中的函数符号覆盖。
reference
C语言全局变量那些事儿
linux动态库的种种要点
关于动态链接库(dynamic library)里static变量和函数的问题
嵌入式Linux内存使用与性能优化
twoon c++ 全局变量初始化的一点总结
gcc选项-g与-rdynamic的异同
GCC/LD编译链接潜规则 (第一弹) : 当一个符号被多重定义时
,
网友评论