• 动静态库--Linux


    🚩库的含义

    大伙最开始接触计算机语言–C语言时,应该都是从printf(“Hello world!\n”)写起的吧(怀念捏😏)。当时只知道必须得写上头文件stdio.h,但是并不清楚为什么要这么做。后来随着逐渐学习,又了解到原来包含的头文件是C语言提供的库哇,但是也仅限于此了。库到底是干什么的,又是怎样的结构呢?

    库就像是一个打包器,将你所需要用的所有函数、宏等等全都包含在内。就比如上面使用到的printf函数,首先这肯定不是我们自己写的,我们也写不出这么底层的函数,因为这需要和硬件(键盘)建立联系。那么就只能依靠会写的人(C委员会)给我们写咯,写完之后为了方便我们使用,把写的全部打包成库发给我们就好了,我们只需要知道有这么个东西能用就好了,完全不关心是怎么实现的。这也是库存在的意义–给使用者提供现成的,能够直接使用的一些方法或结构。

    还有一个问题,我们自己写的程序要想使用库中的内容,就必须还得将别人的程序加载到我们自己的程序中,这就需要在编译的阶段将自己程序形成的.o文件与库中的.o文件链接起来一起形成可执行文件。这从侧面也能说明库里面包含了各种.o文件和头文件。

    🚩动静态库及简单实现

    库分为静态库和动态库两种,从字面意义可以理解为时态不同的两种库。啥意思呢?下面详细介绍。

    🍁静态库

    静态库是程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库。由于需要全部将库链接加载进去,可以看出静态库的所占用的空间是比较大的,并且对一个程序来讲,库里的绝大多数内容都是用不到的。

    其实库的实现倒是很简单,🔺难点在于如何去使用自己写的库。

    🍁动态库

    程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚 拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。

    🍁动静态库的实现

    我先假设有两个函数功能要实现:累加函数、打印函数(字符串与时间戳糅合输出)。

    🍂累加函数

    image-20221107215952343

    累加函数很简单,从begin累加到end,并将累加值返回。

    头文件:

    #pragma once 
    #include
    extern int accrual(int begin,int end);
    
    • 1
    • 2
    • 3

    源文件:

    #include"Mymath.h"
    #include
    int accrual(int begin,int end)
    {
        int add=0;
        while(begin<=end)
        {
            add+=begin;
            begin++;
        }
        return add;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    🍂打印函数

    image-20221107220438022

    参数传一个字符串,直接输出该字符串与时间戳。

    头文件:

    #pragma once 
    #include
    extern void printstr(const char* str);
    
    • 1
    • 2
    • 3

    源文件:

    #include"Myprint.h"
    #include
    #include
    void printstr(const char* str)
    {
        printf("%s---->%lld\n",str,(long long)time(NULL));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    函数写好后,我们得把文件打包成库,思路就是库里有一个存放所有头文件的文件夹,有一个存放所有.o文件的文件夹。那么我们在makefile中直接实现一下

    🍂makefile

    具体的细节都在代码中了,需要注意的就是特殊文件生成的一些指令,以及形成的.a和.so文件名的命名有点要求:lib+库文件名+后缀,别问,问就是规定就是这样的😅,不带就会出现使用的时候匹配失败的情况。

    .PHONY:all
    all:libMymath.a libMymath.so //库文件名为Mymath,因此命名方式分别为libMymath.a 和 libMymath.so
    
        
    //静态库的.o文件要链接形成.a文件,代表是静态库
    libMymath.a:Mymath_s.o Myprint_s.o
    	ar -rc libMymath.a Mymath_s.o Myprint_s.o  //特殊的编译形式: ar -rc xxx.a xxx.o xxx.o ...
    //两个函数形成各自的.o文件(注意区分下面动态库依赖的.o文件名!!!)
    Mymath_s.o:Mymath.c
    	gcc -c Mymath.c -o Mymath_s.o
    Myprint_s.o:Myprint.c
    	gcc -c Myprint.c -o Myprint_s.o
    
        
    
    //动态库的.o文件要链接形成.so文件,代表是动态库    
    libMymath.so:Mymath.o Myprint.o
    	gcc -shared -o libMymath.so Mymath.o Myprint.o //特殊编译命令:-shared 表示是共享的
    //两个函数形成各自的.o文件,比较特殊的是也加了指令:-fPIC,表示形成与位置无关的.o文件
    Mymath.o:Mymath.c
    	gcc -fPIC -c Mymath.c -o Mymath.o
    Myprint.o:Myprint.c
    	gcc -fPIC -c Myprint.c -o Myprint.o
    
        
        
    //形成动静态库
    .PHONY:lib 
    lib:
    	//存放静态库lib-static中的.a文件于其lib文件夹中
    	//存放静态库lib-static中的头文件于其include文件夹中
    	mkdir -p lib-static/lib    
    	mkdir -p lib-static/include 
    	cp *.a lib-static/lib        
    	cp *.h lib-static/include
    	
        //存放静态库lib-dyl中的.so文件于其lib文件夹中
        //存放静态库lib-dyl中的头文件于其include文件夹中
        mkdir -p lib-dyl/lib 
    	mkdir -p lib-dyl/include
    	cp *.so lib-dyl/lib 
    	cp *.h lib-dyl/include
            
            
    //清除库
    .PHONY:clean
    clean:
    	rm -rf *.o *.a *.so lib-static lib-dyl
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    ⌨测试一下:

    image-20221107231002779

    第2步make与make lib之后,确实将各个文件与库形成了。第4步make clean之后又把所有的文件删得干干净净,不得不说,makefile用着就是舒爽。

    🍁动静态库的使用

    🍂静态库的使用

    为了实验达到效果,我们单独再创建一个文件夹libtest,与刚刚生成的各种文件与库分隔开,专门用来测试我们能不能正常使用库。

    🍃接着要想使用静态库,我们得把静态库给拷到新建的文件夹下。

    image-20221107233947306

    🍃我们接着在libtest.c中使用我们写的两个函数试试

    #include
    #include"Mymath.h"  //头文件包含
    #include"Myprint.h"
    int main()
    {
        int begin=0;
        int end=5;
        int result=accrual(begin,end);
        printf("accrual result:%d\n",result);
        printstr("What time is it now?");
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    image-20221107235327115

    发现想要编译都不行,报的错误是找不到对应函数,也就是找不到头文件。原因在于使用静态库要在编译的时候链接上,而我们并没有这么做。有两种做法,我们接下来一一尝试。

    < 做法1 >将自己的头文件和库文件拷贝到系统路径下–(污染库,除非对自己的库特别有信心,否则不推荐!)

    系统的头文件路径:/usr/include/

    系统的库文件路径:/lib64/

    image-20221108002126821

    我们在添加的时候需要的权限还比较高,由此也可见往系统路径中添加第三方库的谨慎性。

    🔺即使我们在系统路径中添加了相应的文件,但是在编译的时候仍旧要加上 -l Mymath(中间有没有空格无所谓),-l表示链接,后面的Mymath就是第三方库的名字,在库实现中的makefile开头中我已经提到了库文件命名的规则。

    上面的过程也叫安装库,接下来我们进行删除库的操作。

    image-20221108001958242

    同样是需要更高的权限才能删除库。

    然后我们再次运行发现删完库之后确实没法完成编译了。

    image-20221108002057241

    之所以污染库是因为第三方不一定安全,很有可能发生无法预知的bug,因此最好是不要采取这种方式使用第三方库。

    <做法2🔺>使用 -I(大写的i)+第三方库的头文件目录 -L+第三方库的文件目录 -l+第三方库中具体的库文件 三个指令进行编译

    感觉文字描述的不是特别清晰,直接上操作吧:

    image-20221108003533085

    好长一大串的编译指令😵,但确实完成编译了。总体的思想就是先链接第三方头文件,再来链接第三方库,最后再链接第三方库中的具体库文件,麻烦但是有效。

    🍂动态库的使用

    总共有四种做法:

    <做法1>将自己的头文件和库文件拷贝到系统路径下–(污染库,除非对自己的库特别有信心,否则不推荐!)

    和静态库的第一种使用方式一样,这里我就不再重复实验了。


    <做法2>添加动态库文件目录的绝对路径到环境变量:LD_LIBRARY_PATH 中去,再次使用三个指令进行编译链接(最后一步与静态库的做法2一样)。

    image-20221108172547093

    🔺为什么不像静态库那样直接在编译时链接头文件目录、库文件目录、具体库文件呢?因为这仅仅是在编译阶段告诉编译器库的具体位置,使用静态库时,在编译时就已经把库加载到程序中去了,只要编译过了,形成的可执行的文件中就能够找到库和库函数的地址(链接那一步完成的),因此只要程序跑起来,由于库已经在程序内部加载好了,程序不会出一点问题。

    但是换到使用动态库的场景时,如果也是直接在编译时完成三种链接,虽然告诉编译器库在哪了,形成的可执行文件里也有库和库函数的地址,但是程序跑起来的时候,动态库没提前加载到程序中,一旦使用对应的库内容,链接去找,然后发现找不到。(只有库的地址,库没加载是没法使用库的。)因此得在程序运行的时候,用一些手段辅助去帮助找库,一旦用到了与库相关的内容,就通过辅助手段只去加载需要用到的库内容,完成动态库的使用。

    这也是我们在环境变量添加动态库绝对路径作为辅助找库的手段的原因。

    但是,由于是在环境变量中添加的,环境变量是从配置文件中读取的,只要重启服务器,就会自动刷新环境变量,原来添加的动态库路径就找不到了。因此直接在环境变量中添加动态库的绝对路径是不够方便的,倒不如直接在配置文件中添加,一劳永逸。接下来我们就介绍这个方法。

    <做法3>添加动态库文件目录的绝对路径到配置文件:ld.so.conf.d

    Linux的/etc/路径下的配置文件ld.so.conf.d中保存着动态库

    image-20221109105211717

    我们随便打开一个配置文件下存放的路径对应的库文件目录,发现确实是存放了一堆的.so的文件。

    既然已经知道在哪添加了,接下来就试试把第三方库的路径添加到配置文件中去(注意不是添加文件,是添加路径)

    🍃首先要手动在/etc/ld.so.conf.d/路径下创建一个配置文件,用来存放第三方库的绝对路径(该路径是具体到使用的哪一个库的路径而不只是到库文件目录)。

    🍃接着调用ldconfig,更新配置文件,使得刚刚新添加的配置文件生效。

    image-20221109115839481

    现在我们使用ldd指令查看libtest可执行文件的共享库依赖情况:

    image-20221109121414259

    确实是通过配置文件找到了动态库。而一旦我们删除配置文件并再次使用ldconfig更新配置文件,就会出现依赖关系的缺失。

    image-20221109152716234

    <做法4>在系统路径下建立软连接,链接到使用的库文件。

    🔺注意,这里仅仅只是在系统的库里添加了软链接,并没有真正添加库文件,相当于有一层跳转的保护,并不会污染库。

    image-20221109154401800

    需要一提的是,建立软连接起到了类似于建立库文件的作用,所以并不需要在编译的时候指明要链接哪个库目录了,只需要链接头文件目录和库文件就成。编译的时候找库文件会直接遍历系统的库文件,然后找到我们建立的软链接跳转到对应的库文件。

    删除软链接之后就会使库文件的依赖关系消失:

    image-20221109160734314

    可以看出建立软连接是比较妥当的方法,但是每个方法都可能会有自己的特殊应用场景,同时也看个人喜好了。

    🍁动态库使用的深度理解

    image-20221109171141205

    多个进程同时运行时,也只需要加载一个动态库到内存中去,动态库映射的位置是虚拟地址空间的共享区。并且由于动态库文件与位置无关,因此动态库在内存中的位置并不是固定的。所以以上就是和静态库的区别了,假如使用静态库,有一个进程就会有一个对应的静态库被加载到内存中去,并且静态库的位置对于使用它的进程是完全透明可知的。而动态库必须得有辅助手段去帮助寻找动态库在内存的位置,编译和运行时都是需要去找动态库的位置的。

    🚩总结

    动静态库的知识总结下来,就是多个文件的链接和在内存中的加载关系。上面的知识有助于我们去正确使用别人写好的第三方库,这会给我们今后的学习带来诸多便利,也能去尝试很多好玩的库。总之动静态库的内容并复杂,但是确实我们程序员必须要掌握的知识。

  • 相关阅读:
    VScode---php环境搭建
    【精华】具身智能:人工智能的下一个浪潮
    arcgis插件 批量出图 按地块批量出图工具
    Java 数组
    vue通过接口下载文件的封装及使用
    为大模型而生!顶流大佬发起成立学术会议 COLM,或成为未来 NLP 最强顶会?!
    2022年在uniapp中引入vant Weapp
    神经网络pid控制器设计,pid神经网络什么原理
    查看Android应用方法耗时
    SpringCloud Alibaba - Sentinel
  • 原文地址:https://blog.csdn.net/qq_63412763/article/details/127774464