Silentdoer

导航

linux下gcc/g++创建一个共享库项目以及创建一个可执行项目动态链接该共享库

1.先确保有g++命令

2.创建一个c++项目目录,并cd到该目录

3.创建共享库头文件:dynamic_so.h

#ifndef __TESTb__
#define __TESTb__

int testFun(int a, int b);

#endif

4.创建对应共享库的实现文件:dynamic_so.cpp

#include "dynamic_so.h"

int testFun(int a, int b)
{
    return a + b;
}

5.编译共享库:g++ dynamic_so.cpp -fPIC -shared -o libtest.so
此时可以看到在项目根目录下多了一个叫libtest.so的文件,这里注意,共享库要以lib开头.so结尾

 

6.开始写主程序:main.cpp

#include <iostream>
#include "dynamic_so.h"

using namespace std;

int main()
{
    cout << testFun(10, 20) << endl;

    return 0;
}

可以看到这个程序也引用了这个头文件,用于后面代码编写不会报错

7.编译成可执行文件main:g++ main.cpp -L . -ltest -o main【这里的-ltest的test就是上面的共享库去掉前缀lib和后缀.so的名字,最后的main就是可执行文件名,这里的-L .应该是指编译时从当前目录搜索依赖库,如果libtest.so是在lib下可以-L ./lib/】
可以看到在项目根目录下也多出了一个main文件

8.执行main文件:./main会发现找不到动态链接库,我们用ldd main会发现libtest.so链接的是not found

解决方式为将libtest.so放到公共目录,比如/usr/lib目录下,这个时候再用ldd main就可以看到libtest.so是链接的/usr/lib/libtest.so了

此时再执行就会输出30;

 

9.如果想编译时就让main可以从程序所在目录(而非工作目录)里搜索libtest.so呢?可以这么编译:g++ main.cpp -L . -Wl,-z,origin -Wl,-rpath='$ORIGIN' -ltest -o main
此时ldd main可以看到libtest.so链接的就是main文件当前所在目录的libtest.so文件;(libtest.so => /home/silentdoer/Projects/CppProjs/cpp_test/./libtest.so

还可以切换到上一层用./cpp_test/main来执行一下,发现确实可以输出30

10.如果是希望libtest.so链接的是main文件同级目录下的lib目录里的libtest.so文件呢?可以这样写:g++ main.cpp -L ./lib -Wl,-z,origin -Wl,-rpath='$ORIGIN/lib' -ltest -o main

如果同时依赖libtest.so和libtest2.so则是这样写:-ltest -ltest2

这种情况下,编译要求libtest.so在main.cpp所在目录,但是执行main时要求libtest.so在main所在目录的lib目录下;

【注意,一个库或可执行程序只有一个rpath,所以不存在给main依赖的共享库a设置搜索路径是'$ORIGIN/lib1',而依赖的共享库b的搜索路径是'$ORIGIN/lib2'】

 

11.通过命令,将已经编译好的可执行文件的修改其rpathpatchelf --set-rpath '$ORIGIN/lib/ttt' main
这个时候我们再用ldd main会发现not found了,可以将lib下的libtest.so移动到lib/ttt目录下,再ldd main就能看到了;

 

【总结1,rpath的出现,可以解决这样的问题,可执行程序a依赖了库libfoo版本是1,名字是libfoo,可执行程序b依赖了libfoo版本是2,名字也是libfoo,

那如果libfoo是安装在全局共享库目录,则a和b是无法一起安装的,这个时候就可以将a设置rpath,将版本是1的libfoo放到其rpath里即可】

 

12.但是如果有这样的情况:程序a依赖libfoo库版本是1和依赖了libkk库版本是1,程序2依赖了libfoo版本是2和依赖了libkk版本是2,那么这个时候应该要把libfoo v1和libkk v1都复制到程序a的rpath里,但是libfoo v1库又依赖了库libbar版本是1,而libkk v1依赖的libbar版本确是v2,那么这个时候显然libbar的v1和v2不能放共享库目录,但是放到程序a的rpath里也有冲突,因为程序a依赖的libfoo和libkk它们自己就有冲突,所以又要给程序a rpath里的libfoo v1自己也设置rpath,然后把libbar的v1放libfoo v1的rpath目录里即可解决,其他类似的冲突都可以用这种方式去解决】

 

13.我们来模拟一下上面的库与库之间也有冲突的解决方式,即再创建一个共享库,头文件为 foo.h:

#ifndef __TESTa__
#define __TESTa__

int funcAdd(int a, int b);

#endif

其cpp文件foo.cpp:

#include "foo.h"

int funcAdd(int a, int b)
{
    return a + b + 10;
}

然后编译出libfoo.so文件:g++ foo.cpp -fPIC -shared -o libfoo.so

接着改写之前的dynamic_so.cpp文件:

#include "dynamic_so.h"
#include "foo.h"

int testFun(int a, int b)
{
    return a + b + funcAdd(a, b);
}

 然后编译:g++ dynamic_so.cpp -fPIC -shared -o libtest.so -Wl,-Bsymbolic -Wl,-rpath='$ORIGIN/test_dep:$ORIGIN/test_bbb' -L ./lib/ttt/test_dep -lfoo

注意,上面的rpath可以用:分隔多个路径,上面的-Bsymbolic也不太行,只有这个 -fvisibility=hidden 符合文章里的要求,但是它会把没有__attribute__ ((visibility ("default")))
开头的方法实现都弄成hidden,使得里面的方法无法被外部使用

然后将libtest.so覆盖lib/ttt里的,且将libfoo.so放到lib/ttt/test_dep里,然后ldd lib/ttt/libtest.so可以看到:

libfoo.so => /home/silentdoer/Projects/CppProjs/cpp_test/lib/ttt/test_dep/libfoo.so (0x00007f89805e5000)

然后执行./main输出70,说明我们的算法替换成功了【注意,这里没有重新编译main.cpp,说明cpp这种没有反射的语言也可以通过动态链接库的方式实现插件功能】

我们再用readelf -d lib/ttt/libtest.so
查看libtest.so的rpath,可以看到它和main是不一样的(可以再测试一下main),说明一个库或文件可以有多个rpath,而且一个程序依赖路径上的所有可执行文件和库的rpath是可以不同的,这个为解决依赖冲突提供了切入点。

 

【经过测试,程序main如果依赖lib1和lib2,而lib1和lib2又分别依赖libfoo的v1和v2版本(但是名字没有改就是libfoo),通过rpath的方式将lib1依赖的libfoo放到了lib1的rpath:lib1_foo目录下,而lib2的是放在lib2的rpath:lib2_foo目录下,所以单独ldd lib1和lib2都能分别看到它们链接了不同目录下的libfoo.so文件,但是直接ldd main的话则只能看到链接了一个libfoo.so文件,执行了main也确实只用到了其中一个libfoo.so的算法,即当是通过main来执行时,lib1和lib2调用的是一个libfoo来执行的,这里就产生了重大问题,如果lib1和lib2就是要求用不同的libfoo.so里的算法才正确,那么这种就无解了,除非是将比如lib1依赖的libfoo.so改名如libfoo_1.so(而且是内部元数据也要改名,不单单是路径名称),然后将lib1的依赖libfoo.so改成依赖新的libfoo_1.so;

linux居然不支持一个可执行文件依赖路径上间接可以使用多个相同签名的依赖库,难怪会有依赖地狱的问题(如果linux整个都不支持同名共享库的加载,那就是真的gg了,不过应该不会不然rpath有个毛用)

linux给一个程序加载依赖库的原理应该是根据元数据里的依赖递归的加载到一个盒子里,因此加载lib1的时候递归也会将lib1的libfoo放到该盒子里,然后后面加载lib2的libfoo时发现盒子里已经有这个依赖库了就不再加载了(所以优先判断“盒子”)

 

可以用如下命令将一个共享库真正改名:(archlinux测试了一下,似乎改文件名就行了。。但似乎是ldd的bug,只是显示能加载,但是实际上加载的还是旧的,还是需要改elf里面的名字

patchelf --set-soname libfoo_newname.so libfoo.so
# 这里是将libfoo.so文件的elf格式(元数据)里的库名改成libfoo_newname.so

可以用这个命令将一个共享库libtest2.so依赖libfoo.so改成依赖libfoo_newname.so

patchelf --replace-needed libfoo.so libfoo_newname.so libtest2.so

 

鉴于linux对单个程序是无法同时使用两个一模一样的共享库的原因,所以可执行程序的依赖库就都放到其所在目录的lib目录下,而递归查找出来的其他共享库也放该目录下,然后记录中间出现的需要覆盖的问题。

 

妈的,似乎是哪怕程序a依赖库1和库2,而库1和库2分别依赖库foo的两个版本,且这两个版本的名称是完全区分开来了,但是里面提供给库1和库2的方法名是一样的(实现不一样)

这个时候linux居然加载了foo1.so和foo2.so但是执行库1和库2时,两个库调用的都只是foo1.so里的方法(也就是TM的现在不光是库名不能重复,连里面的方法声明如果重复都TM会只调用一个。。)

https://segmentfault.com/a/1190000021920959,具体详情可以看这个,有个什么全局符号,默认funcAdd没有加作用域是全局的(不管怎么说,库就算了,方法重名都不会自动进行分别调用就是傻逼)

经过查找,可以用<dlfcn.h>里而dlopen()和dlsym()实现a引用了m和n两个共享库,而m和n又分别调用了y和z里符号完全一样的fff方法,执行a时可以保证m调用y的fff,n调用z的fff,

但是dlopen是动态加载技术,可能会通过ldd找不到,假设编译时没有加-l   ?】

 

liba如果调用了libb的方法,在编译阶段,会把要调用的方法symbol信息写到liba里(libb里自然肯定有它自己是提供方)

所以当符号链接不是GLOBAL时,需要重新编译一下liba;(不行,除非有源码)

 

可以将两个共享库合并为一个:g++ -Wl,-Bsymbolic -shared layer.o conflict.o -o libconflict.so

 

但是找到解决a依赖libm和libn,而libm和libn又分别依赖libk的不同版本(哪怕库名也不一样)的但是函数签名相同的某个函数,之前的话最终libm和libn只会调用到其中一个版本(或说其中某个版本的方法);现在可以这样解决:ar cr libk_combine.a libm.so libk_m.so

然后之前是a依赖libm.so,现在改成用libk_combine.a来依赖(但是打包的时候libm.so和libk_m.so和libk_combine.a都需要一起打包进来)

 

【经过查找资料,除非反编译,没有源码的情况下无法将两个so文件重新编译成一个so文件】

 

但至少,这个应该只限定一个程序会有这样的冲突,不至于程序a加载了某个方法,程序b也会自动用这个方法,否则如果共享库有状态就gg了;而且nixos也无法实现

 

14.如果一个库没有任何依赖,会是这样:

ldd /usr/lib64/ld-linux-x86-64.so.2
        statically linked

 

15.共享库的真实名称(单纯改文件名没有意义),可以通过:patchelf --print-soname fff.so 来查看

 

16.linux共享库问答:

先说防止:当动态库的ABI发生改变后,升级库的大的版本号。实际上就是在编译库时指定一个版本号。
如果程序使用了新的动态库,则链接指定版本的动态库。这样在运行时,loader就会装载正确的动态库。
当系统里有多个版本的动态库和使用不同版本动态库的程序,都可以用这种方法互不干扰。
如果动态库的ABI发生改变而没有版本区分,而且在系统的不同目录中存在多个版本,程序运行就看运气了,看loader的搜索路径,
有些机器正确就运行正常,有些就会崩溃。要检测符号冲突,就得编写脚本检查了。如果偷懒,在编译程序时,linker会帮你检查。
比如有3个动态库: a.so/b.so/c.so,都存在'print()'函数,如果你的程序用了'print()'函数并且同时链接a.so/b.so/c.so 这3个库,
则linker会报错。但如果你只链接a.so,而系统中同时存在a.so/b.so/c.so,运行程序时,loader只装载a.so,不会出现符号冲突。


windows也有这样的符号冲突问题吗?还是linux独有的?比如:a程序依赖m和n库,而m和n库又依赖y和z库,
然后y和z库里都有完全一样命名但是不同实现的方法fff,m和n就是需要分别用这两个方法,然而linux却无法做到m和n去分别调用y和z里的fff实现,
而是要么是调用y里的要么是调用z里的;windows是不是就能自动识别m是调用y里的fff,而n是调用z里的fff?

原理是一样的。你要相信大家都是懒人,能偷懒的地方就不会多做一步,那么该出问题的地方都会出问题。
linux的做法是先来先用,所有的库串联成一个单向链表,先来的库放表头,就先搜索到。
如果想做到m和n去分别调用y和z里的fff实现,算法太麻烦,还要保留很多信息,如果多交叉引用几个函数,系统就乱如麻了。
所以现在只是有自身函数优先的参数。比如m, y和z里有fff实现,装载时间为y<z<m,缺省是m用y中的fff实现。
但是m里面也有fff实现,就是因为不信任y和z里的fff实现,这时可以设置参数,
不管别的库怎么排序,先用自身的函数,自身没有了再找外部。


哦哦,那再问一下这个应该只限于单个程序吧,不会说程序a里依赖的m共享库有fff方法在运行,
此时程序b也起来了它依赖了n共享库也有fff方法,然后b程序最终却使用的是m共享库的fff方法,这种应该不会出现吧。。

是的,这个问题只限于单个程序。并且在同一个环境,行为是确定的。这是因为对于进程空间其实是私有的。其它进程不能跨进来影响到。


不过还是挺奇怪的,如果windows也是这样的原理,按理说依赖的外部库多了的程序还是很容易出现类似的问题的,但是好像很少看到windows程序有依赖问题?

windows程序的依赖问题同样是大问题。你可以搜索一下DLL依赖问题。以前也有人想过各种招式解决,始终没有好的解决方案。
到现在倒反用另外的方式解决了这个问题,就是采用容器。程序A依赖库 b/c/d,那就在容器里只放上它依赖的最小集合。
那样,不管外界怎么变化/升级,程序A的运行环境是确定的。只是这种方法有点费硬盘。

 

posted on 2023-08-19 10:00  Silentdoer  阅读(255)  评论(0编辑  收藏  举报