linux动态库之同名符号.docx
- 文档编号:7710042
- 上传时间:2023-01-25
- 格式:DOCX
- 页数:5
- 大小:22.32KB
linux动态库之同名符号.docx
《linux动态库之同名符号.docx》由会员分享,可在线阅读,更多相关《linux动态库之同名符号.docx(5页珍藏版)》请在冰豆网上搜索。
linux动态库之同名符号
Linux动态库之同名符号
[cpp]viewplaincopy万事皆有缘由,还是先从我遇到的这个问题说起~~~问:
有一个主执行程序main,其中实现了函数foo(),同时调用动态库liba.so中的函数bar(),而动态库liba.so中也实现了foo()函数,那么在执行的时候如果在bar()中调用foo()会调用到哪一个?
在main()中调用呢?
直接给答案:
如果是在Linux上,liba.so中的foo()函数是一个导出的(extern)”可见”函数,那么调用会落入主程序里,这对于liba.so的作者来说实在是个灾难,自己辛辛苦苦的工作竟然被自己人视而不见,偏偏误入歧途,偏偏那个歧途“看起来”(declaration)和自己完全一样,但表里(definition)不一的后果就是程序出错或者直接crash~~~到这里故事讲完了,只想知道结论的可以离开了,觉得不爽的别忙着扔臭鸡蛋,下面待我从头慢慢叙来。
先贴代码:
main.cpp#include<stdio.h>#include<stdlib.h>#include"mylib.h"extern"C"voidfoo(){printf("fooinmain/n");}intmain(){bar();return0;}mylib.cpp#include"mylib.h"#include<stdio.h>#include<stdlib.h>#include"mylib_i.h"voidbar(){foo();}mylib_i.cpp#include"mylib_i.h"#include<stdio.h>#include<stdlib.h>extern"C"voidfoo(){printf("fooinlib/n");}Makefileall:
g++-c-omylib_i.omylib_i.cppg++-c-omylib.omylib.cppg++-shared-olibmylib.somylib.omylib_i.og++-c-omain.omain.cppg++-omainmain.o-L.-lmylib代码很简单(没有贴header文件),结果也很简单,正如前面所说,会输出fooinmain。
那么,为什么会这样呢?
看一下libmylib.so里的东西:
[root@zouftestlib]#objdump-tlibmylib.so|grep"foo|bar"[root@zouftestlib]#objdump-tlibmylib.so|egrep"foo|bar"00000614gF.text00000034foo000005f4gF.text0000001ebar[root@zouftestlib]#nmlibmylib.so|egrep"foo|bar"000005f4Tbar00000614Tfoo我们看到libmylib.so中导出了两个全局符号(global)foo和bar,而Linux中动态运行库的符号是在运行时进行重定位的,我们可以用objdump-R看到libmylib.so的重定位表,中间有foo符号,为什么没有bar符号呢?
这里有点复杂了,先扯开谈一下另一个问题,即地址重定位发生的时机,而在这之前先看无处不在的符号的问题,事实上我们的C/C++程序从可读的代码变成计算机可执行的进程,中间经过了很多步骤:
编译、链接、加载、最后才是真正运行我们的代码。
C/C++代码的变量函数等符号最后怎么变成正确的内存中地址,这个和符号相关的问题牵涉到很多:
这里和我们相关的主要是两个话题:
符号的查找(resolve,或者叫决议、绑定、解析)和符号的重定位(relocation)符号的查找/绑定在每一个步骤中都可能会发生(严格说编译时并不是符号的绑定,它只会牵涉到局部符号(static)的绑定,它是基于token的语法和语义层面的“绑定”)。
链接时的符号绑定做了很多工作,这也是我们经常看到ld会返回"undefinedreferenceto…"或"unresolvedsymbol…",这就是ld找不到符号,完成不了绑定工作,只能向我们抱怨老,但并不是所有的链接时都会把符号绑定进行到底的,只要在生成最终的执行程序之前把所有的符号全部绑定完成就可以了。
这样我们可以理解为什么链接动态库或静态库是即使有符号找不到也不会报错(但如果是局部符号找不到那时一定会报错的,因为链接器知道已经没有机会再找到了)。
当然静态链接(ar)和动态链接(ld)在这方面有着众多生理以及心理、外在以及本源的差别,比如后面系列会提到的弱符号解析、地址无关代码等等,这里按下不表。
由于编译和链接都不能保证所有符号都已经解析,因此我们通过nm查看.o或者.a或者.so文件时会看到U符号,即undefined符号,那都是待字闺中的代表(无视剩女的后果是当你生成最终的执行程序时报告unresolvedsymbol…)加载时的绑定,其实加载时对于符号的绑定和链接对应的执行程序做的事情基本类似,需要重复劳动的原因是OS无法保证加载程序时当初的原配是否还在,通过绑定来找到符号在那个模块的那个地址。
既然加载时都已经绑定了,那为什么运行时还要?
唉,懒惰的OS总是抱着侥幸的心理想可能有些符号不需要绑定,加载时就不理它们了,直到运行时不能不用时火烧眉头再来绑定,此所谓延迟绑定,在Linux里通过PLT实现。
另一个符号的概念是重定位(relocation),似乎这个概念和符号的绑定是很相关、甚至有点重叠的。
我的理解,符号的绑定一般包含了重定位这样一个操作(但也不绝对,比如局部符号就没有重定位的发生),而要完成重定位则需要符号的查找先。
一般而言,我们更多地提重定位是在加载和运行的时候。
严格的区分,嗯,我也说不清,哪位大大解释一下?
所以类似地,重定位也可以分为链接时重定位加载时重定位运行时重定位所有重定位都要依赖与重定位表(relocationtable),可以通过objdump–r/-R看到,就是前文中提到objdump–Rlibmylib.so会看到foo这个符号需要重定位,为什么呢?
因为链接器发现libmylib.so中的bar()函数调用了foo()函数,而foo()也可能会被外面的函数调用到(extern函数),所以链接器得让它在加载或运行时再次重定位以完成绑定。
那为什么没有bar()函数在重定位表中呢?
ld在链接libmylib.so时还没看到任何它需要加载/运行时重定位的迹象,那就放过吧,但是,main…我要用啊,试试objdum–Rmain?
OK,果然ld在这时候把它加上了。
所以,foo/bar这两个函数都需要在加载/运行时进行重定位。
那究竟是加载还是运行时呢?
前面已经提过,就是PLT的差别,具体到编译选项,是-fPIC,也就是地址无关代码,如果编译.o文件时有-fPIC,那就会是PLT代码,即运行时重定位,如果只在链接时指定-shared,那就是加载时运行咯。
搞这么复杂的目的有两个:
一是杜绝浪费,二是为了COW。
Windows上采用的就是加载时重定位,并且引入了所谓基地址重置(rebasing)来解决共享问题。
糟糕,剧透了,打住~~~这篇我们编译的时候没有带-fPIC参数,所以重定位会发生在程序加载的时候。
OK,说了这么多,其实只是想说明:
对于用到的动态库中的extern符号,在加载/运行时会发生重定位(如果我们注意前面的结果其实会看到中间还有printf,也就是用到的libc.so中的符号,同样它也会在加载/运行时被重定位)。
可是,这个和最初提到的问题有啥关系?
然,正是重定位导致了问题的出现!
这个就源于加载器对于重定位的实现逻辑了(重定位这个操作是由加载器完成,也就是我们在lddmain是会看到的/lib/ld-linux.so.2,它里面实现了一个重要的函数_dl_runtime_resolve,会真正完成重定位的逻辑)加载器在进行符号解析和重定位时,会把找到的符号及对应的模块和地址放入一个全局符号表(GlobalSymbolTable),但GST中是不能放多个同名的符号的,否则使用的人就要抓狂了,所以加载器在往全局符号表中加入item时会解决同名符号冲突的问题,即引入符号优先级,原则其实很简单:
当一个符号需要被加入全局符号表时,如果相同的符号名已经存在,则后加入的符号被忽略。
所以在Linux中,发生全局符号冲突真是太正常了~~~,主执行程序中的符号可能会覆盖动态库中的符号(因为主程序肯定是第一个加载的),甚至动态库a中符号也会覆盖动态库b中的符号。
这又被称为共享对象全局符号介入(GlobalSymbolInterpose)。
当然,对于动态库和动态库中的符号冲突,又牵涉到另一个问题,即动态库的加载顺序问题,这可是生死攸关的大问题,毕竟谁在前面谁就说话算数啊。
加载器的原则是广度优先(BFS),即首先加载主程序,然后加载所有主程序显式依赖所有动态库,然后加载动态库们依赖的动态库,知道全部加载完毕。
当然,这边还有个动态加载动态库(dlopen)的问题,原则也是一样,真正的逻辑同样发生在_dl_runtime_resolve中。
好,到这里这个问题的来龙去脉已经搞清楚了。
应该可以清晰地回答同名函数的执行究竟是怎样这样一个问题了。
至于那么,这个行为是好是坏呢?
在我看来很痛苦~~~因为这个冲突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("visibility_type")Thisattributeaffectsthelinkageofthedeclarationtowhichitisattached.Therearefoursupportedvisibility_typevalues:
default,hidden,protectedorinternalvisibility.void__attribute__((visibility("protected")))f(){/*Dosomething.*/;}inti__attribute__((visibility("hidden")));Thepossiblevaluesofvisibility_typecorrespondtothevisibilitysettingsintheELFgABI.defaultDefaultvisibilityisthenormalcasefortheobjectfileformat.Thisvalueisavailableforthevisibilityattributetooverrideotheroptionsthatmaychangetheassumedvisibilityofentities.OnELF,defaultvisibilitymeansthatthedeclarationisvisibletoothermodulesand,insharedlibraries,meansthatthedeclaredentitymaybeoverridden.OnDarwin,defaultvisibilitymeansthatthedeclarationisvisibletoothermodules.Defaultvisibilitycorrespondsto“externallinkage”inthelanguage.hiddenHiddenvisibilityindicatesthattheentitydeclaredwillhaveanewformoflinkage,whichwe'llcall“hiddenlinkage”.Twodeclarationsofanobjectwithhiddenlinkagerefertothesameobjectiftheyareinthesamesharedobject.internalInternalvisibilityislikehiddenvisibility,butwithadditionalprocessorspecificsemantics.UnlessotherwisespecifiedbythepsABI,GCCdefinesinternalvisibilitytomeanthatafunctionisnevercalledfromanothermodule.Comparethiswithhiddenfunctionswhich,whiletheycannotbereferenceddirectlybyothermodules,canbereferencedindirectlyviafunctionpointers.Byindicatingthatafunctioncannotbecalledfromoutsidethemodule,GCCmayforinstanceomittheloadofaPICregistersinceitisknownthatthecallingfunctionloadedthecorrectvalue.protectedProtectedvisibilityislikedefaultvisibilityexceptthatitindicatesthatreferenceswithinthedefiningmodulewillbindtothedefinitioninthatmodule.Thatis,thedeclaredentitycannotbeoverriddenbyanothermodule.Allvisibilitiesaresupportedonmany,butnotall,ELFtargets(supportedwhentheassemblersupportsthe`.visibility'pseudo-op).Defaultvisibilityissupportedeverywhere.HiddenvisibilityissupportedonDarwintargets.Thevisibilityattributeshouldbeappliedonlytodeclarationswhichwouldotherwisehaveexternallinkage.Theattributeshouldbeappliedconsistently,sothatthesameentityshouldnotbedeclaredwithdifferentsettingsoftheattribute.InC++,thevisibilityattributeappliestotypesaswellasfunctionsandobjects,becauseinC++typeshavelinkage.Aclassmustnothavegreatervisibilitythanitsnon-staticdatamembertypesandbases,andclassmembersdefaulttothevisibilityoftheirclass.Also,adeclarationwithoutexplicitvisibilityislimitedtothevisibilityofitstype.InC++,youcanmarkmemberfunctionsandstaticmembervariablesofaclasswiththevisibilityattribute.Thisisusefulifyouknowaparticularmethodorstaticmembervariableshouldonlybeusedfromonesharedobject;thenyoucanmarkithiddenwhiletherestoftheclasshasdefaultvisibility.CaremustbetakentoavoidbreakingtheOneDefinitionRule;forexample,itisusuallynotusefultomarkaninlinemethodashiddenwithoutmarkingthewholeclassashidden.AC++namespacedeclarationcanalsohavethevisibilityattribute.Thisattributeappliesonlytotheparticularnamespacebody,nottootherdefinitionsofthesamenamespace;itisequivalenttousing`#pragmaGCCvisibility'beforeandafterthenamespacedefinition(seeVisibilityPragmas).InC++,ifatemplateargumenthaslimitedvisibility,thisrestrictionisimplicitlypropagatedtothetemplateinstantiation.Otherwise,templateinstantiationsandspecializationsdefaulttothevisibilityoftheirtemplate.Ifboththetemplateandenclosingclasshaveexplicitvisibility,thevisibilityfromthetemplateisused.正如这段描述,通过visibility属性可以让链接器知道,这个符号是否是extern的,如果把一个变量或函数的visibility设为hidden或internal(我不清楚这两者有什么差别?
)那其他模块是调用不到这个变量或函数的,即只能内部使用。
既然链接器知道它们只会被内部使用,那应该就不需要重定位了吧?
下面我们通过例子来看一下。
有两种使用方法指定visibility属性编译时指定,指定编译选项-fvisibility=hidden,则该编译出的.o文件中的所有符号均为外部不可访问代码中指定,即上面例子中的代码,void__attribute__((visibility(“hidden”)))foo();则foo()就只能在模块内部使用了。
我们先来简单地改一下Makefile:
all:
g++-c-fvisibility=hidden-omylib_i.omylib_i.cppg++-c-omylib.omylib.cppg++-shared-olibmylib.somylib.omylib_i.og++-c-omain.omain.cppg++-omainmain.o-L.-lmylib再运行,发现结果变成了"fooinlib”。
深入一下:
[root@zouftestlib]#objdump-tlibmylib.so|egrep"foo|bar"000005eclF.text00000022.hiddenfoo000005dcgF.text0000000dbar[root@zouftestlib]#nmlibmylib.so|egrep"foo|bar"000005dcTbar000005ectfoo我们看到nm输出中foo()函数变成了t,nm的man手册中说:
Thesymboltype.Atleastthefollowingtypesareused;othersare,aswell,dependingontheobjectfileformat.Iflowercase,thesymbolislocal;ifuppercase,thesymbolisglobal(external).很清楚了,我们已经成功地把这个函数限定在动态库内部。
再深入一点,看一下没加visibility=hidden和加了visibility=hidden的bar()函数调用foo()函数的汇编码有什么不一样:
没加vis
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- linux 动态 同名 符号