Linux下的动态库和静态库

144 篇文章 6 订阅

什么是库?

在 Linux 开发时,我们经常会看到一些形如 xxx.so 的名称出现,其中 so 是 Shared Object 的缩写,即可以共享的目标文件,也就是我们所称为的动态链接库,和在 Windows 下大家玩游戏时遇到的 xxx.dll 错误中的文件是一个类型的。

面试 | Linux 下的动态链接库问题

 

所谓“库”,就是稳定成熟的可以复用的代码;库从本质上来说是一种可执行代码的二进制形式,可以被操作系统载入内存执行。

库有两种:静态库(.a(linux)、.lib(windows))和动态库(.so(linux)、.dll(windows))。

所谓静态、动态是指链接,可以看下编译链接的过程:面试中经常会问到以下问题:

  • 怎么创建一个动态库?
  • 动态库文件的后缀名是什么?
  • 怎么使用一个动态库?
  • 动态库的命名规范?
  • 系统默认的动态库的查找路径?
  • 动态库显示连接所使用的系统库是什么?

在一个程序的编译过程中,分为以下几个步骤:预处理编译汇编链接。本文中讨论的链接库就是针对最后一个步骤「链接」而言的。

面试 | Linux 下的动态链接库问题

 

动态库和静态库的区别

左图为静态链接库,右图为动态链接库

面试 | Linux 下的动态链接库问题

对于静态链接库而言在链接阶段,会将汇编生成的「目标文件.o」与引用到的库一起链接打包到可执行文件中。因此对应的链接方式称为静态链接:

  • 静态链接库对函数库的链接是放在编译时期完成的。程序在运行时与函数库就没有了任何的联系。
  • 它比较浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件。
  • 静态库对程序的更新和发布也会带来麻烦。如果静态库更新了,所有使用它的应用程序都需要重新编译、部署、发布给用户。

静态链接可以理解为最后生成了一个「单文件免安装绿色版」的程序,优点在于移植的时候只需要移动这一个文件,缺点在于文件体积非常大,为了解决这样的问题,就有了动态链接库。动态链接库在程序编译时并不会被连接到目标代码中,而是在程序运行时才被载入。

  • 不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,可以实现进程之间的资源共享。(因此动态库也称为共享库)规避了空间浪费问题。
  • 动态库在程序运行时才被载入,也解决了静态库对程序的更新、部署和发布带来的麻烦。用户只需要更新动态库即可将一些程序升级变得简单,增量更新。

库与可执行文件区别

  • 库文件无法直接执行,从生产库的源码中可以查看出,源码是没有`main`函数,都是一些函数模块的定义和实现,由于没有主入口,所以无法直接运行库。

动态库是什么?

动态库连接到系统空间,如果多个程序连接了同一个库,那么只需要一份,优点在于编译程序的时候不会将对应的库文件全部打包在生成的程序中,而是保留了到对应库的链接,缺点就是移植的时候如果只移动了对应的程序没有安装相关的库的话,就会看到类似以下喜闻乐见的结果了。

面试 | Linux 下的动态链接库问题

在 Linux 下一个动态库一般由三个不同名字的文件组成:

  • soname 文件
  • lib + 链接库名字 + .so + .版本号
  • real name 文件
  • lib + 链接库名字 + .so + .版本号.次版本号.发行号
  • linker name 文件
  • lib + 链接库名字 + .so

当程序在内部列出所需要的链接库时,仅仅使用 soname。当你创建一个链接库时,使用 real name。安装一个新的链接库时,把它复制到一个DLL文件夹里,然后运行程序 ldconfig。ldconfig 检查存在的 real name 文件,并且创建指向它符号链接 soname 文件。可能大家比较常见到的有 libsodium 等。

创建动态库的步骤:

  • 编写源文件。

  • 将一个或几个源文件编译链接,生成共享库。

  • 通过 `-L -lxxx` 的`gcc`选项链接生成的`libxxx.so`。

  • 把`libxxx.so`放入链接库的标准路径,或指定 `LD_LIBRARY_PATH`,才能运行链接了`libxxx.so`的程序。

创建一个动态库

有了上面关于库的一些基础知识之后,我们可以开始尝试创建一个动态库来供程序使用了。

比如我们有一个求最大值的函数 max(int a,int b,int c) ,放在文件 max.c 中文件内容如下:

面试 | Linux 下的动态链接库问题

可以通过:

面试 | Linux 下的动态链接库问题

将其编译为共享库,-fPIC是编译选项,PIC是 Position Independent Code 的缩写,表示要生成位置无关的代码,这是动态库需要的特性; -shared是链接选项,告诉 gcc 生成动态库而不是可执行文件。为了让用户知道我们的动态库中有哪些接口可用,我们需要编写对应的头文件,比如可以写一个 max.h :

面试 | Linux 下的动态链接库问题

设置一个驱动函数来测试我们编写的动态库:

面试 | Linux 下的动态链接库问题

通过 gcc test.c -L. -lmax来生成 a.out,其中-lmax表示(小写的L)要链接 libmax.so,-L.表示搜索要链接的库文件时包含当前路径。

同一目录下同时存在同名的动态库和静态库,比如 libmax.so和 libmax.a都在当前路径下,则 gcc 会优先链接动态库。

但是这样直接运行的话,会出现一个错误:

面试 | Linux 下的动态链接库问题

由于 Linux 是通过/etc/ld.so.cache文件搜寻要链接的动态库的,而 /etc/ld.so.cache 是 ldconfig 程序读取 /etc/ld.so.conf 文件生成的,本次使用的动态库 libmax.so 并不在对应的目录下,就会导致程序无法找到对应的动态链接库,这样我们的解决方法有二:

  • 如果仅仅是本地使用,可以在编译后指定一个环境变量:LD_LIBRARY_PATH=. ./a.out ,这样程序会在本地寻找
  • 如果需要在系统层面共享这个库,可以把 libmax.so 所在的路径添加到 /etc/ld.so.conf 中,再以 root 权限运行 ldconfig 程序,更新 /etc/ld.so.cache
  • 具体采用的方法因使用场景而异,如果仅仅是测试用途的话,可以直接使用添加环境变量的方式解决。

当系统加载可执行代码时候,能够知道其所依赖的库的名字,但是还需要知道绝对路径。此时就需要系统动态载入器(dynamic linker/loader)。

如何让系统能够找到它:

  • 如果安装在/lib或者/usr/lib下,那么ld默认能够找到,无需其他操作。

  • 如果安装在其他目录,需要将其添加到/etc/ld.so.cache文件中,步骤如下:

    • 编辑/etc/ld.so.conf文件,加入库文件所在目录的路径

    • 运行ldconfig ,该命令会重建/etc/ld.so.cache文件

对于elf格式的可执行程序,是由ld-linux.so*来完成的,它先后搜索elf文件的 DT_RPATH段—环境变量LD_LIBRARY_PATH—/etc/ld.so.cache文件列表—/lib/,/usr/lib 目录找到库文件后将其载入内存。

 

 

静态库是什么?

静态库实际就是一些目标文件(一般以.o结尾)的集合,静态库一般以.a结尾,只用于链接生成可执行文件阶段。这里注意区分下linux环境下是以.a结尾的,而windows环境下是以.lib结尾的(我这里就使用linux版本进行,文章就主要以linux环境进行撰写)

在链接步骤中,连接器将从库文件取得所需的代码,复制到生成的可执行文件中。这种库称为静态库;

特点:

  • 可执行文件中包含了库代码的一份完整拷贝

缺点:

  • 缺点就是被多次使用就会有多份冗余拷贝。

  • 静态库对程序的更新、部署和发布页会带来麻烦。如果静态库`libxxx.lib`更新了,所以使用它的应用程序都需要重新编译、发布给用户;

静态库生成

创建静态库的步骤:

  • 写源文件,通过 `g++ -c xxx.cpp` 生成目标文件。

  • 用 `ar` 归档目标文件,生成静态库。(ar cr libhello.a hello.o)

  • 使用静态库中函数的头文件。

  • 使用静态库时,在源码中包含对应的头文件,链接生成的静态库。

生成静态库案例

文件:aLibTest.h aLibTest.cpp main.cpp

root@iZuf67# ls aLibTest.cpp aLibTest.h main.cpp

aLibTest.h

#include <iostream>

using namespace std;

int getParam();

aLibTest.cpp

#include "aLibTest.h"

int getParam()
{
    int iparam = 10;
    iparam++;
    return iparam;
}

制作动态库:

  • 先编译生成`.o`链接文件

    root@iZuf67# g++ -c aLibTest.cpp 

  • root@iZuf67# ls aLibTest.cpp aLibTest.h aLibTest.o main.cpp

  • 归档,生成静态库

    root@iZuf67# ar cr libaLibTest.a aLibTest.o
    root@iZuf67# ls
    aLibTest.cpp  aLibTest.h  aLibTest.o  libaLibTest.a  main.cpp
  • main函数调用

    main.cpp

    #include "aLibTest.h"
    
    int main()
    {
        int res = getParam();
        cout<<"res:"<<res<<endl;
        return 0;
    }
    
  • 链接

    root@iZuf67# g++ main.cpp -L. -laLibTest
    root@iZuf67# ./a.out 
    res:11
    
  • 通过使用makefile做到自动化

    CXX=g++
    
    build:libaLibTest.a
    
    libaLibTest.a:aLibTest.o
     ar crv $@ aLibTest.o
    
    aLibTest.o:aLibTest.cpp
     $(CXX) -c aLibTest.cpp
    
    target:aLibTest
    
    aLibTest:main.cpp
     $(CXX) main.cpp -o $@ -L. -laLibTest
    

    使用make build会构建出libaLibTest.a,使用make target生成可执行文件;

 

小插曲(常用参数)

  • -l参数就是用来指定程序要链接的库,-l参数紧接着就是库名,拿数学库来说,它的库名是m,它的库文件名是`libm.so`,很容易看出,把库文件名的头`lib`和尾`.so`去掉就是库名了。

  • -L参数跟着的是库文件所在的目录名。比如我们把`libtest.so`放在`/tmp`目录下,那链接参数就是`-L/tmp -ltest`另外,大部分`libxxxx.so`只是一个链接;

  • `-include`用来包含头文件,但一般情况下包含头文件都在源码里用`#include xxxxxx`实现,`-include`参数很少用。`-I`参数是用来指定头文件目录,`/usr/include`目录一般是不用指定的,`gcc`知道去那里找,但是如果头文件不在`/usr/include`里我们就要用`-I`参数指定了,比如头文件放在`/myinclude`目录里,那编译命令行就要加上`-I/myinclude`参数了,如果不加你会得到一个`"xxxx.h: No such file or directory"`的错误。`-I`参数可以用相对路径,比如头文件在当前目录,可以用`-I.`来指定;


参考:https://juejin.cn/post/6886962863089385479
https://www.toutiao.com/i6739763511628399112/?tt_from=weixin&utm_campaign=client_share&wxshare_count=1&timestamp=1609748349&app=news_article&utm_source=weixin&utm_medium=toutiao_android&use_new_style=1&req_id=20210104161909010149021155484E07B6&share_token=3c0a05b6-3915-4309-9fa8-199f9e5fb6e2&group_id=6739763511628399112

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值