linu/unix系统编程--共享库

107 阅读8分钟

共享库

将库函数打包为一个单元使之能够在运行时被多个进程共享。

目标库

构建程序的一种方式是简单的将每一个源文件编译为目标文件,然后将这些目标文件链接在一起组成一个可执行程序

cc -g -c prog.c mod1.c mod2.c mod3.c 
cc -g -o prog_nolib prog.o mod1.0 mod2.o mod3.o

链接器实际上是由一个单独的链接器程序ld来完成的。当使用cc(gcc)命令链接一个程序时,编译器会在幕后调用ld,在linux总是应该通过gcc简介调用链接器,因为gcc能够保证使用正确的选项来调用ld并将程序与正确的库文件链接起来

在很多情况下,源代码文件也可以被多个程序共享。因此要降低工作量的第一步就是将这些源代码文件只编译一次,然后在需要的时候将它们链接进不同的可执行文件中。(大量的目标文件会散落在系统的各个目录中,从而造成目录中内容的混乱)

为了解决这个问题,可以将一组目标文件组成成一个被称为对象库的单元。

静态库

创建和维护静态库

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

使用ar创建和维护静态库

ar options archive object-file... 
r:替换,将一个目标文件插入到归档文件中并取代同名的目标文件
cc -g -c mod1.c mod2.c mod3.c 
ar -r libdemo.a mod1.o mod2.o mod3.o 
rm mod1.o mod2.o mod3.o 

t:目录表,显示归档中的目录表
ar tv libdemo.a 
d:删除,从 归档文件中删除一个模块
ar d libdemo.a mod3.o 

使用静态库

将程序与静态库链接起来:

  1. 在链接命令中指定静态库的名称
cc -g -c prog.c 
cc -g -o prog prog.o libdemo.a 

2.将静态库放在链接器搜索的其中一个标准目录中(如/usr/lib),然后使用-l选项指定库名(库的文件名去除了lib前缀和.a后缀)

cc -g -o prog prog.o -ldemo
  1. 如果库不位于链接器搜索的目录中,那么可以只有-L选项指定链接器应该搜索这个额外的目录
cc -g -o prog prog.o -Lmylibdir -ldemo

虽然一个静态库可以包含很多目标模块,但链接器只会包含那些程序需要的模块

共享库

将静态库与程序链接起来时,得到的可执行文件会包含所有被链接进程序的目标文件的副本。这样当几个不同的可执行程序使用了同样的目标模块式,每个可执行程序都会拥有自己目标文件的副本

缺点:

  1. 存储同一个目标模块的多个副本会浪费磁盘空间
  2. 如果几个使用了同一模块的程序在同一时刻运行,那么每个程序会独立的在虚拟内存中保存一份目标模块的副本,从而提高系统中虚拟内存的整体使用量
  3. 如果需要修改一个静态库中的目标模块,所有使用这个模块的可执行文件需要重新连接

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

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

共享库在编译时必须要使用位置独立的代码,这在大多数架构上都会带来性能开销,因为他需要使用额外的一个寄存器

在运行时必须要执行符号重定位。在重定位给期间,需要将对共享库中的每个符号的引用修改成符号在虚拟内存中的实际运行时位置

创建和使用共享库

创建一个共享库

gcc -g -c -fPIC -Wall mod1.c mod2.c  mod3.c 
gcc -g -shared -o libfoo.so mod1.o mod2.o mod3.o 
根据惯例,共享库前缀为lib后缀为.so 
创建共享库的命令行选项是依赖于编译器的 
gcc -g -fPIC -Wall mod1.c mod2.c mod3.c -shared -o libfoo.so 

位置独立的代码

-fPIC指定编译器生成位置独立的代码,这会改变编译器生成执行特定操作的代码的方式,包括全局、静态和外部变量,访问字符串常量以及获取函数的地址。这些变更是的代码可以在运行时被放置在任意一个虚拟地址处

使用一个共享库

  1. 由于可执行文件不再包含它所需要的目标文件的副本,因此它必须要通过某种机制找出在运行时所需的共享库。这是通过在链接阶段将共享库的名称嵌入可执行文件中来完成的(在ELF中,库依赖性世纪路在可执行文件爱你的DT_NEEDED标签中的)。一个程序所依赖的所有共享库列表被称为程序的动态依赖列表
  2. 在运行时必须要存在某种机制来解析嵌入的库名--找出与在可执行文件中指定的名称对应的共享库文件,接着如果库不在内存中的话就将库加载进内存

将程序与共享库链接起来时会自动将库的名字嵌入可执行文件

gcc -g -Wall -o prog prog.c libfoo.so 
./prog 
error in loading sharedclibraries: libfoo.so cannot open shared object file: no such file or directory 

解决这个问题要做的第二件事:动态链接,即在运行时解析内嵌的库名,这个任务是由动态连接器来完成的。动态链接器本身是一个共享库,/lib/ld-linux.so.2,所有使用共享库的ELF可执行文件都会用到这个共享库

路径名/lib/ld-linux.so.2通常是一个指向动态链接器可执行文件的符号链接。这个文件名称为ld-version.so,其中version表示安装在系统上的glibc版本,如ld-2.11.so

动态连接器会检查程序所需的共享库清单并使用一组预先定义好的规则来在文件系统上找出相关的库文件

LD_LIBRARY_PATH环境变量:通知动态连接器一个共享库位于一个非标准目录中的一种方法是将该目录添加到LD_lIBRARY_PATH环境变量中以分号分割的目录列表中

LD_LIBRARY_PATH=. ./prog #使用shell语法在执行prog的进程中创建一个环境变量定义,这个定义高速动态连接器在.,即当前工作目录中搜索共享库

共享库 soname

使用别名创建共享库,称为soname(ELF中的DT_SONAME)

如果共享库拥有一个soname那么在静态链接阶段会将soname嵌入到可执行文件中,而不会使用真实名称,同时后面的动态链接器在运行时也会使用这个soname来搜索库

使用soname的第一步实在创建共享库时指定soname

gcc -g -c -fPIC -Wall mod1.c mod2.c mod3.c 
gcc -g -shared -W1,-soname,libbar.so -o libfoo.so mod1.o mod2.o mod3.o

确定一个既有共享库的soname,使用以下命令之一

objdump -p libfoo.so | grep SONAME 
readelf -d libfoo.so | grep SONAME

当使用soname时还需要做一件事:必须要好粗昂见一个符号链接将soname指向库的真实名称,并且必须要将这个符号链接放在动态连接器搜索的其中一个目录

ln -s libfoo.so libbar.so 

使用共享库的有用工具

ldd: 显示一个程序运行时所需的共享库

objdump和readelf

nm:命令会列出目标库或可执行问阿金中定义的一组符号,用途是找出那些库定义了一个符号

nm -A /usr/lib/lib*.so.2 > /dev/null | grep 'crypt$'

安装共享库

ldconfig解决了共享库的两个潜在问题:

  1. 共享库可以位于各种目录中,如果动态链接器需要通过搜索所有这些目录来找出一个库并加载这个共享库,那么整个过程将非常慢
  2. 当安装了新版本的库或删除了旧版本的库,那么soname符号链接就不是最新的

共享库高级特性

  • dlopen API 使得程序能够在运行时打开一个工行库,根据名字在库中搜索一个函数,然后调用这个函数
  • dlopen()打开一个共享库,返回一个供后续调用使用的句柄
  • dlsym()函数在库中搜索一个符号并返回其地址
  • dlclose()关闭之前打开的库
  • dlerror()返回一个错误消息字符串