动态库同名函数问题

今天看到一篇关于函数同名的问题,讲解的不错,转发了。
下面是转发的内容: 万事皆有缘由,还是先从我遇到的这个问题说起~~~问:有一个主执行程序main,其中实现了函数foo(),同时调用动态库liba.so中的函数bar(),而动态库liba.so中也实现了foo()函数,那么在执行的时候如果在bar()中调用foo()会调用到哪一个?在main()中调用呢? 直接给答案:如果是在Linux上,liba.so中的foo()函数是一个导出的(extern)”可见”函数,那么调用会落入主程序里,这对于liba.so的作者来说实在是个灾难,自己辛辛苦苦的工作竟然被自己人视而不见,偏偏误入歧途,偏偏那个歧途“看起来”(declaration)和自己完全一样,但表里(definition)不一的后果就是程序出错或者直接crash~~~ 到这里故事讲完了,只想知道结论的可以离开了,觉得不爽的别忙着扔臭鸡蛋,下面待我从头慢慢叙来。 先贴代码:
main.cpp
#include
#include
#include “mylib.h”
void foo(void * p)
{
printf(“foo in main, %d\n”, ((int)p));
}
int main()
{
bar();
return 0;
}
mylib.cpp
#include “mylib.h”
#include
#include
#include “mylib_i.h”
__attribute ((visibility(“default”))) void bar()
{
int i = 9;
foo((void*)&i);
}
mylib_i.cpp
#include “mylib_i.h”
#include
#include
void foo(void * p)
{
printf(“foo in lib = %d\n”, ((int)p));
}
makefile:
all:
g++ -fPIC -c -o mylib_i.o mylib_i.cpp
g++ -fPIC -c -o mylib.o mylib.cpp
g++ -shared -o libmylib.so mylib.o mylib_i.o
g++ -c -o main.o main.cpp
g++ -o main main.o -L. -lmylib
objdump -t libmylib.so |c++filt |egrep “foo|bar” 0000000000000778 g F .text 0000000000000025 foo(void*)
000000000000075c g F .text 000000000000001a bar()
nm libmylib.so | c++filt | egrep “foo|bar” 000000000000075c T bar()
0000000000000778 T foo(void*)
foo我们看到libmylib.so中导出了两个全局符号(global)foo和bar,而Linux中动态运行库的符号是在运行时进行重定位的,我们可以用objdump -R看到libmylib.so的重定位表,中间有foo符号,为什么没有bar符号呢?
objdump -R libmylib.so | c++filt libmylib.so: file format elf64-x86-64
DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
0000000000100aa0 R_X86_64_RELATIVE ABS+0x0000000000100aa0
0000000000100aa8 R_X86_64_RELATIVE ABS+0x0000000000100898
0000000000100a58 R_X86_64_GLOB_DAT __cxa_finalize
0000000000100a60 R_X86_64_GLOB_DAT _Jv_RegisterClasses
0000000000100a68 R_X86_64_GLOB_DAT gmon_start
0000000000100ab0 R_X86_64_64 __gxx_personality_v0
0000000000100a88 R_X86_64_JUMP_SLOT foo(void*)
0000000000100a90 R_X86_64_JUMP_SLOT printf
0000000000100a98 R_X86_64_JUMP_SLOT __cxa_finalize
这里有点复杂了,先扯开谈一下另一个问题,即地址重定位发生的时机,而在这之前先看无处不在的符号的问题,事实上我们的C/C++程序从可读的代码变成计算机可执行的进程,中间经过了很多步骤:编译、链接、加载、最后才是真正运行我们的代码。C/C++代码的变量函数等符号最后怎么变成正确的内存中地址,这个和符号相关的问题牵涉到很多:这里和我们相关的主要是两个话题:符号的查找(resolve,或者叫决议、绑定、解析)和符号的重定位(relocation)
符号的查找/绑定在每一个步骤中都可能会发生(严格说编译时并不是符号的绑定,它只会牵涉到局部符号(static)的绑定,它是基于token的语法和语义层面的“绑定”)。
链接时的符号绑定做了很多工作,这也是我们经常看到ld会返回"undefined reference to …“或"unresolved symbol …”,这就是ld找不到符号,完成不了绑定工作,只能向我们抱怨老,但并不是所有的链接时都会把符号绑定进行到底的,只要在生成最终的执行程序之前把所有的符号全部绑定完成就可以了。这样我们可以理解为什么链接动态库或静态库是即使有符号找不到也不会报错(但如果是局部符号找不到那时一定会报错的,因为链接器知道已经没有机会再找到了)。当然静态链接(ar)和动态链接(ld)在这方面有着众多生理以及心理、外在以及本源的差别,比如后面系列会提到的弱符号解析、地址无关代码等等,这里按下不表。
由于编译和链接都不能保证所有符号都已经解析,因此我们通过nm查看.o或者.a或者.so文件时会看到U符号,即undefined符号,那都是待字闺中的代表(无视剩女的后果是当你生成最终的执行程序时报告unresolved symbol…)
加载时的绑定,其实加载时对于符号的绑定和链接对应的执行程序做的事情基本类似,需要重复劳动的原因是OS无法保证加载程序时当初的原配是否还在,通过绑定来找到符号在那个模块的那个地址。
既然加载时都已经绑定了,那为什么运行时还要?唉,懒惰的OS总是抱着侥幸的心理想可能有些符号不需要绑定,加载时就不理它们了,直到运行时不能不用时火烧眉头再来绑定,此所谓延迟绑定,在Linux里通过PLT实现。
另一个符号的概念是重定位(relocation),似乎这个概念和符号的绑定是很相关、甚至有点重叠的。我的理解,符号的绑定一般包含了重定位这样一个操作(但也不绝对,比如局部符号就没有重定位的发生),而要完成重定位则需要符号的查找先。一般而言,我们更多地提重定位是在加载和运行的时候。严格的区分,嗯,我也说不清,哪位大大解释一下?
所以类似地,重定位也可以分为
链接时重定位
加载时重定位
运行时重定位
所有重定位都要依赖与重定位表(relocation table),可以通过objdump –r/-R看到,就是前文中提到objdump –R libmylib.so会看到foo这个符号需要重定位,为什么呢?因为链接器发现libmylib.so中的bar()函数调用了foo()函数,而foo()也可能会被外面的函数调用到(extern函数),所以链接器得让它在加载或运行时再次重定位以完成绑定。那为什么没有bar()函数在重定位表中呢?ld在链接libmylib.so时还没看到任何它需要加载/运行时重定位的迹象,那就放过吧,但是,main…我要用啊,试试objdum –R main?OK,果然ld在这时候把它加上了。
所以,foo/bar这两个函数都需要在加载/运行时进行重定位。那究竟是加载还是运行时呢?前面已经提过,就是PLT的差别,具体到编译选项,是-fPIC,也就是地址无关代码,如果编译.o文件时有-fPIC,那就会是PLT代码,即运行时重定位,如果只在链接时指定-shared,那就是加载时运行咯。搞这么复杂的目的有两个:一是杜绝浪费,二是为了COW。Windows上采用的就是加载时重定位,并且引入了所谓基地址重置(rebasing)来解决共享问题。糟糕,剧透了,打住~~~
这篇我们编译的时候没有带-fPIC参数,所以重定位会发生在程序加载的时候。
OK,说了这么多,其实只是想说明:对于用到的动态库中的extern符号,在加载/运行时会发生重定位(如果我们注意前面的结果其实会看到中间还有printf,也就是用到的libc.so中的符号,同样它也会在加载/运行时被重定位)。
可是,这个和最初提到的问题有啥关系?然,正是重定位导致了问题的出现!
这个就源于加载器对于重定位的实现逻辑了(重定位这个操作是由加载器完成,也就是我们在ldd main是会看到的/lib/ld-linux.so.2,它里面实现了一个重要的函数_dl_runtime_resolve,会真正完成重定位的逻辑) 加载器在进行符号解析和重定位时,会把找到的符号及对应的模块和地址放入一个全局符号表(Global Symbol Table),但GST中是不能放多个同名的符号的,否则使用的人就要抓狂了,所以加载器在往全局符号表中加入item时会解决同名符号冲突的问题,即引入符号优先级,原则其实很简单:当一个符号需要被加入全局符号表时,如果相同的符号名已经存在,则后加入的符号被忽略。所以在Linux中,发生全局符号冲突真是太正常了~~~,主执行程序中的符号可能会覆盖动态库中的符号(因为主程序肯定是第一个加载的),甚至动态库a中符号也会覆盖动态库b中的符号。这又被称为共享对象全局符号介入(Global Symbol Interpose)。
当然,对于动态库和动态库中的符号冲突,又牵涉到另一个问题,即动态库的加载顺序问题,这可是生死攸关的大问题,毕竟谁在前面谁就说话算数啊。加载器的原则是广度优先(BFS),即首先加载主程序,然后加载所有主程序显式依赖所有动态库,然后加载动态库们依赖的动态库,知道全部加载完毕。
当然,这边还有个动态加载动态库(dlopen)的问题,原则也是一样,真正的逻辑同样发生在_dl_runtime_resolve中。
好,到这里这个问题的来龙去脉已经搞清楚了。应该可以清晰地回答同名函数的执行究竟是怎样这样一个问题了。至于http://wf.xplore.cn/post/test_lib.php中提到的动态库和静态库同名冲突的问题,可以看成是这里的一个特例,原则并不是静态库优先,而是主程序优先,因为静态库被静态链接进了主程序,所以“看起来”被优先了。
那么,这个行为是好是坏呢?在我看来很痛苦~~~因为这个冲突gdb半天可不是闹着玩的~~~C/C++中的全局命名空间污染问题由来已久,也纠结很多很多人,《C++大规模程序开发》中也给出很多实务的方法解决这个问题。那这个行为就一点好处没有?也未必,比如可以很方便地hook,通过控制动态库的加载循序可以用自己的代码替换掉其他人库里的代码,应该可以实现很酷的功能(比如LD_PRELOAD),不清楚valgrind是不是就是通过这种方法实现?hook住malloc/free/new/delete应该可以~~~
但总的看起来这个行为还是很危险的,特别是如果你鸵鸟了,那它就会在最不经意间刺出一刀,然后让你话费巨大的精力查找、修改(对于大型项目查找很难,修改也很难)
那难道就没法避免了?毕竟在大规模团队开发中,每个人在写自己的代码,怎样保证自己的就一定和别人的不冲突呢?
C++的namespace(C怎么办?),合理的命名规范(所有人都能很好地遵守吗?),尽可能地避免全局变量和函数(能够static的尽量static)(static只能在同一个c/cpp中使用,不同c/cpp文件中的变量或函数怎么办?),这些都是很好的解决方法,但都是着眼在避免同名冲突,但如果真的无法避免同名冲突,有什么方面能够解决吗?比如动态库中就必须有个函数也叫foo(),而且动态库中必须调用这样一个foo(),而不是可被别人覆盖的foo()(static或许可以解决问题,呵呵)
这里再多介绍一个新学习到的方法:GCC的visibility属性http://gcc.gnu.org/onlinedocs/gcc/Function-Attributes.html#Function-Attributes:
正如这段描述,通过visibility属性可以让链接器知道,这个符号是否是extern的,如果把一个变量或函数的visibility设为hidden或internal(我不清楚这两者有什么差别?)那其他模块是调用不到这个变量或函数的,即只能内部使用。既然链接器知道它们只会被内部使用,那应该就不需要重定位了吧?下面我们通过例子来看一下。
有两种使用方法指定visibility属性
编译时指定,指定编译选项-fvisibility=hidden,则该编译出的.o文件中的所有符号均为外部不可访问
代码中指定,即上面例子中的代码,void attribute((visibility (“hidden”))) foo(); 则foo()就只能在模块内部使用了。
我们先来简单地改一下Makefile:
all:
g++ -fPIC -fvisibility=hidden -c -o mylib_i.o mylib_i.cpp
g++ -fPIC -c -o mylib.o mylib.cpp
g++ -shared -o libmylib.so mylib.o mylib_i.o
g++ -c -o main.o main.cpp
g++ -o main main.o -L. -lmylib
再运行,发现结果变成了"foo in lib”。
深入一下:
objdump -t libmylib.so |c++filt | egrep “foo|bar” 0000000000000728 l F .text 0000000000000025 .hidden foo(void*)
000000000000070c g F .text 000000000000001a bar()
nm libmylib.so |c++filt | egrep “foo|bar” 000000000000070c T bar()
0000000000000728 t foo(void*)
我们看到nm输出中foo()函数变成了t,nm的man手册中说:
The symbol type. At least the following types are used; others are, as well, depending on the object file format. If lowercase, the symbol is local; if uppercase, the symbol is global (external).
很清楚了,我们已经成功地把这个函数限定在动态库内部。
再深入一点,看一下没加visibility=hidden和加了visibility=hidden的bar()函数调用foo()函数的汇编码有什么不一样:
没加visibility=hidden
objdump -d libmylib.so 000005fc :
5fc: 55 push %ebp
5fd: 89 e5 mov %esp,%ebp
5ff: 83 ec 08 sub $0x8,%esp
602: e8 fc ff ff ff call 603
607: c9 leave
608: c3 ret
609: 90 nop
60a: 90 nop
60b: 90 nop
0000060c :
加了visibility=hidden
objdump -d libmylib.so …
000005dc :
5dc: 55 push %ebp
5dd: 89 e5 mov %esp,%ebp
5df: 83 ec 08 sub $0x8,%esp
5e2: e8 05 00 00 00 call 5ec
5e7: c9 leave
5e8: c3 ret
5e9: 90 nop
5ea: 90 nop
5eb: 90 nop
000005ec :

可以清楚地看到,没加visibility=hidden的bar()函数,在调用foo()函数的地方,填入的是0xfffffffc,也就是call 603,那603是什么?
[root@zouf testlib]# objdump -R libmylib.so |grep -r foo
00000603 R_386_PC32 foo哈哈,原来603就是要重定位的foo啊。
而我们看加了visibility=hidden的bar()函数,其中调用foo()函数的地方被正确地填入相对偏移地址0x00000005,也就是foo()函数所在地,所以它已经不再需要依赖重定位来找到正确的地址啦。
再提一遍,没加visibility=hidden的那个也没有加-fPIC,否则看到的就不是这个结果了,预知详情,下回分解~~~
这样看起来多好啊,动态库内部的变量或函数就内部使用,只有需要导出的符号才参与全局导入。事实上这样的行为也正是Windows上DLL的行为,在DLL中定义的变量或函数默认是不会被导出的,只有在.DEF文件加上EXPORTS,或者__declspec__(dllexport)才会导出,别的模块才能使用,而对于未导出的符号,内部使用时是不经过全局导入的,从而保证了全局空间的“相对”清洁。
唉,Linux/GCC为什么不这样设计呢?是不是有什么不可告人的秘密?哪位高人指点一下?莫非又是历史遗留问题?为了兼容性而妥协?
不过GCC在wiki上对于visilibity的建议似乎也很清楚,就是尽量在Makefile中打开visilibity属性,同时在明确需要导出的变量/函数/类加上修饰符http://gcc.gnu.org/wiki/Visibility
甚至GCC都给出了很清楚的代码来展示:
// Generic helper definitions for shared library support
#if defined _WIN32 || defined CYGWIN
#define FOX_HELPER_DLL_IMPORT __declspec(dllimport)
#define FOX_HELPER_DLL_EXPORT __declspec(dllexport)
#define FOX_HELPER_DLL_LOCAL
#else
#if GNUC >= 4
#define FOX_HELPER_DLL_IMPORT attribute ((visibility(“default”)))
#define FOX_HELPER_DLL_EXPORT attribute ((visibility(“default”)))
#define FOX_HELPER_DLL_LOCAL attribute ((visibility(“hidden”)))
#else
#define FOX_HELPER_DLL_IMPORT
#define FOX_HELPER_DLL_EXPORT
#define FOX_HELPER_DLL_LOCAL
#endif
#endif
// Now we use the generic helper definitions above to define FOX_API and FOX_LOCAL.
// FOX_API is used for the public API symbols. It either DLL imports or DLL exports (or does nothing for static build)
// FOX_LOCAL is used for non-api symbols.
【动态库同名函数问题】#ifdef FOX_DLL // defined if FOX is compiled as a DLL
#ifdef FOX_DLL_EXPORTS // defined if we are building the FOX DLL (instead of using it)
#define FOX_API FOX_HELPER_DLL_EXPORT
#else
#define FOX_API FOX_HELPER_DLL_IMPORT
#endif // FOX_DLL_EXPORTS
#define FOX_LOCAL FOX_HELPER_DLL_LOCAL
#else // FOX_DLL is not defined: this means FOX is a static lib.
#define FX_API
#define FOX_LOCAL
#endif // FOX_DLL

    推荐阅读