linux c共享库(动态库)基础(1)

2018-06-15 12:12:25

在很多情况下,源代码文件也可以被多个程序共享。因此要降低工作量的第一步就是将这些源代码文件只编译一次,然后在需要的时候将它们链接进不同的可执行文件中。虽然这项技术能够竹省编译时间,但其缺点是在链接的时候仍然需要为所有目标文件命名。此外,大量的目标文件会散落在系统的各个日录,从而造成目录中内容的混乱。

为解决这个问题,可以将一组目标文件组织成一个被称为对象库的单元。对象库分为两种:静态的和共享的。共享库是一种更加现代化的对象库,它比静态库更具优势。

静态库

在开始讨论共享库之前首先对静态库作简短的介绍,这样读者就能够弄清楚共享库与静态库之间的差别以及共享库所具备的优势了。

静态库也被称为归档文件,它是 UNIX 系统提供的第一种库。静态库能带来下列好处。

1.可以将一组经常被用到的目标文件组织进单个库文件,这样就可以使用它来构建多个可执行程序并且在构建各个应用程序的时候无需重新编译原来的源代码文件。
2.链接命令变得更加简单了。在链接命令行中只需要指定静态库的名称即可,而无需一个个地列出目标文件了。链接器知道如何搜素静态库并将可执行程序需要的对象抽取出来。

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

#[静态库]
gcc -c mod1.c mod2.c mod3.c
ar r libdemo.a mod1.o mod2.o mod3.o
ar tv libdemo.a			#查看
ar d libdemo.a mod3.o		#删除


#使用
gcc main.c libdemo.a
gcc main.c -ldemo		#放在搜索路径可以直接使用
gcc main.c -Lmydir -ldemo	#手动指定路径


共享库(动态库)

将程序与静态库链接起来时(或没有使用静态库),得到的可执行文件会包含所有被链接进程序的目标文件的副本。这样当不同的可执行程序使用了同样的目标模块时,每个可执行程序会拥有自己的目标模块的副本。这种代码的冗余存在几个缺点。

1.存储同一个目标模块的多个副本会浪费磁盘空间,并且所浪费的空间是比较大的。
2.如果几个使用了同一模块的程序在同一时刻运行,那么每个程序会独立地在虚拟内存中保存一份目标模块的副本,从而提高系统中虚拟内存的整体使用量。
3.如果需要修改一个静态库中的一个目标模块(可能是因为安全性或需要修正 bug ) ,那么所有使用那个模块的可执行文件都必须要重新进行链接以合并这个变更。这个缺点还会导致系统管理员需要弄清楚哪些应用程序链接了这个库。

共享库就是设计用来解决这些缺点的。共享库的关键思想是目标模块的单个副本由所有需要这些模块的程序共享。目标模块不会被复制到链接过的可执行文件中,相反,当第一个需要共享库中的模块的程序启动时,库的单个副本就会在运行时被加载进内存。当后面使用同一共享库的其他程序启动时,它们会使用己经被加载进内存的库的副本。使用共享库意味着可执行程序需要的磁盘空间和虚拟内存(在运行的时候)更少了。

创建共享库

gcc -g -c -fPIC -Wall mod1.c mod2.c mod3.c
gcc -g -shared -o libfoo.so mod1.o mod2.o mod3.o

#或者也可以整合成一句话
gcc -g -fPIC -Wall mod1.c mod2.c mod3.c -shared -o libfoo.so

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

#下面都是用来检测目标文件在编译时是否使用了-fPIC
nm mod1.o | grep _GLOBAL_OFFSET_TABLE_
readelf -s mod1.o | grep _GLOBAL_OFFSET_TABLE_

objdump --all-headers libfoo.so | grep TEXTREL
readelf -d libfoo.so | grep TEXTREL


使用共享库

为了使用—个共享库就需要做两件事情,而使用静态库的程序则无需完成这两件事。

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

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

gcc -g -Wall -o prog prog.c libfoo.so

尝试执行程序一般会报一下错误

$ ./prog
./prog: error in loading shared libraries: libfoo.so: cannot
open shared object file: No such file or directory

解决这个问题系统做第二件事情:动态链接,即在运行时解析内嵌的库名。这个任务由动态连接器(也称动态链接加载器)来完成。动态链接器本身也是一个共享库,其名称为/lib64/ldxxxx,比如说我的系统如下

[root@izj6cfw9yi1iqoik31tqbgz c]# ll /lib64/ld-linux-x86-64.so.2 
lrwxrwxrwx. 1 root root 10 Oct 15  2017 /lib64/ld-linux-x86-64.so.2 -> ld-2.17.so

动态链接器会检查程序所需的共享库清单并使用一组预先定义好的规则来在文件系统上找出相关的库文件。其中一些规则指定了一组存放共享库的标准目录。如很多共享库位于/lib、/usr/lib(64位的在/lib64、/usr/lib64中)。之所以出现上面的错误消息是因为程序所需的库位于当前工作目录,而不位于动态链接器搜索的标准目录清单中。

LD_LIBRARY_PATH 环境变量

通知动态链接器一个共享库位于一个非标准目录中的一种方法是将该目录添加到LD_LIBRARY_PATH环境变量中以分号分隔的目录列表中。(在使用分号时必须将列表放在引号中以防止 shell 将分号解释了其他用途。)如果定义了LD_LIBRARY_PATH,那么动态链接器在查找标准库目录之前会先查找该环境变量列出的目录中的共享库.(稍后会介绍一个生产应用程序永远都不应该依赖于 LD_LIBRARY_PATH,但此刻通过这个变量可以方便地开始使用共享库了。)因此,可以使用下面的命令来运行程序。

LD_LIBRARY_PATH=.
./prog

共享库 soname

如果共享库拥有一个soname,那么在静态链接阶段会将 soname 嵌入到可执行文件中,而不会使用真实名称,同时后面的动态链接器在运行时也会使用这个 soname 来搜索库。引入 soname 的目的是为了提供一层间接,使得可执行程序能够在运行时使用与链接时使用的库不同的(但兼容的)共享库。

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

-Wl、-soname以及libbar.so 选项是传给连接器的指令将libfoo.so的soname设置为libbar.so

要确定一个既有共享库的soname,可以使用如下命令。

objdump -p libfoo.so | grep SONAME
#SONAME libbar.so

readelf -d libfoo.so | grep SONAME
#0x0000000e (SONAME) Library soname: [libbar.so]

这时,连接器检查到库libfoo.so 包含了soname libbar.so,于是将这个soname 嵌入到了可执行文件中。

gcc -g -Wall -o prog prog.c libfoo.so

那么下次运行的时候,动态链接器会去查找libbar.so,所以一般我们会在动态链接器搜索目录建立软连接。

ln -s libfoo.so libbar.so



共享库工具

ldd 命令显示了一个程序运行所需的共享库。

[root@izj6cfw9yi1iqoik31tqbgz c]# ldd a.out
	linux-vdso.so.1 =>  (0x00007ffcc4bf9000)
	libc.so.6 => /lib64/libc.so.6 (0x00007f23fdc49000)
	/lib64/ld-linux-x86-64.so.2 (0x0000559987ce3000)

objdump readelf命令

objdump 命令能够用来获取各类信息 - 包括反汇编的二进制机器码 - 从一个可执行文件、编译过的目标以及共享库中。它还能够用来显示这些文件中各个 ELF 节的头部信息,当这样使用 objdump 时它就类似于 readelf , readelf 能显示类似的信息,但显示格式不同。

nm 命令 nm 命令会列出目标库或可执行程序中定义的一组符号。这个命令的一种用途是找出哪些库定义了某个符号。如要找出哪个库定义了 crypt() 函数则可以像下面这样做。 

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

nm 的 -A 选项指定了在显示符号的每一行的开头处应该列出库的名称。这样做是有必要的,因为在默认情况下, nm 只列出库名一次,然后在后面会列出库中包含的所有符号,这对像上面那样进行某种过滤的例子来讲是没有用处的。此外,这里还丢弃了标准错误输出以便隐藏与 nm 命令无法识别文件格式有关的错误消息。从上面的输出中可以看出,crypt() 被定义在libcrypt 库中。

详细用法将在 linux命令系列 中讲解。


共享库命名

一般来讲,一个共享库相互连续的两个版本是相互兼容的,这意味着每个模块中的函数对外呈现出来的调用接口是一致的,并且函数的语义是等价的(即它们能取得同样的结果)。这种版本号不同但相互兼容的版本被称为共享库的次要版木。但有时候需要创建创建一个库的新主版本 - 与上一个版本不兼容的版本。同时,必须要确保使用老版木的库的程序仍然能够运行。为了满足这些版本的要求,共享库的真实名称和 soname 必须要使用一种标准的命名规范。

共享库的真实名称格式一般为 libname.so.主版本号.次版本号.补丁号

共享库的 soname 包括了相应的真实名称中的主要版本标识符,但是不包含次要版本标识符。因此 soname 的形式为 libname.so.主版本号

libdemo.so.1 -> libdemo.so.1.0.2
libdemo.so.2 -> libdemo.so.2.0.0
libreadline.so.5 -> libreadline.so.5.0

共享库名称命名总结如下图


ldconfig

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

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

ldconfig 程序通过执行两个任务来解决这些问题。
1.它搜索一组标准的目录并创建或更新一个缓存文件 /etc/ld.so.cache 使之包含在所有这些目录中的主要库版本(每个库的主要版本的最新的次要版本)列表。动态链接器在运行时解析库名称时会轮流使用这个缓存文件。为了构建这个缓存, ldconfig 会搜索在  /etc/ld.so.conf  中指定的目录,然后搜索/Iib(64) 和/usr/lib(64) 。/etc/ld.so.conf 文件由一个目录路径名(应该是绝对路径名)列表构成,其中路径名之间用换行、空格、制表符、逗号或冒号分隔。在一些发行版中,/usr/local/lib(64) 目录也位于这个列表中。(如果不在这个列表中,那么就需要手工将其添加到列表中。)

#显示/etc/ld.so.cache的当前内容
ldconfig -p

2.它检查每个库的各个主要版本的最新次要版本(即具有最大的次要版本号的版本)以找出嵌入的 soname,然后在同一目录中为每个 soname 创建(或更新)相对符号链接。

为了能够正确执行这些动作, ldconfig 要求库的名称要根据前而介绍的规范来命名(即库的真实名称包含主要和次要标识符,它们随着库的版本的更新而恰当的增长)。在默认情况下, ldconfig 会执行上面两个动作,但可以使用命令行选项来指定它执行其中一个动作:-N 选项会防止缓存的重建,-x 选项会阻 止 soname 符号链接的创建。此外,-v ( verbose ) 选项会使得 ldconfig 输出描述其所执行的动作的信息。

每当安装了一个新的库,更新或删除了一个既有库,以及/etc/ld.so.conf 中的目录列表被修改之后,都应该运行ldconfig。


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

到目前为止本章已经介绍了两种通知动态链接器共享库的位置的方式:

1.使用 LD_LIBRARY_PATH 环境变量和将共享库安装到其中一个标准库目录中(/lib(64), /usr/lib(64), /etc/ld.so.conf 中指定的目录)。

还存在第三种方式:在静态编译阶段可以在可执行文件中插入一个在运行时搜索共享库的目录列表。这种方式对于库位于一个固定的但不属于动态链接器搜索的标准位置的位置中时是非常有用的。要实现这种方式需要在创建可执行文件时使用 -rpath 链接器选项。

gcc -g -Wall -Wl,-rpath,/home/mtk/pdir -o prog prog.c libdemo.so

上面的命令将字符串 /home/mtk/pdir 复制到了可执行文件 prog 的运行时库路径( rpath )列表中,因此当运行这个程序时,动态链接器在解析共享库引用时还会搜索这个目录。如果有必要的话,可以多次指定 -rpath 选项;所有这些列出的目录会被连接成一个放到可执行文件中的有序 rpath 列表。或者,在一个 -rpath 选项中可以指定多个由分号分割开来的目录列表。在运行时,动态链接器会按照指定的目录顺序来搜索目录。

-rpath 选项的替代方案为 指定 LD_RUN_PATH 环境变量,多个可以由分号隔开。只有当没指定 -rpath 时才会用到这个。

默认情况下,利用 -rpath 选项链接器会创建 DT_RPATH标签,优先级比LD_LIBRARY_PATH高,如果额外指定 --enable-new-dtags 则会创建为DT_RUNPATH 新标签,优先级比 LD_LIBRARY_PATH 低。


运行时找出共享库步骤

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

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(64) 和 /usr/lib(64) 目录


运行时符号解析会以最先找到的那个为准。


编译时库和头文件查找

有时候我们安装第三方库时,安装到了自定义路径,此时当编译其他软件用到该库时就会找不到,此时 pkg-config 就会派上用场。

centos7 64 位默认的 pkg-config 文件路径为 /lib64/pkgconfig/,里面有很多 .pc 结尾的文件,最好每次安装了第三方库到非标准路径后,把该库自带的 .pc 文件复制到 /lib64/pkgconfig/,或者也可以通过制定环境变量 PKG_CONFIG_PATH,比如:

export PKG_CONFIG_PATH=/usr/local/xxx/lib/pkgconfig

我们以 uuid.pc 为例看下:

prefix=/usr
exec_prefix=/usr
libdir=/usr/lib64
includedir=/usr/include

Name: uuid
Description: Universally unique id library
Version: 2.23.0
Requires:
Cflags: -I${includedir}/uuid
Libs: -L${libdir} -luuid

此时我们通过如下命令就可以获取正确的编译参数

[root@centos_242 pkgconfig]# pkg-config --libs uuid
-luuid  
[root@centos_242 pkgconfig]# pkg-config --cflags uuid
-I/usr/include/uuid

Requires 和 Requires.private 为依赖的库,Requires.private 代表不会暴露给应用,避免再次被链接,也就是只被当前库所链接,而不会被最终可执行程序所链接。版本号可以通过 =、>、<、>=、<= 来指定比如:

Requires.private: glib-2.0 >= 2.16.0, gio-2.0, gnutls >= 2.12

编译时临时指定环境变量

export C_INCLUDE_PATH="/tmp/include:/root/test/include"

CFLAGS=-I/usr/include -I/path/include

LDFLAGS=-L/usr/lib -L/path/to/your/lib

LIBS = -lpthread -liconv


备注
1.编译器版本gcc4.8,运行环境centos7 64位
2.原文地址http://www.freecls.com/a/2712/5a

 

©著作权归作者所有
收藏
推荐阅读
  • linux c进程资源

    每个进程都会消耗诸如内存和CPU时间之类的系统资源,本文将介绍与资源相关的系统调用。#include &lt;sys/resource.h&gt; int getrusage(int who, st...

  • err
    linux c使用syslog记录消息

    syslog 工具提供了一个集中式日志工具,系统中的所有应用程序都可以使用这个记录日志消息。如下图syslogd 从两个不同的源接收日志消息:一个是unix domain...

  • linux c进程调度-CPU亲和力

    进程切换 CPU 时对性能会有一定的影响:如果在原来的 CPU 的高速缓冲器中存在进程的数据,那么为了将进程的一行数据加载进新 CPU 的高速缓冲器中,首先必须使这行数据失效(即在没被修改的情况下丢弃...

  • err
    linux c线程(2)-同步和线程取消

    本篇将介绍线程用来同步彼此行为的两个工具:互斥量和条件变量。我们先来看一个例子。#include &lt;sys/types.h&gt; #include &lt;std...

  • err
    linux c线程(1)-基础

    与进程类似,线程是允许应用程序并发执行多个任务的一种机制。一个进程可以包含多个线程。线程间共享同一份全局内存区域,其中包括初始化数据段,未初始化数据段,堆内存。传统uni...

  • nginx模块 ngx_http_headers_module

    ngx_http_headers_module 模块是用来增加 Expires 和 Cache-control,或者是任意的响应头。Syntax: add_header name value [alw...

  • nginx模块 ngx_http_gunzip_module、ngx_http_gzip_module、ngx_http_gzip_static_module

    ngx_http_gunzip_module 模块将文件解压缩后并在响应头加上 "Content-Encoding: gzip" 返回给客户端。为了解决客户端不支持gzip压缩。编译的时候带上 --w...

  • nginx模块 ngx_http_flv_module、ngx_http_mp4_module

    ngx_http_flv_module模块提供了对 flv 视频的伪流支持。编译的时候带上 --with-http_flv_module。它会根据指定的 start 参数来指定跳过多少字节,并在返回数...

  • nginx模块 ngx_http_fastcgi_module

    ngx_http_fastcgi_module 模块使得nginx可以与 fastcgi 服务器通信。比如目前要使得 nginx 支持 php 就得使用 fastcgi技术,在服务器上装上 nginx...

  • nginx模块 ngx_http_autoindex_module

    ngx_http_autoindex_module 模块可以将uri以 / 结尾时,列出里面的文件和目录。Syntax: autoindex on | off; Default: autoindex ...

简介
天降大任于斯人也,必先苦其心志。