静态链接一个隐藏的坑以及深入理解静态和动态链接

1. 静态链接库,在linux上是.a文件,在win上是.lib文件,这个文件可以看成是一堆.o文件(或.obj文件)的压缩包,每个cpp文件会被编译成.o文件,然后.a只是把这些文件压缩在了一起

早期的生成静态链接的过程中,还有强符号和弱符号的概念,也就是说,可以允许多个.o文件中有同名的符号,如果有初始化的为强符号,没有初始化的为弱符号,强符号会覆盖弱符号等一系列规则,现代的静态链接已经没有这些概念,不允许有同名的符号

一堆源代码想静态链接一个.a文件,生成的产物可以有两种:一种还是静态库.a,一种是动态库.so(或win上.dll),二者的区别:

如果还生成.a,还其实还是先把这一段源代码生成一堆.o文件,然后再压缩到一个生成个压缩包就是.a文件,这个过程中不允许有同名符号,否则会报错

如果生成的是.so文件,则:动态链接库的一个特点是:代码段和数据段的偏移量是固定的,也就是每个变量,函数(每个符号)都有一个偏移量GOT(Global Offset Table),这个偏移量是固定的,当.so被进程加载后,该.so有个起始地址,那么进程中的其他代码想要访问某个函数或变量,只需要用这个起始地址加上GOT中该符号的值,就是最终的地址

所以,如果是 一堆cpp + .a = .so的过程,就是组装代码段和数据段的过程,该过程一旦组装好,就不会再去做调整,这个过程除了检查同名符号外,还会检查有没有那个符号被使用了,但是遍历了所有的.o文件都没有

找到,这种也会报错。

下面说动态链接,当拿到.so或dll后,有 一堆cpp + .so = .so 或者 一堆cpp + .so = exe,这两种情况差不多,当成一种情况讨论。

动态库中默认导出符号,在linux和mac上是不同的,在linux上只要不做特殊声明,全局变量,全局函数,类等都是默认导出的,其他的代码只要动态链接它,就能访问到,但是在win上都是默认不导出的,如果要导出

需要显式在代码中用__declspec(dllexport)标识,标明这是一个导出符号,而使用到这个符号的地方需要用__declspec(dllimport)先声明一下我要用到这个符号,然后才能正常使用这个符号。

注意:win上如果一旦一个dll中有某个符号标识了__declspec(dllexport),则编译后除了生成dll外,还会生成一个.lib文件,这个文件只是扩展名和静态库lib相同,其内容完全不同,这个内容就是导出的所有符号的名称和它的GOT偏移量,那么如果某个cpp依赖了该

动态库,可以在编译设置中引入该lib文件,那么编译生成exe(或dll)时,就可以把访问该符号的代码的地方事先用该偏移量写死,等该exe真正加载该dll时,用实时加载的起始地址加上编译时就写死的偏移量,就

能获得该符号的真正的地址。

而linux上没有这个所谓的lib,但是如果要隐式链接,这个类似的导出符号+他们的偏移量,这个表还是要有的,这个信息也存放在.so文件中。

这里就和另外两个linux命令关联了,strip和nm,strip xx.so可以去掉.so中的符号,去掉后,用nm xxx.so查看会提示没有符号,但此时,还是有全局变量和函数的符号在.so文件里面的,这些符号用strip无法去掉。

这些符号极有可能就是刚才所谓链接需要的导出符号,用nm -D xxx.so可以查看,或者用readelf -s xxx.so(file xxx.so)readelf -d xxx.so

如何在linux上隐藏某些符号呢?在代码中,不想被外面访问的符号可以用__attribute__((visibility ("hidden")))修饰,这样该符号的就从GLOBAL符号变成了LOCAL符号,外面就不可见了

还有一种方法是在链接的参数中增加-fvisibility=hidden,由于默认的符号都是default的,这个设置会让所有的符号都不可见。

还有另一篇文章,说明如何在linux和macOS上去掉某些符号:

linux和macOS的局部符号可以用strip去掉,但全局符号不可以,以下为去掉(或保留)全局符号的方法 Linux 加入编译选项: "-Wl,--retain-symbols-file=retain.txt -Wl,--version-script=symver.txt" 示例1:去掉所有符号 symver.txt内容: { local: *; }; retain.txt内容(空): 示例2:保留部分符号 symver.txt内容: { global: function1; function2; local: *; }; retain.txt内容: function1 function2 macOS 对于执行程序,若要去除所有符号,直接: strip <filename> 对于动态库dylib,加入编译选项: "-Wl,-exported_symbols_list export.txt" export.txt内容: _function1 _function2 注:这是符号名,不是原函数名,可以使用nm工具查看函数对应的符号

macOS上还可以用unexported_symbols_list 参数

注意:macOS上对二进制的操作,还有个lipo命令,这个是macOS上独有的,因为mac上的编译产物dylib往往里面会包含多个平台,比如arm64,x64等,这些合在了一个dylib中,所以如果想瘦身可以只提取某个平台

但是该命令没有strip符号的功能,只能提取某个平台,由于linux上没有这种.so包含多个平台的操作,所以也没有这个命令

总结一下,查看二进制的符号最好的命令是readelf -s ,nm, strip就是用strip命令。

用windows上测试,发现dll隐式链接,也不是写在编译参数中的所有lib都会被链接的,必须要在调用的地方真正的调到,才会被加载进来,否则不会被加载。

发现动态链接的一个坑:

如果两个dll中有同一个全局变量abc,然后一个cpp动态链接这两个dll: dll1, dll2,并且使用abc,那么cpp中的abc具体是dll1和dll2中的哪个,取决于链接顺序,先链接dll1,就用的是dll1里面的。

这种情况下,居然还不会报错,正常的编译cpp都可以正常,包括运行也正常,但其实会隐藏这个问题。

原因是什么呢?当编译cpp时,会找abc的实际的位置,当链接dll1时,就把cpp里面的abc修正为 dll1起始地址(加载是才确定,这里可能只是个变量$)+ GOT偏移量,此时cpp中的abc已经确定,后面也不会再改了

那dll2中的abc也不会再做修正了,前面说到,动态库中代码段和数据段的偏移是固定的,或者可以理解为,编译成动态库后,代码段和数据段就不再变了,也不会说某个变量再变成指向别的dll中的变量,所以这里dll2

中的abc不会再做修正,都是自己的abc,所以当所有dll都加载进cpp后,就出现了两个abc变量,cpp中用的是dll1中的,dll2中用的是他自己的。

这个坑其实更多的是由于静态链接引入,因为一般不太可能是两个dll中就有同名的符号,而是当两个dll都链接同一个.a后,这两个dll中.a的部分都是独立的,即使加载到同一个进程中,也是两份,所以就出现了同名的两个变量

所以这个坑更多的源头是静态链接!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值