现在的位置: 首页 > 综合 > 正文

Linux 动态库

2013年03月20日 ⁄ 综合 ⁄ 共 11737字 ⁄ 字号 评论关闭

简介:

动态链接的共享库是 GNU/Linux® 的一个重要方面。该种库允许可执行文件在运行时动态访问外部函数,从而(通过在需要时才会引入函数的方式)减少它们对内存的总体占用。本文研究了创建和使用静态库的过程,详细描述了开发它们的各种工具,并揭秘了这些库的工作方式。

什么概念呢?就是说,某个程序的在运行中要调用某个动态链接库函数的时候,操作系统首先会查看所有正在运行的程序,看在内存里是否已有此库函数的拷贝了。如果有,则让其共享那一个拷贝;只有没有才链接载入。这样的模式虽然会带来一些动态链接额外的开销,却大大的节省了系统内存资源。C的标准库就是动态链接库,也就是说系统中所有运行的程序共享着同一个C标准库的代码段。

将一些程序升级变得简单。用户只需要升级动态链接库,而无需重新编译链接其他原有的代码就可以完成整个程序的升级。

程序员在编写程序的时候,可以明确的指明什么时候或者什么情况下,链接载入哪个动态链接库函数。你可以有一个相当大的软件,但每次运行的时候,由于不同的操作需求,只有一小部分程序被载入内存。所有的函数本着有需求才调入的原则,于是大大节省了系统资源。比如现在的软件通常都能打开若干种不同类型的文件,这些读写操作通常都用动态链接库来实现。在一次运行当中,一般只有一种类型的文件将会被打开。所以直到程序知道文件的类型以后再载入相应的读写函数,而不是一开始就将所有的读写函数都载入,然后才发觉在整个程序中根本没有用到它们。

gcc编译器,只需添加上 -fPIC 标签,如:

gcc -fPIC -c file1.c
gcc -fPIC -c file2.c
gcc -shared libxxx.so file1.o file2.o

注意到最后一行,-shared 标签告诉编译器这是要建立动态链接库。这与静态链接库的建立很不一样,后者用的是 ar 命令。也注意到,动态链接库的名字形式为 “libxxx.so” 后缀名为 “.so”

库用于将相似函数打包在一个单元中。然后这些单元就可为其他开发人员所共享,并因此有了模块化编程这种说法 ,从模块中构建程序。Linux 支持两种类型的库,每一种库都有各自的优缺点。静态库包含在编译时静态绑定到一个程序的函数。动态库则不同,它是在加载应用程序时被加载的,而且它与应用程序是在运行时绑定的。图 展示了 Linux 中的库的层次结构

通常情况下,对函数库的链接是放在编译时期(compile time)完成的。所有相关的对象文件(object file)与牵涉到的函数库(library)被链接合成一个可执行文件(executable file)。程序在运行时,与函数库再无瓜葛,因为所有需要的函数已拷贝到自己门下。所以这些函数库被成为静态库(static libaray),通常文件名为“libxxx.a”的形式。
其实,我们也可以把对一些库函数的链接载入推迟到程序运行的时期(runtime)。这就是如雷贯耳的动态链接库(dynamic link library)技术。

. Linux 中的库层次结构

使用共享库的方法有两种:您既可以在运行时动态链接库,也可以动态加载库并在程序控制之下使用它们。本文对这两种方法都做了探讨。

静态库较适宜于较小的应用程序,因为它们只需要最小限度的函数。而对于需要多个库的应用程序来说,则适合使用共享库,因为它们可以减少应用程序对内存(包括运行时中的磁盘占用和内存占用)的占用。这是因为多个应用程序可以同时使用一个共享库;因此,每次只需要在内存上复制一个库。要是静态库的话,每一个运行的程序都要有一份库的副本。

GNU/Linux 提供两种处理共享库的方法(每种方法都源于 Sun Solaris)。您可以动态地将程序和共享库链接并让 Linux 在执行时加载库(如果它已经在内存中了,则无需再加载)。另外一种方法是使用一个称为动态加载的过程,这样程序可以有选择地调用库中的函数。使用动态加载过程,程序可以先加载一个特定的库(已加载则不必),然后调用该库中的某一特定函数(图 展示了这两种方法)。这是构建支持插件的应用程序的一个普遍的方法。我稍候将在本文探讨并示范该应用程序编程接口(API)。

静态链接与动态链接

用 Linux 进行动态链接

如何程序在连接时使用了共享库,就必须在运行的时候能够找到共享库的位置。linux的可执行程序在执行的时候默认是先搜索/lib/usr/lib这两个目录,然后按照/etc/ld.so.conf里面的配置搜索绝对路径。同时,Linux也提供了环境变量LD_LIBRARY_PATH供用户选择使用,用户可以通过设定它来查找除默认路径之外的其他路径,如查找/work/lib路径,你可以在/etc/rc.d/rc.local或其他系统启动后即可执行到的脚本添加如下语句:LD_LIBRARY_PATH =/work/lib:$(LD_LIBRARY_PATH)。并且LD_LIBRARY_PATH路径优先于系统默认路径之前查找(详细参考《使用LD_LIBRARY_PATH》)。

不过LD_LIBRARY_PATH的设定作用是全局的,过多的使用可能会影响到其他应用程序的运行,所以多用在调试。(LD_LIBRARY_PATH的缺陷和使用准则,可以参考《Why LD_LIBRARY_PATH is bad》 )。通常情况下推荐还是使用gcc-R-rpath选项来在编译时就指定库的查找路径,并且该库的路径信息保存在可执行文件中,运行时它会直接到该路径查找库,避免了使用LD_LIBRARY_PATH环境变量查找。

现在,让我们深入探讨一下使用 Linux 中的动态链接的共享库的过程。当用户启动一个应用程序时,它们正在调用一个可执行和链接格式(Executable and Linking FormatELF)映像。内核首先将 ELF 映像加载到用户空间虚拟内存中。然后内核会注意到一个称为 .interp 的 ELF 部分,它指明了将要被使用的动态链接器(/lib/ld-linux.so),如清单 所示。这与 UNIX® 中的脚本文件的解释器定义(#!/bin/sh)很相似:只是用在了不同的上下文中。

3库的链接时路径和运行时路径

现代连接器在处理动态库时将链接时路径(Link-time path)和运行时路径(Run-time path)分开,用户可以通过-L指定连接时库的路径,通过-R(或-rpath)指定程序运行时库的路径,大大提高了库应用的灵活性。比如我们做嵌入式移植时#arm-linux-gcc $(CFLAGS) –o target –L/work/lib/zlib/ -llibz-1.2.3 (work/lib/zlib下是交叉编译好的zlib),将target编译好后我们只要把zlib库拷贝到开发板的系统默认路径下即可。或者通过-rpath(或-R )、LD_LIBRARY_PATH指定查找路径。

清单 1. 使用 readelf 来显示程序标题

mtj@camus:~/dl$ readelf -l dl

Elf file type is EXEC (Executable file)

Entry point 0x8048618

There are 7 program headers, starting at offset 52

Program Headers:

  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align

  PHDR           0x000034 0x08048034 0x08048034 0x000e0 0x000e0 R E 0x4

  INTERP         0x000114 0x08048114 0x08048114 0x00013 0x00013 R   0x1

  [Requesting program interpreter: /lib/ld-linux.so.2]

  LOAD           0x000000 0x08048000 0x08048000 0x00958 0x00958 R E 0x1000

  LOAD           0x000958 0x08049958 0x08049958 0x00120 0x00128 RW  0x1000

  DYNAMIC        0x00096c 0x0804996c 0x0804996c 0x000d0 0x000d0 RW  0x4

  NOTE           0x000128 0x08048128 0x08048128 0x00020 0x00020 R   0x4

  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x4..

mtj@camus:~dl$

注意,ld-linux.so 本身就是一个 ELF 共享库,但它是静态编译的并且不具备共享库依赖项。当需要动态链接时,内核会引导动态链接(ELF 解释器),该链接首先会初始化自身,然后加载指定的共享对象(已加载则不必)。接着它会执行必要的再定位,包括目标共享对象所使用的共享对象。LD_LIBRARY_PATH 环境变量定义查找可用共享对象的位置。定义完成后,控制权会被传回到初始程序以开始执行。

再定位是通过一个称为 Global Offset TableGOT)和 Procedure Linkage TablePLT)的间接机制来处理的。这些表格提供了 ld-linux.so 在再定位过程中加载的外部函数和数据的地址。这意味着无需改动需要间接机制(即,使用这些表格)的代码:只需要调整这些表格。一旦进行加载,或者只要需要给定的函数,就可以发生再定位(稍候在 用 Linux 进行动态加载 小节中会看到更多的差别)。

再定位完成后,动态链接器就会允许任何加载的共享程序来执行可选的初始化代码。该函数允许库来初始化内部数据并备之待用。这个代码是在上述 ELF 映像的 .init 部分中定义的。在卸载库时,它还可以调用一个终止函数(定义为映像的 .fini 部分)。当初始化函数被调用时,动态链接器会把控制权转让给加载的原始映像。

用 Linux 进行动态加载

Linux 并不会自动为给定程序加载和链接库,而是与应用程序本身共享该控制权。这个过程就称为动态加载。使用动态加载,应用程序能够先指定要加载的库,然后将该库作为一个可执行文件来使用(即调用其中的函数)。但是正如您在前面所了解到的,用于动态加载的共享库与标准共享库(ELF 共享对象)无异。事实上,ld-linux 动态链接器作为 ELF 加载器和解释器,仍然会参与到这个过程中。

动态加载(Dynamic LoadingDLAPI 就是为了动态加载而存在的,它允许共享库对用户空间程序可用。尽管非常小,但是这个 API 提供了所有需要的东西,而且很多困难的工作是在后台完成的。表 展示了这个完整的 API

表 1. Dl API

函数

描述

dlopen

使对象文件可被程序访问

dlsym

获取执行了 dlopen 函数的对象文件中的符号的地址

dlerror

返回上一次出现错误的字符串错误

dlclose

关闭目标文件

该过程首先是调用 dlopen,提供要访问的文件对象和模式。调用 dlopen 的结果是稍候要使用的对象的句柄。mode 参数通知动态链接器何时执行再定位。有两个可能的值。第一个是 RTLD_NOW,它表明动态链接器将会在调用 dlopen 时完成所有必要的再定位。第二个可选的模式是 RTLD_LAZY,它只在需要时执行再定位。这是通过在内部使用动态链接器重定向所有尚未再定位的请求来完成的。这样,动态链接器就能够在请求时知晓何时发生了新的引用,而且再定位可以正常进行。后面的调用无需重复再定位过程。

还可以选择另外两种模式,它们可以按位 OR 到 mode 参数中。RTLD_LOCAL 表明其他任何对象都无法使加载的共享对象的符号用于再定位过程。如果这正是您想要的的话(例如,为了让共享的对象能够调用原始进程映像中的符号),那就使用 RTLD_GLOBAL 吧。

dlopen 函数还会自动解析共享库中的依赖项。这样,如果您打开了一个依赖于其他共享库的对象,它就会自动加载它们。函数返回一个句柄,该句柄用于后续的 API 调用。dlopen 的原型为:

#include <dlfcn.h>

void *dlopen( const char *file, int mode );

有了 ELF 对象的句柄,就可以通过调用 dlsym 来识别这个对象内的符号的地址了。该函数采用一个符号名称,如对象内的一个函数的名称。返回值为对象符号的解析地址:

void *dlsym( void *restrict handle, const char *restrict name );

如果调用该 API 时发生了错误,可以使用 dlerror 函数返回一个表示此错误的人类可读的字符串。该函数没有参数,它会在发生前面的错误时返回一个字符串,在没有错误发生时返回 NULL

char *dlerror();

最后,如果无需再调用共享对象的话,应用程序可以调用 dlclose 来通知操作系统不再需要句柄和对象引用了。它完全是按引用来计数的,所以同一个共享对象的多个用户相互间不会发生冲突(只要还有一个用户在使用它,它就会待在内存中)。任何通过已关闭的对象的 dlsym 解析的符号都将不再可用。

char *dlclose( void *handle );

动态加载示例

了解了 API 之后,下面让我们来看一看 DL API 的例子。在这个应用程序中,您主要实现了一个 shell,它允许操作员来指定库、函数和参数。换句话说,也就是用户能够指定一个库并调用该库(先前未链接于该应用程序的)内的任意一个函数。首先使用 DL API 来解析该库中的函数,然后使用用户定义的参数(用来发送结果)来调用它。清单 展示了完整的应用程序。
清单 2. 使用 DL API 的 Shell

#include <stdio.h>

#include <dlfcn.h>

#include <string.h>

#define MAX_STRING      80

void invoke_method( char *lib, char *method, float argument )

{ void *dl_handle;

  float (*func)(float);

  char *error;

/* Open the shared object */

  dl_handle = dlopen( lib, RTLD_LAZY );

  if (!dl_handle) {

    printf( "!!! %s\n", dlerror() );

    return;

  }

/* Resolve the symbol (method) from the object */

  func = dlsym( dl_handle, method );

  error = dlerror();

  if (error != NULL) {

    printf( "!!! %s\n", error );

    return;

  }

 /* Call the resolved method and print the result */

  printf("  %f\n", (*func)(argument) );

  /* Close the object */

  dlclose( dl_handle );

  return;

}

int main( int argc, char *argv[] )

{

  char line[MAX_STRING+1];

  char lib[MAX_STRING+1];

  char method[MAX_STRING+1];

  float argument;

  while (1) {

    printf("> ");

    line[0]=0;

    fgets( line, MAX_STRING, stdin);

    if (!strncmp(line, "bye", 3)) break;

    sscanf( line, "%s %s %f", lib, method, &argument);

    invoke_method( lib, method, argument );

  }

}

要构建这个应用程序,需要通过 GNU Compiler CollectionGCC)使用如下的编译行。选项 -rdynamic 用来通知链接器将所有符号添加到动态符号表中(目的是能够通过使用 dlopen 来实现向后跟踪)。-ldl 表明一定要将 dllib 链接于该程序。

gcc -rdynamic -o dl dl.c -ldl

再回到 清单 2main 函数仅充当解释器,解析来自输入行的三个参数(库名、函数名和浮点参数)。如果出现 bye 的话,应用程序就会退出。否则的话,这三个参数就会传递给使用 DL API 的 invoke_method 函数。

首先调用 dlopen 来访问目标文件。如果返回 NULL 句柄,表示无法找到对象,过程结束。否则的话,将会得到对象的一个句柄,可以进一步询问对象。然后使用 dlsym API 函数,尝试解析新打开的对象文件中的符号。您将会得到一个有效的指向该符号的指针,或者是得到一个 NULL 并返回一个错误。

在 ELF 对象中解析了符号后,下一步就只需要调用函数。要注意一下这个代码和前面讨论的动态链接的差别。在这个例子中,您强行将目标文件中的符号地址用作函数指针,然后调用它。而在前面的例子是将对象名作为函数,由动态链接器来确保符号指向正确的位置。虽然动态链接器能够为您做所有麻烦的工作,但这个方法会让您构建出极其动态的应用程序,它们可以再运行时被扩展。

调用 ELF 对象中的目标函数后,通过调用 dlclose 来关闭对它的访问。

清单 展示了一个如何使用这个测试程序的例子。在这个例子中,首先编译程序而后执行它。接着调用了 math 库(libm.so)中的几个函数。完成演示后,程序现在能够用动态加载来调用共享对象(库)中的任意函数了。这是一个很强大的功能,通过它还能够给程序扩充新的功能。

清单 3. 使用简单的程序来调用库函数

mtj@camus:~/dl$ gcc -rdynamic -o dl dl.c -ldl

mtj@camus:~/dl$ ./dl

libm.so cosf 0.0

  1.000000

libm.so sinf 0.0

  0.000000

libm.so tanf 1.0

  1.557408

bye

mtj@camus:~/dl$

工具

Linux 提供了很多种查看和解析 ELF 对象(包括共享库)的工具。其中最有用的一个当属 ldd 命令,您可以使用它来发送共享库依赖项。例如,在 dl 应用程序上使用 ldd 命令会显示如下内容:

mtj@camus:~/dl$ ldd dl

        linux-gate.so.1 =>  (0xffffe000)

        libdl.so.2 => /lib/tls/i686/cmov/libdl.so.2 (0xb7fdb000)

        libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0xb7eac000)

        /lib/ld-linux.so.2 (0xb7fe7000)

mtj@camus:~/dl$

ldd 所告诉您的是:该 ELF 映像依赖于 linux-gate.so(一个特殊的共享对象,它处理系统调用,它在文件系统中无关联文件)、libdl.soDL API)、GNU C 库(libc.so)以及 Linux 动态加载器(因为它里面有共享库依赖项)。

readelf 命令是一个有很多特性的实用程序,它让您能够解析和读取 ELF 对象。readelf 有一个有趣的用途,就是用来识别对象内可再定位的项。对于我们这个简单的程序来说(清单 2 展示的程序),您可以看到需要再定位的符号为:

mtj@camus:~/dl$ readelf -r dl

Relocation section '.rel.dyn' at offset 0x520 contains 2 entries:

 Offset     Info    Type            Sym.Value  Sym. Name

08049a3c  00001806 R_386_GLOB_DAT    00000000   __gmon_start__

08049a78  00001405 R_386_COPY        08049a78   stdin

Relocation section '.rel.plt' at offset 0x530 contains 8 entries:

 Offset     Info    Type            Sym.Value  Sym. Name

08049a4c  00000207 R_386_JUMP_SLOT   00000000   dlsym

08049a50  00000607 R_386_JUMP_SLOT   00000000   fgets

08049a54  00000b07 R_386_JUMP_SLOT   00000000   dlerror

08049a58  00000c07 R_386_JUMP_SLOT   00000000   __libc_start_main

08049a5c  00000e07 R_386_JUMP_SLOT   00000000   printf

08049a60  00001007 R_386_JUMP_SLOT   00000000   dlclose

08049a64  00001107 R_386_JUMP_SLOT   00000000   sscanf

08049a68  00001907 R_386_JUMP_SLOT   00000000   dlopen

mtj@camus:~/dl$

从这个列表中,您可以看到各种各样的需要再定位(到 libc.so)的 库调用,包括对 DL APIlibdl.so)的调用。函数 __libc_start_main 是一个 库函数,它优先于程序的 main 函数(一个提供必要初始化的 shell)而被调用。

其他操作对象文件的实用程序包括:objdump,它展示了关于对象文件的信息;nm,它列出来自对象文件(包括调试信息)的符号。还可以将 EFL 程序作为参数,直接调用 Linux 动态链接器,从而手动启动映像:

mtj@camus:~/dl$ /lib/ld-linux.so.2 ./dl

libm.so expf 0.0

  1.000000

>

另外,可以使用 ld-linux.so 的 --list 选项来罗列 ELF 映像的依赖项(ldd 命令也如此)。切记,它仅仅是一个用户空间程序,是由内核在需要时引导的。

动态链接库的创建

gcc编译器,只需添加上 -fPIC 标签,如:
gcc -fPIC -c file1.c
gcc -fPIC -c file2.c
gcc -shared libxxx.so file1.o file2.o
注意到最后一行,-shared 标签告诉编译器这是要建立动态链接库。这与静态链接库的建立很不一样,后者用的是 ar 命令。也注意到,动态链接库的名字形式为 “libxxx.so” 后缀名为 “.so”
使用动态链接库,首先需要在编译期间让编译器检查一些语法与定义。
这与静态库的实用基本一样,用的是 -Lpath 和 -lxxx 标签。如:
gcc file1.o file2.o -Lpath -lxxx -o program.exe
编译器会先在path文件夹下搜索libxxx.so文件,如果没有找到,继续搜索libxxx.a(静态库)。在程序运行期间,也需要告诉系统去哪里找你的动态链接库文件。在UNIX下是通过定义名为 LD_LIBRARY_PATH 的环境变量来实现的。只需将path赋值给此变量即可。csh 命令为:
setenv LD_LIBRARY_PATH your/full/path/to/dll
一切安排妥当后,你可以用 ldd 命令检查是否连接正常。
ldd program.exe
动态链接库*.so的编译与使用- -
动态库*.solinux下用cc++编程时经常会碰到,最近在网站找了几篇文章介绍动态库的编译和链接,总算搞懂了这个之前一直不太了解得东东,这里做个笔记,也为其它正为动态库链接库而苦恼的兄弟们提供一点帮助。
1、动态库的编译
下面通过一个例子来介绍如何生成一个动态库。这里有一个头文件:so_test.h,三个.c文件:test_a.ctest_b.ctest_c.c,我们将这几个文件编译成一个动态库:libtest.so
so_test.h
#include
#include
void test_a();
void test_b();
void test_c();
test_a.c
#include "so_test.h"
void test_a()
{
printf("this is in test_a...\n");
}
test_b.c
#include "so_test.h"
void test_b()
{
printf("this is in test_b...\n");
}
test_a.c
#include "so_test.h"
void test_c()
{
printf("this is in test_c...\n");
}
将这几个文件编译成一个动态库:libtest.so
$ gcc test_a.c test_b.c test_c.c -fPIC -shared -o libtest.so

2、动态库的链接
1、中,我们已经成功生成了一个自己的动态链接库libtest.so,下面我们通过一个程序来调用这个库里的函数。程序的源文件为:test.c
test.c

#include "so_test.h"
int main()
{
test_a();
test_b();
test_c();
return 0;
}
test.c与动态库libtest.so链接生成执行文件test
$ gcc test.c -L. -ltest -o test
测试是否动态连接,如果列出libtest.so,那么应该是连接正常了
$ ldd test
执行test,可以看到它是如何调用动态库中的函数的。
3、编译参数解析
最主要的是GCC命令行的一个选项:
-shared 该选项指定生成动态连接库(让连接器生成T类型的导出符号表,有时候也生成弱连接W类型的导出符号),不用该标志外部程序无法连接。相当于一个可执行文件

l -fPIC:表示编译为位置独立的代码,不用此选项的话编译后的代码是位置相关的所以动态载入时是通过代码拷贝的方式来满足不同进程的需要,而不能达到真正代码段共享的目的。l -L.:表示要连接的库在当前目录中
l -ltest:编译器查找动态连接库时有隐含的命名规则,即在给出的名字前面加上lib,后面加上.so来确定库的名称
l LD_LIBRARY_PATH:这个环境变量指示动态连接器可以装载动态库的路径。
当然如果有root权限的话,可以修改/etc/ld.so.conf文件,然后调用 /sbin/ldconfig来达到同样的目的,不过如果没有root权限,那么只能采用输出LD_LIBRARY_PATH的方法了
4、注意
调用动态库的时候有几个问题会经常碰到,有时,明明已经将库的头文件所在目录 通过 “-I” include进来了,库所在文件通过“-L”参数引导,并指定了“-l”的库名,但通过ldd命令察看时,就是死活找不到你指定链接的so文件,这时你要作的就是通过修改LD_LIBRARY_PATH或者/etc/ld.so.conf文件来指定动态库的目录。通常这样做就可以解决库无法链接的问题了。

抱歉!评论已关闭.