• Linux 动、静态库原理深剖


    传统艺能😎

    小编是双非本科大二菜鸟不赘述,欢迎米娜桑来指点江山哦
    在这里插入图片描述
    1319365055

    🎉🎉非科班转码社区诚邀您入驻🎉🎉
    小伙伴们,满怀希望,所向披靡,打码一路向北
    一个人的单打独斗不如一群人的砥砺前行
    这是和梦想合伙人组建的社区,诚邀各位有志之士的加入!!
    社区用户好文均加精(“标兵”文章字数2000+加精,“达人”文章字数1500+加精)
    直达: 社区链接点我


    在这里插入图片描述

    本质🤔

    动静态库的本质其实就是一个只差颜色的画,它是可执行程序的半成品 \color{red} {动静态库的本质其实就是一个只差颜色的画,它是可执行程序的半成品 } 动静态库的本质其实就是一个只差颜色的画,它是可执行程序的半成品

    在之前博客谈预处理的部分就说过,源文件和头文件要最终形成一个 .exe 可执行文件必须经历四步

    1. 预处理:头文件展开,去注释,宏替换,条件编译后形成 .i 后缀文件
    2. 编译:词法分析,语法分析,语义分析,符号汇总后形成汇编指令,生成 .s 后缀文件
    3. 汇编:汇编指令转二进制指令,生成 .o 后缀文件
    4. 链接:链接每个 .o 文件最后形成一个 .exe 可执行文件

    动静态库就是将可执行程序的前身,也就是将 .o 文件抠出来进行封装,然后程序中哪里需要用就直接调用库进行链接即可

    在这里插入图片描述
    在这里插入图片描述

    关于动静态库🤔

    在 Linux 下我们以下面代码为例:

    #include 
    
    int main()
    {
    	printf("hello world\n"); //库函数
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    结果一定是成功的打印 “hello world”,之所以可以调用 printf 输出,主要原因是 gcc 编译器在生成可执行程序时,将 C 标准库顺利链接进来了

    在Linux下,我们可以通过 ldd 文件名来查看一个可执行程序所依赖的库文件:
    在这里插入图片描述
    我们发现有个库libc.so.6是带路径的,我们不妨用 ls -l 命令看看他是何方神圣:
    在这里插入图片描述
    哟,这不是软链接

    实际上该软链接 libc-2.17.solibc.so.6 在同一个目录下,为了进一步了解,我们可以通过 file 文件名命令来查看 libc-2.17.so 的文件类型:
    在这里插入图片描述
    不难发现两个关键词:共享文件对象,动态链接,这就是我们所说的动态库

    在Linux当中,.so 后缀的是动态库.a 后缀的是静态库。
    在Windows当中,.dll 后缀的是动态库,.lib 后缀的是静态库

    libc.so.6 实际上就是动态库,去掉一个动静态库前缀 lib,再去掉后缀 .so 或者 .a 及其后面的版本号,剩下的就是这个库的名字了。而 gcc 编译器默认的是动态链接若想进行静态链接,需携带 -static 选项

    在这里插入图片描述
    我们再对比一下动态库和静态库,就会发现静态库的大小明显比动态库大了两个量级:

    在这里插入图片描述
    静态链接也会有这样的信息:

    在这里插入图片描述

    特征🤔

    静态库😋

    静态库在编译链接的时候会直接把库的代码复制到可执行文件当中,生成可执行程序在运行时将不需要静态库,因此使用静态库生成的可执行程序的大小一般比较大

    优点: \color{red} {优点:} 优点:
    使用静态库生成可执行程序后,该可执行程序就可以独自运行,不再需要库的支持
    缺点: \color{red} {缺点:} 缺点:
    使用静态库生成可执行程序会占用大量空间,特别是当有多个静态程序同时加载而这些静态程序使用的都是相同的库,这时在内存中会存在大量重复代码

    动态库😋

    动态库是程序运行时才去链接相应的动态库代码,多个程序共享使用库的代码。一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是函数的整个二进制编码

    在可执行文件开始运行前,外部函数的机器码由操作系统从磁盘上该动态库中复制到内存中,这个过程称为动态链接。动态库在多个程序间共享,节省了磁盘空间,操作系统采用虚拟内存机制允许物理内存中的一份动态库实现所有进程共用,节省了内存和磁盘空间

    在这里插入图片描述
    优点: \color{red} {优点:} 优点:
    节省磁盘空间,且库文件会通过进程地址空间进行共享,内存当中不会存在重复代码;减少页面交换,且适用于大规模的软件开发,使开发过程独立、耦合度小
    缺点: \color{red} {缺点:} 缺点:
    运行需要绝对依赖动态库

    打包静态库与使用🤔

    下面演示动静态库的打包与使用,以下面的四个文件为例,其中两个源文件 add.csub.c ,两个头文件 add.hsub.h 来实现一个简单的加减法接口

    add.h:

    #pragma once
    extern int my_add(int x, int y);
    
    • 1
    • 2

    add.c:

    #include "add.h"
    
    int my_add(int x, int y)
    {
    	return x + y;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    sub.h:

    #pragma once
    extern int my_sub(int x, int y);
    
    • 1
    • 2

    sub.c:

    #include "sub.h"
    
    int my_sub(int x, int y)
    {
    	return x - y;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    首先,我们需要对 .c 文件进行编译生成 .o 的目标文件:

    在这里插入图片描述
    再使用 ar 命令将目标文件进行打包

    ar 命令是 gnu 的归档工具,常用于将目标文件打包为静态库,下面我们使用ar命令的-r选项和-c选项进行打包:
    -r :若静态库文件当中的目标文件有更新,则用新的目标文件替换旧的目标文件。
    -c :建立静态库文件
    -t :列出静态库中的文件。
    -v :显示详细的信息

    打包成静态库:

    ar -rc libcal.a add.o sub.o
    
    • 1

    查看静态库文件:

    ar -tv libcal.a
    
    • 1

    效果如下:
    在这里插入图片描述
    在这里插入图片描述
    第三步组织头文件和静态库

    当我们把自己的库给别人用的时候,实际上需要给别人两个文件夹,一个文件夹放的是头文件,另一个放的是所有库文件

    因此,在这里我们可以将 add.h 和 sub.h 这两个头文件放到一个名为 include 的目录下,将生成的静态库文件 libcal.a 放到一个名为 lib 的目录下,然后将这两个目录都放到 mathlib 下,此时就可以将 mathlib 给别人用了
    在这里插入图片描述
    第四步进行 Makefile

    上述所要执行的命令全部写到 Makefile 当中,后续要生成静态库以及组织头文件和库文件时就可以直接 make 一步到位,不至于每次都要敲这么多命令,所以 Makefile 是真的香香!

    mylib=libcal.a 
    CC=gcc
    
    $(mylib):add.o sub.o 
    
    	ar -rc -o $(mylib) $^ 
    
    %.o:%.c
    
    	$(CC)  -c $<
    
    .PHONY:clean 
    clean: 
    
    	rm -f $(mylib)./*.o 
    
    .PHONY:output 
    output: 
    
    	mkdir -p mlib/include 
    	mkdir -p mlib/lib 
    	cp ./*.h mlib/include 
    	cp ./*.a mlib/lib 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    make output 就能将头文件和静态库组织起来

    在这里插入图片描述
    接下来就可以使用了!我们直接代入 main.c 进行尝试:

    #include 
    #include 
    int main()
    {
    	int x = 5;
    	int y = 10;
    	int z = my_add(x, y);
    	printf("%d + %d = %d\n", x, y, z);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    此时使用 gcc 编译 main.c 生成可执行程序时需要携带三个选项

    -I:指定头文件搜索路径。
    -L:指定库文件搜索路径。
    -l:指明需要链接库文件路径下的哪一个库

    gcc main.c -I./mlib/include -L./mlib/lib -lcal
    
    • 1

    此时就生成了可执行程序:

    在这里插入图片描述
    我们执行一下成功运行,结果如下:

    在这里插入图片描述

    打包动态库与使用🤔

    动态库和静态库操作基本一样,只有一点小差别

    位置无关码😎

    首先生成编译后的 .o 后缀对象文件,但是注意动态库需要携带 -fPIC 选项:即位置无关码

    在这里插入图片描述
    -fPIC 作用于编译阶段,告诉编译器产生与位置无关的代码,代码中没有绝对地址,全部使用相对地址,从而代码加载到内存的任意位置都可以正确执行。这正是共享库所要求的,共享库被加载时,在内存的位置不是固定的

    如果不加-fPIC选项,则加载.so文件的代码段时,引用的数据对象需要重定位,重定位会修改代码段的内容,这就造成每个使用这个库文件的进程在内核里都会生成这个代码段的拷贝,并且每个拷贝都不一样,取决于这个 .so 文件代码段和数据段内存映射的位置

    不加 -fPIC 编译出来的 .so 是要在加载时根据加载到的位置再次重定位的,因为它里面有位置无关代码。如果该 .so 文件被多个应用程序共同使用,那么它们必须每个程序维护一份 .so 的代码副本(因为 .so 被每个程序加载的位置都不同,显然这些重定位后的代码也不同,当然不能共享)

    我们总是用 -fPIC 来生成 .so,但从来不用 -fPIC 来生成 .a;但是 .so 一样可以不用 -fPIC 选项进行编译,只是这样的 .so 必须在加载到用户地址空间时重定向所有表目

    再使用 gcc 的 shared 命令将目标文件进行打包

    gcc -shared -o libcal.so add.o sub.o
    
    • 1

    在这里插入图片描述
    第三步组织头文件和静态库

    这一步就和静态库一模一样了,不在赘述:
    在这里插入图片描述

    第四步进行 Makefile

    后续也可以直接 make 一步到位,兄弟们还是那句话 Makefile 真的香的PB!

    mylib=libcal.so 
    CC=gcc
    $(mylib):add.o sub.o 
    	$(CC) -shared -o $(mylib) $^ 
    
    %.o:%.c 
    	$(CC) -fPIC -c $<
    
    .PHONY:clean 
    clean: 
    	rm -rf $(mylib)./*.o 
    .PHONY:output 
    output: 
    	mkdir -p dlib/include 
    	mkdir -p dlib/lib 
    	cp ./*.h dlib/include 
    	cp ./*.so dlib/lib 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    在这里插入图片描述
    接下来就可以正常使用了,这里依然用 main.c 来实验:

    #include 
    #include 
    int main()
    {
    	int x = 5;
    	int y = 10;
    	int z = my_add(x, y);
    	printf("%d + %d = %d\n", x, y, z);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    此时使用 gcc 编译 main.c 生成可执行程序时,需要用-I选项指定头文件搜索路径,用-L选项指定库文件搜索路径,最后用-l选项指明需要链接库文件路径下的哪一个库:

    gcc main.c -I./mlib/include -L./mlib/lib -lcal
    
    • 1

    在这里插入图片描述
    最后执行成功,结果如下:
    在这里插入图片描述
    此时我们ldd 命令查看他依赖的库就可以看到我们的库:

    在这里插入图片描述

    找不到动态库报错🤔

    如果你出现了如下画面,请不要慌张:

    在这里插入图片描述
    使用 -I,-L,-l 这三个选项告诉编译器我们使用的头文件和库文件在哪里以及是谁,但是当生成的可执行程序生成后就与编译器没有关系了,运行起来后操作系统找不到该可执行程序所依赖的动态库,此时的 ldd 命令是这样的:

    在这里插入图片描述
    解决该问题的方法有以下三个

    方法一:拷贝.so文件到系统共享库路径下

    sudo cp mlib/lib/libcal.so /lib64
    
    • 1

    既然系统找不到我们的库文件,那么我们直接将库文件拷贝到系统共享的库路径下,这样一来系统就能够找到对应的库文件了

    方法二:更改 LD_LIBRARY_PATH

    LD_LIBRARY_PATH是程序运行时查找库所要搜索的路径,我们只需将动态库所在的目录路径添加到 LD_LIBRARY_PATH 环境变量当中即可

    export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/cl/BasicIO/testlib/project/mlib/lib
    
    • 1

    在这里插入图片描述
    这种方法有点复杂,操作和代码成本都是,因为该操作是临时性操作,在每次打开 Linux 后都需要像这样配置一次!

    方法三:配置/etc/ld.so.conf.d/

    /etc/ld.so.conf.d/路径下存放的全部都是以.conf为后缀的配置文件,这些配置文件中存放的都是路径,系统会自动在 /etc/ld.so.conf.d/ 下找所有配置文件里面的路径,之后在每个路径下查找你所需要的库。

    所以我们要先将库文件所在目录的路径存入一个以 .conf 为后缀的文件当中:
    在这里插入图片描述
    然后将该 .conf 文件拷贝到 /etc/ld.so.conf.d/ 目录下
    在这里插入图片描述

    最后还需要使用 ldconfig 命令将配置文件更新一下,更新之后系统就可以找到该可执行程序所依赖的动态库了

    sudo ldconfig
    
    • 1

    外部库安装使用😋

    Linux 中经常还会使用到一些比较常见或者热门的开源库,比如 C++ 的 boost ,嵌入式的 SQLite,libevent 网络库,mcrypt/libmcrypt 加密算法拓展库等

    这些库如果通过 Linux 进行 yum insatll 操作下载的话,就只需要进行ldconfig 命令更新一下就可以使用了!本地下载的话需要按上面步骤在解压后配置即可

  • 相关阅读:
    使用Fiddler进行手机抓包
    解决表单action属性传参时值为null的问题
    当他们在私域里,掌握了分寸感
    Java密码库Password4j
    【ELM回归预测】探路者优化极限学习机回归预测【含Matlab源码 2231期】
    使用 Docker Compose 部署 RabbitMQ 的一些经验与踩坑记录
    【NOI模拟赛】最大生成树(Brouvka算法,并查集)
    ORB-SLAM2从理论到代码实现(十四):KeyFrame类长
    复现MySQL的索引选择失误以及通过OPTIMIZER_TRACE分析过程
    MFC|按钮的自绘
  • 原文地址:https://blog.csdn.net/qq_61500888/article/details/127715274