共享库01-静态库和共享库的使用

共享库01-静态库和共享库的使用

1. 库文件链接

1.1 头文件与库文件

1、概要

  • 在使用C语言和其他语言进行程序设计的时候,我们需要头文件来提供对常数的定义和对系统及库函数调用的声明。
  • 库文件是一些预先编译好的函数集合,那些函数都是按照可重用原则编写的。它们通常由一组互相关联的用来完成某项常见工作的函数构成。

2、使用库的优点:

  • 1)模块化开发
  • 2)可重用性
  • 3)可维护性

3、头文件与库文件的位置

  • /usr/include及其子目录底下的include文件夹
  • /usr/local/include及其子目录底下的include文件夹
  • /usr/lib
  • /usr/local/lib
  • /lib

3.2 库的搜索路径

  • 1)从左到右搜索-I -L指定的目录。
  • 2)由环境变量指定的目录
    • 可以定义C_INCLUDE_PATH/CPP_INCLUDE_PATH(头文件搜索路径)、LIBRARY_PATH(库文件搜索路径)保存在~/.bash_profile中,有些系统也可以将这些定义存放在~/.bashrc中
  • 3)由系统指定的目录:/usr/include,/usr/lib等

2. 静态库

静态库(.a):静态库也被称为归档文件。程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库,静态库占用磁盘空间较大。

静态库实际上就是一个保存所有被添加到其中的目标文件的副本的文件。这个归档文件还记录着每个目标文件的各种特性,包括文件权限、数字用户和组 ID 以及最后修改时间。根据惯例,静态库的名称的形式为 libxxx.a

链接实际上是由一个单独的链接器程序 ld 来完成的。当使用 cc(或 gcc)命令链接一个程序时,编译器会在幕后调用 ld。

2.1 生成静态库

代码目录结构:–在附录中

编译、归档方法:

gcc -g -c mod1.c mod2.c mod3.c  #编译生成 mod1.o mod2.o mod3.o 文件
ar -cr libdemo.a mod1.o mod2.o  mod3.o  #归档生成libdemo.a 文件
#也可以直接生成:
ar -cr -o libdemo.a mod1.c mod2.c mod3.c 

注意:我们要生成的库的文件名必须形如 libxxx.a ,这样我们在链接这个库时,就可以用 -lxxx。
反过来讲,当我们告诉编译器 -lxxx时,编译器就会在指定的目录中搜索 libxxx.a 或是 libxxx.so。

ar 的命令选项:

  • c:如果需要生成新的库文件,不要警告
  • r(替换):代替库中现有的文件或者插入新的文件
  • v(verbose):输出详细信息
  • t(目录表):显示归档中的目录表。在默认情况下只会列出归档文件中目标文件的名称。添加 v 输出详细的信息。

    [lincoln@bj-develop-01 shlib]$ ar -tv libdemo.a 
    rw-r--r-- 1042/1001   3184 Aug 20 21:06 2018 mod1.o
    rw-r--r-- 1042/1001   3184 Aug 20 21:06 2018 mod2.o
    rw-r--r-- 1042/1001   3184 Aug 20 21:06 2018 mod3.o
  • d(删除):从归档文件中删除一个模块。

    [lincoln@bj-develop-01 shlib]$ ar -d libdemo.a mod3.o 
    [lincoln@bj-develop-01 shlib]$ ar -tv libdemo.a 
    rw-r--r-- 1042/1001   3184 Aug 20 21:06 2018 mod1.o
    rw-r--r-- 1042/1001   3184 Aug 20 21:06 2018 mod2.o

2.2 使用静态库

将程序与静态库链接起来存在两种方式。

  • 第一种是在链接命令中指定静态库的名称。
  • 第二种是将静态库放在链接器搜索的其中一个标准目录中( 如/usr/lib),然后使用 -l 选项指定库名( 即库的文件名去除了lib 前缀和.a 后缀)。如果库不位于链接器搜索的目录中,那么可以只用 -L 选项指定链接器应该搜索这个额外的目录。
方法 1:
gcc -g -o prog prog.c libdemo.a 
方法 2 情况2:
gcc -g -o prog prog.c -L. -ldemo

上面的命令中 -L. 告诉 gcc 搜索链接库时包含当前路径,-ldemo 告诉 gcc 生成可执行程序时要链接 libdemo.a。

3. 共享库

共享库(.so/.sa):程序在运行的时候才去链接共享库的代码,多个程序可共享使用库的代码。

共享库的关键思想是目标模块的单个副本由所有需要这些模块的程序共享。目标模块不会被复制到链接过的可执行文件中,相反,当第一个需要共享库中的模块的程序启动时,库的单个副本就会在运行时被加载进内存。当后面使用同一共享库的其他程序启动时,它们会使用已经被加载进内存的库的副本。

虽然共享库的代码是由多个进程共享的,但其中的变量却不是的。每个使用库的 进程会拥有自己的在库中定义的全局和静态变量的副本。

根据惯例,共享库的前缀为 lib,后缀为 .so(表示 shared object)。

共享库的优点:

  • 一个与共享库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码
  • 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该共享库中复制到内存中,这个过程称为动态链接(dynamic linking)
  • 共享库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份共享库被要用到该库的所有进程共用,节省了内存和磁盘空间。

通知动态链接器共享库的位置的方式:

  • 使用 LD_LIBRARY_PATH 环境变量—3.3节
  • 将共享库安装到其中一个标准库目录中(/lib、/usr/lib 或在/etc/ld.so.conf 中列出的其中一个录)。 —3.5节
  • 在静态编辑阶段可以在可执行文件中插入一个在运行时搜索共享库的目录列表。—3.6节

3.1 生成共享库文件

使用和静态库相同的代码:prog.c mod1.c mod2.c mod3.c 文件

  • 首先,生成目标文件,此时要加编译器选项-fpic
gcc -fPIC -c mod1.c mod2.c mod3.c

-fPIC 创建与地址无关的编译程序(pic,position independent code),是为了能够在多个应用程序间共享。

  • 然后,生成动态库,此时要加链接器选项-shared
gcc -shared -o libfoo.so mod1.o mod2.o mod3.o

-shared指定生成动态链接库。

  • 其实上面两个步骤可以合并为一个命令:
gcc -shared -o libfoo.so -fPIC mod1.c mod2.c mod3.c 

3.2 位置独立的代码

gcc -fPIC 选项指定编译器应该生成位置独立的代码,这会改变编译器生成执行特定操作的代码的方式,包括访问全局、静态和外部变量,访问字符串常量,以及获取函数的地址。这些变更使得代码可以在运行时被放置在任意一个虚拟地址处。这一点对于共享库来讲是必需的,因为在链接的时候是无法知道共享库代码位于内存的何处的。

可以用以下方法确认编译时是否使用了 –fPIC 选项:

  • 可以使用下面两个命令中的一个来检查目标文件符号表中是否存在名称 GLOBAL_OFFSET_TABLE。若存在,则表示使用了 –fPIC 选项
[lincoln@bj-develop-01 shlib]$ nm mod1.o | grep _GLOBAL_OFFSET_TABLE_
                 U _GLOBAL_OFFSET_TABLE_
[lincoln@bj-develop-01 shlib]$ readelf -s mod1.o | grep _GLOBAL_OFFSET_TABLE_
    10: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _GLOBAL_OFFSET_TABLE_
  • 字符串 TEXTREL 表示存在一个目标模块,其文本段中包含需要运行时重定位的引用。若存在 TEXTREL ,则表示没有使用 –fPIC 选项
[lincoln@bj-develop-01 shlib]$ objdump --all-headers libfoo.so | grep TEXTREL
[lincoln@bj-develop-01 shlib]$ readelf -d libfoo.so | grep TEXTREL

3.3 使用共享库—LD_LIBRARY_PATH方法

动态库需要做的事情(静态库不需要):

  • 由于可执行文件不再包含它所需的目标文件的副本,因此它必须要通过某种机制找出在运行时所需的共享库。这是通过在链接阶段将共享库的名称嵌入可执行件中来完成的。一个程序所依赖的所有共享库列表被称为程序的动态依赖列表
  • 动态链接在运行时必须要存在某种机制来解析嵌入的库名——即找出与在可执行文件中指定的名称对应的共享库文件——接着如果库不在内存中的话就将库加载进内存。动态链接,即在运行时解析内嵌的库名。这个任务是由动态链接器(也称为动态链接加载器运行时链接器)来完成的。动态链接器本身也是一个共享库,其名称为/lib/ld-linux.so.2。

使用方法:

  • 引用动态库编译成可执行文件(跟静态库方式一样):
1:
gcc -o prog prog.c -L. -lfoo
法2:
gcc -o prog prog.c libfoo.so
  • 此时运行会报以下错误:
[lincoln@bj-develop-01 shlib]$ ./prog 
./prog: error while loading shared libraries: libfoo.so: cannot open shared object file: No such file or directory
[lincoln@bj-develop-01 shlib]$ export LD_LIBRARY_PATH=.
[lincoln@bj-develop-01 shlib]$ ./prog 
Called mod1-x1 
Called mod2-x2 
  • 解决方法
    • 法1)拷贝.so文件到系统共享库路径下,一般指/usr/lib
    • 法2)将动态链接库的目录放到程序搜索路径中,可以将库的路径加到环境变量 LD_LIBRARY_PATH 中实现:在~/.bash_profile文件中,配置LD_LIBRARY_PATH变量export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
    • 法3)配置/etc/ld.so.conf,配置完成后调用ldconfig更新ld.so.cache

3.4 共享库的命名惯例

按照共享库的命名惯例,每个共享库有三个文件名:real name、soname和linker name。

$ ls -l  /lib | awk '{print $1,$9,$10,$11}'
...
-rwxr-xr-x  libc-2.8.90.so
lrwxrwxrwx  libcap.so.1 -> libcap.so.1.10
-rw-r--r--  libcap.so.1.10
lrwxrwxrwx  libcap.so.2 -> libcap.so.2.10
-rw-r--r--  libcap.so.2.10

主要版本和次要版本的概念:
* 次要版本:一般来讲,一个共享库相互连续的两个版本是相互兼容的,这意味着每个模块中的函数对外呈现出来的调用接口是一致的,并且函数的语义是等价的( 即它们能得 同样的结果)。这种版本号不同但相互兼容的版本被称为共享库的次要版本。
* 主要版本:共享库的每个不兼容版本是通过一个唯一的主要版本标识符来区分的,这个主要 版本标识符是共享库的真实名称的一部分。

real name

真正的库文件(而不是符号链接)的名字是real name,包含完整的共享库版本号。真实名称的格式规范为 libname.so.major-id.minor-id。例如上面的libcap.so.1.10、libcap.so.2.10。

soname

soname 的形式为 libname.so.major-id。通常,每个库的主要版本的 soname 会指向在主要版本中最新的次要版本。

soname 是一个符号链接的名字,只包含共享库的主版本号,主版本号一致即可保证库函数的接口一致,因此应用程序的.dynamic 段只记录共享库的 soname,只要 soname 一致,这个共享库就可以用。例如上面的libcap.so.1和libcap.so.2是两个主版本号不同的libcap,有些应用程序依赖于libcap.so.1,有些应用程序依赖于libcap.so.2,但对于依赖libcap.so.1的应用程序来说,真正的库文件不管是libcap.so.1.10还是libcap.so.1.11都可以用,所以使用共享库可以很方便地升级库文件而不需要重新编译应用程序,这是静态库所没有的优点。

linker name

链接器名称(linker name),将可执行文件与共享库链接起来时会用到这个名称。链接器名称是一个只包含库名同时不包含主要或次要版本标识符的符号链接,因此其形式为 libname.so

一般来讲,链接器名称与它所引用的文件位于同一个目录中,它既可以链接到真实名称,也可以连接到库的最新主要版本的 soname。通常,最好使用指向 soname 的 链接,因此对 soname 所做的变更会自动反应到链接器名称上。

总结
名称格式描述
真实名称libname.so.maj.min保存库代码的文件;每个库的 major-plus-minor 版本都存在一个真实名称
sonamelibname.so.maj库的每个主要版本都存在一个 soname;在链接时 被嵌入到可执行文件中;在运行时用来找出指向相应的( 最新的)真实名称的同名符号链接所引用的库
链接器名称libname.so指向真实名称或最新的( 更常见的做法)soname 的符号链接;只存在一个实例;允许构建版本独立的链接命令

这里写图片描述

例子:

# 创建目标文件
$ gcc -g -c -fPIC -Wall mod1.c  mod2.c mod3.c
# 创建共享库,其真实名称为 libdemo.so.1.0.1,soname 为 libdemo.so.1。
$ gcc -g -shared -Wl,-soname,libdemo.so.1 -o libdemo
# 创建符号链接
$ ln -s libdemo.so.1.0.1 libdemo.so.1
$ ln -s libdemo.so.1 libdemo.so

#文件如下:
$ ll
total 40
lrwxrwxrwx 1 lincoln dev   12 Aug 21 20:30 libdemo.so -> libdemo.so.1
lrwxrwxrwx 1 lincoln dev   16 Aug 21 20:30 libdemo.so.1 -> libdemo.so.1.0.1
-rwxr-xr-x 1 lincoln dev 9771 Aug 21 20:29 libdemo.so.1.0.1
$ gcc -g -Wall -o prog prog.c -L. -ldemo #链接命令不会用到版本号
$ export LD_LIBRARY_PATH=.  
$ ./prog 
Called mod1-x1 
Called mod2-x2 

3.5 安装和升级共享库—将共享库安装到标准库中

标准库目录包括:

  • /usr/ lib,它是大多数标准库安装的目录。
  • /lib,应该将系统启动时用到的库安装在这个目录中(因为在系统启动时可能还没有挂载 /usr/lib)。
  • /usr/local/lib,应该将非标准或实验性的库安装在这个目录中(对于 /usr/lib 是一个由多个系统共享的网络挂载但需要只在本机安装一个库的情况则 可以将库放在这个目录中)。
  • 其中一个在 /etc/ld.so.conf 中列出的目录。

ldconfig(8)命令默认执行以下两个动作

  • 它搜索一组标准的目录并创建或更新一个缓存文件 /etc/ld.so.cache 使之包含在所有这些目录中的主要库版本(每个库的主要版本的最新的次要版本)列表。动态链接器在运行时解析库名称时会轮流使用这个缓存文件。为了构建这个缓存,ldconfig 会搜索在 /etc/ld.so.conf 中指定的目录,然后搜索 /lib 和 /usr/lib。/etc/ld.so.conf 文件由一个目录路径名(应该是绝对路径名)列表 构成,其中路径名之间用换行、空格、制表符、逗号或冒号分隔。在一些发行版中,/usr/local/lib 目录也位于这个列表中。( 如果不在这个列表中,那么就需要手工将其添加到列表中。)
  • 它检查每个库的各个主要版本的最新次要版本( 即具有最大的次要版本号的本) 以找出嵌入的 soname,然后在同一目录中为每个 soname 创建(或更新)相对符号链接。注意:必须要手工更新链接器名称的符号链接。

ldconfig选项说明:

  • -N 选项会防止缓存的重建,
  • -X 选项会阻止 soname 符号链接的创建。
  • -v (verbose) 选项会使得 ldconfig 输出描述其所执行的动作的信息。
安装共享库
#注意:一般需要root权限才能移入/usr/lib中
root@10.3.1.11:/data/lincoln/test/prog# mv libdemo.so.1.0.1 libdemo.so.2.0.1 /usr/lib  
#使用ldconfig命令,自动生成soname并软连到real name
root@10.3.1.11:/data/lincoln/test/prog# ldconfig -v | grep libdemo
        libdemo.so.2 -> libdemo.so.2.0.1 (changed)
        libdemo.so.1 -> libdemo.so.1.0.1 (changed)
root@10.3.1.11:/data/lincoln/test/prog# ll /usr/lib/libdemo* | awk '{print $1,$9,$10,$11}'
    lrwxrwxrwx /usr/lib/libdemo.so.1 -> libdemo.so.1.0.1
    -rwxr-xr-x /usr/lib/libdemo.so.1.0.1  
    lrwxrwxrwx /usr/lib/libdemo.so.2 -> libdemo.so.2.0.1
    -rwxr-xr-x /usr/lib/libdemo.so.2.0.1 
root@10.3.1.11:/data/lincoln/test/prog# cd /usr/lib
#需要手动创建linker name
root@10.3.1.11:/usr/lib# ln -s libdemo.so.2 libdemo.so
root@10.3.1.11:/usr/lib# cd -
    /data/lincoln/test/prog
root@10.3.1.11:/data/lincoln/test/prog# gcc -g -Wall -o prog prog.c /usr/lib/libdemo.so
升级共享库

共享库的优点之一是当一个运行着的程序正在使用共享库的一个既有版本时也能够安装库的新主要版本或次要版本。

升级前:
这里写图片描述

(prog目录)# gcc -g -c -fPIC -Wall mod1.c mod2.c mod3.c
#生成一个新的real name so文件
(prog目录)# gcc -g -shared -Wl,-soname,libdemo.so.1 -o libdemo.so.1.0.2 mod1.o mod2.o mod3.o
#拷贝到/usr/lib目录下,调用ldconfig
(prog目录)# mv libdemo.so.1.0.2  /usr/lib
(prog目录)# ldconfig -v | grep libdemo
        libdemo.so.1 -> libdemo.so.1.0.2 (changed)
        libdemo.so.2 -> libdemo.so.2.0.1
#注意:需要手动更新链接器名称的符号链接

升级后
这里写图片描述

3.6 在目标文件中指定库搜索目录

在静态编辑阶段可以在可执行文件中插入一个在运行时搜索共享库的目录列表。要实现这种方式需要在创建可执行文件时使用 -rpath 链接器选项(在一个 rpath 选项中可以指定多个由分号分割开来的目录列表)。

gcc -g -Wall -Wl,rpath,/data/lincoln/test/prog -o prog prog.c libdemo.so 
#/data/lincoln/test/prog为libdemo.so文件所在的目录
ELF DT_ RPATH 和 DT_ RUNPATH 条目

优先级顺序:DT_RPATH 的优先级 > LD_LIBRARY_PATH 的优先级 > DT_RUNPATH 的优先级。在默认情况下,链接器会将 rpath 列表创建为 DT_RPATH 标签。为了让链接器将 rpath 列表创建为 DT_RUNPATH 条目必须要额外使用 ––enable–new–dtags( 启用新动态标签) 链接器选项。

在 rpath 中 使用$ ORIGIN

在构建链接器的时候增加了对 rpath 规范中特殊字符串 $ORIGIN( 或 等价 的${ ORIGIN}) 的支持。动态链接器将这个字符串解释成“包含应用程序的目录”。

gcc -Wl.-rpath,'$ORIGIN'/lib ...

上面的命令假设在运行时应用程序的共享库位于包含应用程序的可执行文件的目录的子目录 lib 中。这样就能向用户提供一个简单的包含应用程序及相关 的库的安装包,同时允许用户将这个包安装在任意位置并运行这个应用程序了。

3.7 运行时找出共享库的顺序、运行时符号解析

找出共享库的顺序

在解析库依赖时,动态链接器首先会检查各个依赖字符串以确定它是否包含斜(/)。如果找到了一个斜线,那么依赖字符串就会被解释成一个路径名(绝对 路径名或相对路径名),并且会使用该路径名加载库。否则动态链接器会使用下面的规则来搜索共享库。

  • 1.如果可执行文件的 DT_RPATH 运行时库路径列表(rpath)中包含目录并且不包含 DT_RUNPATH 列表,那么就搜索这些目录(按照链接程序 时指定的目录顺序)。
  • 2.如果定义了 LD_LIBRARY_PATH 环境变量,那么就会轮流搜索该变量值中以冒号分隔的各个目录。如果可执行文件是一个 set-user-ID 或 set-group-ID 程序,那么就会忽略 LD_LIBRARY_PATH 变量。这项 安全措施是为了防止用户欺骗动态链接器让其加载一个与可执行文件所需的库的名称一样的私有库。
  • 3.如果可执行文件 DT_RUNPATH 运行时库路径列表中包含目录,那么就会搜索这些目录(按照链接程序时指定的目录顺序)。
  • 4.检查 /etc/ld.so.cache 文件以确认它是否包含了与库相关的条目。
  • 5.搜索/lib 和 /usr/lib 目录( 按照这个顺序)。

静态库和动态库的优先级:

  • 在默认情况下,当链接器能够选择名称一样的共享库和静态库时(如在链接时 使用 –Lsomedir –ldemo 并且 libdemo.so 和 libdemo.a 都存在)会优先使用共享库。
  • 要强制使用库的静态版本则可以完成下列之一。
    • 在 gcc 命令行中指定静态库的路径名(包括 .a 扩展)。
    • 在 gcc 命令行中指定 -static 选项。使用 –Wl,–Bstatic 和 –Wl,–Bdynamic gcc 选项来显式地指定链接器选择共享库还是静态库。
    • 在 gcc 命令行中可以使用 -l 选项来混合这些选项。链接器会按照选项被指定时的顺序来处理这些选项。
运行时符号解析
  • 主程序中全局符号的定义覆盖库中相应的定义。
  • 如果一个全局符号在多个库中进行了定义,那么对该符号的引用会被绑定到在扫描库时找到的第一个定义,其中扫描顺序是按照这些库在静态链接命令行中列出时从左至右的顺序。
  • 如果想要确保调用共享库中对 xyz() 的调用确实调用了库中定义的相应函数,那么在构建共享库的时候就需要使用 –Bsymbolic 链接器选项。
    这里写图片描述
$ gcc -g -c -fPIC -Wall -c foo.c
$ gcc -g -shared -o libfoo.so foo.o
$ gcc -g -o prog prog.c libfoo.so
$ export LD_LIBRARY_PATH=.
$ ./prog 
main-xyz

使用 –Bsymbolic 链接器选项:

$ gcc -g -c -fPIC -Wall -c foo.c
$ gcc -g -shared -Wl,-Bsymbolic  -o libfoo.so foo.o #仅仅更改这步
$ gcc -g -o prog prog.c libfoo.so
$ export LD_LIBRARY_PATH=.
$ ./prog 
foo-xyz

附录

本文中用到的代码:

[lincoln@bj-develop-01 shlib]$ tree prog/
prog/
├── mod1.c
├── mod2.c
├── mod3.c
└── prog.c

0 directories, 4 files
//mod1.c文件
#include <stdio.h>
#include <unistd.h>

void
x1(void) {
    printf("Called mod1-x1 \n");
}
//mod2.c文件
#include <stdio.h>
#include <unistd.h>

void
x2(void) {
    printf("Called mod2-x2 \n");
}
//mod3.c文件
#include <stdio.h>
#include <unistd.h>

void
x3(void) {
    printf("Called mod3-x3 \n");
}
//prog.c文件
#include <stdio.h>
#include <stdlib.h>

void x1(void);
void x2(void);

int
main(int argc, char *argv[])
{
    x1();
    x2();
    exit(EXIT_SUCCESS);
}

参考

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值