【Linux编程基础】构建Linux 库文件

时间:2021-10-31 13:14:58

作者:gnuhpc
出处:http://www.cnblogs.com/gnuhpc/

实验环境:Ubuntu Linux 10.04 32bit


1.库文件简介

库文件是一个包含了编译后代码、数据的文件,用于与程序其他代码连编,它可以使得程序模块化、编译速度更快,并且易于更新。库文件分为三种(实质为两种,在随后两句话有解释):静态库(在程序之前就已经装载进其中了,也就是说编译的时候就将库中需要的代码拷贝至可执行文件)、共享库(在程序启动之时加载进去,也就是说可执行文件中只包含一个它需要的函数表,具体实现的完整机器码需要从外界在启动时加载,这些机器码在运行前从共享库拷贝到内存中——这个过程称为动态链接)、动态加载库(dynamically loaded,DL)(在程序运行中任何时候都可以被加载进程序中使用,事实上DL并非是一个完全不同的库类型,共享库可以用作DL而被动态加载(静态库在Linux貌似无法用dlopen加载)。注意有些人使用dynamically linked libraries (DLLs)来指代共享库,有些人使用DLL这个词来形容任何可以被用作DL的库文件,这个请区分对待。

在具体使用中,我们应该多使用共享库,这使得用户可以独立于使用该库文件的程序而更新库。DL的确非常有用,但有时候我们可能并不需要那些灵活性,而对于静态库,由于更新起来实在费劲,我们一般不使用。

2.静态库的建立

静态库就是一堆普通的目标文件(object file),习惯上静态库以.a为后缀,这是使用ar命令生成的。静态库允许用户不用重新编译代码就可以链接程序,以节省重新编译的时间,其实这个时间已经在强大的机器配置和快速的编译器中显得微不足道了,这个常常用来提供程序而不是源代码。速度上,静态ELF(Executable and Linking Format)库文件比共享库或者动态加载库快1%-5%,但实际上常常因为其他因素而并不一定快。

我们写主文件prog.c:

 1: #include 
 2: void ctest1(int *);
 3: void ctest2(int *);
 4: 
 5: int main()
 6: {
 7: int x;
 8: ctest1(&x);
 9: printf("Valx=%d/n",x);
 10: 
 11: return 0;
 12: }
 13: 

然后写这两个函数的实现:

ctest1.c

 1: void ctest1(int *i)
 2: {
 3: *i=5;
 4: } 

ctest2.c

 1: void ctest2(int *i)
 2: {
 3: *i=100;
 4: }

我们首先编译这两个函数实现的源文件:

gnuhpc@gnuhpc-desktop:~/MyCode/lib/statics$ gcc -Wall -c ctest1.c ctest2.c
gnuhpc@gnuhpc-desktop:~/MyCode/lib/statics$ ls
ctest1.c  ctest1.o  ctest2.c  ctest2.o  prog.c

然后创建静态库libctest.a:

gnuhpc@gnuhpc-desktop:~/MyCode/lib/statics$ ar -cvq libctest.a ctest1.o ctest2.o

a - ctest1.o
a - ctest2.o

我们查看一下这个库中的文件:

gnuhpc@gnuhpc-desktop:~/MyCode/lib/statics$ ar -t libctest.a
ctest1.o
ctest2.o

此时我们可以编译我们的程序了,一般都会在/usr/local/lib/或者/usr/lib/中寻找库(按顺序),所以此处使用-L选项指定除了标准库位置外的指定目录(本例为当前目录)也要找,另外注意-l选项,后边的参数是去掉lib和.a的部分,放在要编译的文件名之后,否则会报错,另外使用多个库时要注意依赖关系,库A使用库B的函数,那么在指定库时,库A 就要出现在库A的前边(这是个好习惯,虽然有时很麻烦,而且有的编译器也会自动查找所有的库而使这个考虑显得多此一举)。当然,你还可以不用-l选项而直接将libctest.a和源文件放在一起编译:

gnuhpc@gnuhpc-desktop:~/MyCode/lib/statics$ gcc -o test prog.c -L./ –lctest
gnuhpc@gnuhpc-desktop:~/MyCode/lib/statics$ ls
ctest1.c  ctest1.o  ctest2.c  ctest2.o  libctest.a  prog.c  test
gnuhpc@gnuhpc-desktop:~/MyCode/lib/statics$ ./test
Valx=5

3.共享库的建立

共享库是在程序启动时加载的库文件。当共享库加载完毕后所有启动的起来的程序都将使用新的共享库。因此,共享库的方式既减少了可执行文件的大小,又减少了其内存消耗(多个程序共用内存中一段共享库代码时)。另外,它又提供了不需要重新编译即可升级程序的方法。由于这些好处,gcc的-l选项一般都会先检查同名的.so文件后再试图连接.a静态库文件。(除非你指定了‘-static’选项强制使用静态库)。

 

在创建共享库之前,还需要了解一些知识:

  • 命名规则:
    • 每一个共享库都有一个soname,一般都形如libname.so.versionNumber,其中versionNumber每当接口发生改变时都要增加,一个完全的soname的前缀应该是它所在目录,在一个实际系统中,一个完整的soname只是共享库文件的real name的符号链接。程序运行时在内部列出所需的共享库时使用的就是soname。
    • 每一个共享库也有一个real name,这是包含实际代码的文件名,real name使用soname为前缀,并且在后边添加一些信息,一般都形如soname.MinorNumber.ReleaseNumber。 最后的releaseNumber可有可无。这个是生成共享库时实际文件的名称。
    • 同时,在编译器要求使用一个共享库时使用的名字称为linker name,一般都是去掉版本号的soname,用于gcc中-lname这样的选项的编译。
    • 这几个名字的关系:你在创建实际库文件中指定libreadline.so.3.0为real name ,并且使用符号链接创建soname ->libreadline.so.3和linker name-> /usr/lib/libreadline.so。
  • 放置位置:
    • GNU标准推荐将所有默认的库安装在/usr/local/lib,这指的是开发者源代码默认的位置。
    • FHS指出大多数的库文件应该放在/usr/lib,而启动所需的库则应该放在/lib中,而非系统库应该放在/usr/local/lib。这指的是发行版默认的位置,这两个标准并没有矛盾。

共享库的主要有三个步骤:

  • 创建目标代码。
  • 创建库。
  • 使用符号链接创建默认版本的共享库(可选)。

现在我们举个例子来说明,首先我们编译源代码,使用-fPIC选项生成共享库所需的位置独立代码(position-independent code (PIC)):

gnuhpc@gnuhpc-desktop:~/MyCode/lib/shared$ gcc -Wall -fPIC -c *.c
gnuhpc@gnuhpc-desktop:~/MyCode/lib/shared$ ls
ctest1.c  ctest1.o  ctest2.c  ctest2.o  prog.c  prog.o

然后我们创建库文件:

gnuhpc@gnuhpc-desktop:~/MyCode/lib/shared$ gcc -shared -Wl,-soname,libctest.so.1 -o libctest.so.1.0   *.o
gnuhpc@gnuhpc-desktop:~/MyCode/lib/shared$ ls
ctest1.c  ctest1.o  ctest2.c  ctest2.o  libctest.so.1.0  prog.c  prog.o

-shared选项指明生成共享目标文件,-W1(注意是小写L而不是一)指明传入链接器的参数,在此我们设定了该库的soname为libctest.so.1,-o则指明了生成的目标库文件为libctest.so.1.0(这个就是real name)。

最后创建所需的符号链接:

gnuhpc@gnuhpc-desktop:~/MyCode/lib/shared$ sudo mv libctest.so.1.0 /usr/local/lib/libctest.so.1.0
gnuhpc@gnuhpc-desktop:~/MyCode/lib/shared$ sudo ln -sf /usr/local/lib/libctest.so.1.0 /usr/local/lib/libctest.so.1
gnuhpc@gnuhpc-desktop:~/MyCode/lib/shared$ sudo ln -sf /usr/local/lib/libctest.so.1.0 /usr/local/lib/libctest.so

创建的libctest.so就是上面所谓linker name,用于编译时-lctest选项。

创建的libctest.so.1就是soname,我们在上边说过程序在运行时需要这个名字的符号链接。

此时我们的共享库就建好了,接着我们编译程序:

gnuhpc@gnuhpc-desktop:~/MyCode/lib/shared$ gcc -Wall -L/usr/local/lib prog.c -lctest -o prog
gnuhpc@gnuhpc-desktop:~/MyCode/lib/shared$ ls
ctest1.c  ctest1.o  ctest2.c  ctest2.o  prog  prog.c  prog.o

我们编译完毕,该库并不会包含在可执行文件中,只有在执行时来会动态加载进来。我们可以通过ldd列出一个可执行程序所有的依赖,在我的系统中还找不到/usr/local/bin的路径:

gnuhpc@gnuhpc-desktop:~/MyCode/lib/shared$ ldd prog
    linux-gate.so.1 =>  (0x00a5c000)
    libctest.so.1 => not found
    libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0x00a6f000)
    /lib/ld-linux.so.2 (0x00451000)

此时,运行会报找不到库的错误:

gnuhpc@gnuhpc-desktop:~/MyCode/lib/shared$ ./prog
./prog: error while loading shared libraries: libctest.so.1: cannot open shared object file: No such file or directory

我们可以将所需库的路径加入到系统路径中,有三种方法可以完成:

  • A.在/etc/ld.so.conf中加入所在路径,然后执行ldconfig配置链接器运行时绑定配置。你也可以创建一个文件,将路径写入,然后使用ldconfig –f filename将配置写入。
  • B.修改LD_LIBRARY_PATH环境变量(Linux下,AIX下为LIBPATH),在其中添加路径。若你直接在.bashrc(或者.bash_profile)文件中配置则重启后不失效,否则在shell中设置重启后失效。可以使用LD_LIBRARY_PATH=NEWDIRS:$LD_LIBRARY_PATH来扩展这个环境变量,在保留前边设置的情况下添加目录。全局永久生效请在‘/etc/profile’中或者‘/etc/ld.so.conf’中添加。

我们使用A方法中的-f选项:

gnuhpc@gnuhpc-desktop:~/MyCode/lib/shared$ vi libctest.conf
gnuhpc@gnuhpc-desktop:~/MyCode/lib/shared$ sudo ldconfig -f libctest.conf
gnuhpc@gnuhpc-desktop:~/MyCode/lib/shared$ ./prog
Valx=5
gnuhpc@gnuhpc-desktop:~/MyCode/lib/shared$ ldd prog
    linux-gate.so.1 =>  (0x00f6f000)
    libctest.so.1 => /usr/local/lib/libctest.so.1 (0x005d9000)
    libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0x00718000)
    /lib/ld-linux.so.2 (0x001e6000)

其中libctest.conf中写入路径:/usr/local/lib。程序运行正常。

4.动态加载库的使用

动态加载库是在非程序启动时动态加载进入程序的库,这对于实现插件或动态模块有很大的帮助。在Linux中,动态加载库的形式并不特殊,它使用上述两种程序库,使用提供的API在程序运行时动态加载。注意,在不同平台上动态加载库的API并不相同,所以可能会有移植问题出现。

我们可以通过nm命令先查看一下我们创建的库里面有哪些symbol(可以理解为函数方法)供我们使用:

gnuhpc@gnuhpc-desktop:~/MyCode/lib$ nm /usr/local/lib/libctest.so
00001f18 a _DYNAMIC
00001ff4 a _GLOBAL_OFFSET_TABLE_
         w _Jv_RegisterClasses
00001f08 d __CTOR_END__
00001f04 d __CTOR_LIST__
00001f10 d __DTOR_END__
00001f0c d __DTOR_LIST__
000005a0 r __FRAME_END__
00001f14 d __JCR_END__
00001f14 d __JCR_LIST__
00002014 A __bss_start
         w __cxa_finalize@@GLIBC_2.1.3
00000540 t __do_global_ctors_aux
00000420 t __do_global_dtors_aux
00002010 d __dso_handle
         w __gmon_start__
000004d7 t __i686.get_pc_thunk.bx
00002014 A _edata
0000201c A _end
00000578 T _fini
000003a0 T _init
00002014 b completed.7021
000004dc T ctest1
000004ec T ctest2
00002018 b dtor_idx.7023
000004a0 t frame_dummy
000004fc T main
         U printf@@GLIBC_2.0

这个命令对静态库和共享库都支持,第二列为symbol类型,小写字母表示符号是本地的,大写字母表示符号是全局(外部)的,几个常见的字母含义如下:T为代码段普通定义,D为已初始化数据段,B为未初始化数据段,U为未定义(用到该符号但是没有在该库中定义)。

我们创建ctest.h:

 1: #ifndef CTEST_H
 2: #define CTEST_H
 3: 
 4: #ifdef __cplusplus
 5: extern "C" {
 6: #endif
 7: 
 8: void ctest1(int *);
 9: void ctest2(int *);
 10: 
 11: #ifdef __cplusplus
 12: }
 13: #endif
 14: 
 15: #endif

这里使用extern C是为了使得该库既可以用于C语言又可以用于C++。

我们动态加载库进来:progdl.c

 1: #include 
 2: #include 
 3: #include "ctest.h"
 4: 
 5: int main(int argc, char **argv) 
 6: {
 7: void *lib_handle;
 8: double (*fn)(int *);
 9: int x;
 10: char *error;
 11: 
 12: lib_handle = dlopen("/usr/local/lib/libctest.so", RTLD_LAZY);
 13: if (!lib_handle) 
 14: {
 15: fprintf(stderr, "%s/n", dlerror());
 16: exit(1);
 17: }
 18: 
 19: fn = dlsym(lib_handle, "ctest1");
 20: if ((error = dlerror()) != NULL) 
 21: {
 22: fprintf(stderr, "%s/n", error);
 23: exit(1);
 24: }
 25: 
 26: (*fn)(&x);
 27: printf("Valx=%d/n",x);
 28: 
 29: dlclose(lib_handle);
 30: return 0;
 31: }

里面的方法解释如下:

  • void * dlopen(const char *filename, int flag); 
    若filename为绝对路径,那么dlopen就会试图打开它而不搜索相关路径,否则就现在环境变量LD_LIBRARY_PATH处搜索,然后在/etc/ld.so.cache以及/lib和/usr/lib搜索。flag我们只解释两个常用的选项:若为RTLD_LAZY则表示在动态库执行时解决未定义符号问题,而RTLD_NOW则表示在dlopen返回前解决未定义符号问题。当你调试时你应该用RTLD_NOW,这个时候若存在未解决的引用程序还可以继续进行。另外,RTLD_NOW选项可能会使打开库的这个操作稍微慢一点,但是以后寻找函数时就会快一点。注意,若程序库相互依赖则应该按依赖顺序依次载入,比如X依赖Y,那么要先载入Y然后再载入X。返回的是一个句柄,若失败则返回null.
  • char *dlerror(void);
    报告任何上一次对加载库操作的错误。两次调用期间若有操作错误则第二次会报告, 否则第二次则返回null——它报告完错误就等待下一个错误的发生,上一次错误的情况一旦报告就不再提及。
  • void *dlsym(void *handle, const char *symbol);
    寻找对应symbol的函数方法,handle就是dlopen返回的句柄。一般如下使用:

     1: dlerror(); /* clear error code */
     2: s = (actual_type) dlsym(handle, symbol_being_searched_for);
     3: if ((err = dlerror()) != NULL) {
     4: /* handle error, the symbol wasn't found */
     5: } else {
     6: /* symbol found, its value is in s */
     7: }
  • int dlclose(void *handle);
    关闭一个动态加载库。当一个动态库被加载多次时,你需要用同样次数dlclose该动态库才可以deallocated.

我们编译该代码gcc -g -rdynamic -o progdl progdl.c -ldl,即可得到可执行文件(其中-g选项是为了gdb调试所用),其中的库为动态加载后又关闭的。我们使用gdb看一下代码:

(gdb) b main
Breakpoint 1 at 0x804878d: file progdl.c, line 12.
(gdb) r
Starting program: /home/gnuhpc/MyCode/lib/dynamic/progdl

Breakpoint 1, main (argc=1, argv=0xbffff4a4) at progdl.c:12
12       lib_handle = dlopen("/usr/local/lib/libctest.so", RTLD_LAZY);
(gdb) f
#0  main (argc=1, argv=0xbffff4a4) at progdl.c:12
12       lib_handle = dlopen("/usr/local/lib/libctest.so", RTLD_LAZY);
(gdb) s
13       if (!lib_handle)
(gdb) n
19       fn = dlsym(lib_handle, "ctest1");
(gdb)
20       if ((error = dlerror()) != NULL) 
(gdb)
26       (*fn)(&x);
(gdb)
27       printf("Valx=%d/n",x);
(gdb) p x
$1 = 5
(gdb) p fn
$2 = (double (*)(int *)) 0x28c4dc

可以看到fn获得了ctest1的地址。

 

参考文献:

http://www.yolinux.com/TUTORIALS/LibraryArchives-StaticAndDynamic.html

http://www.linuxjournal.com/article/3687

http://www.dwheeler.com/program-library/Program-Library-HOWTO/

 

作者:gnuhpc
出处:http://www.cnblogs.com/gnuhpc/