Lib库使用学习笔记

Lib库使用学习笔记

技术前沿 2008-03-31 14:21:10 阅读177 评论0   字号: 订阅

Chapter 1. 为什么使用库文件

我们在实际编程工作中肯定会遇到这种情况:有几个项目里有一些函数模块的功能相同,实现代码也相同,也是我们所说的重复代码。比如,很多项目里都有一个用户验证的功能。代码段如下:

    //UserLogin.h文件,提供函数声明
    int IsValidUser(char* username, int namelen);
    
    //UserLogin.c文件,实现对用户信息的验证
    int IsValidUser(char* username, int namelen)
    {
   int IsValid = 0;
   /*下面是具体的处理代码,略去*/
   return IsValid;
    }
   

如果每个项目都保存着这两个UserLogin.h和UserLogin.c文件,会有以下几个弊端:

  1. 每个项目里都有重复的模块,造成代码重复。

  2. 代码的重用性不好,一旦IsValidUser的代码发生了变化,为了保持设计的一致性,我们还要手工修改其他项目里的UserLogin.c文件,既费时又费力,还容易出错。 库文件就是对公共代码的一种组织形式。

为了解决上面两个弊端,就提出了用库文件存放公共代码的解决方案,其要点就是把公共的(也就是可以被多次复用的)目标代码从项目中分离出来,统一存放到库文件中,项目要用到这些代码的时候,在编译或者运行的时候从库文件中取得目标代码即可。库文件又分两种:静态库和动态库。

Chapter 2. 静态库和动态库

简单的说,如果程序是在编译时加载库文件的,就是使用了静态库,静态库的文件名格式是"lib*.a"。如果是在运行时加载目标代码,就成为动态库,动态库的文件名格式是"lib*.so.*"。换句话说,如果是使用静态库,则静态库代码在编译时就拷贝到了程序的代码段,程序的体积会膨胀。如果使用动态库,则程序中只保留库文件的名字和函数名,在运行时去查找库文件和函数体,程序的体积基本变化不大。

静态库的原则是“以空间换时间”,增加程序体积,减少运行时间;动态库则是“以时间换空间”,增加了运行时间,但减少了程序本身的体积。在附录的PIC部分,也会做一些说明。

从文件的格式角度讲,静态库的本质是一种档案文件(.o文件的集合),其中包含了一个内容索引 (也可以不包含,但没有索引的静态库不能用来链接,在附录的ar 和各个模块既.o文件;而动态库是ELF格式的文件,可以被用来加载和执行,而静态库不可以。

还有一种库文件,共享库。看到网上有些资料说,动态库就是共享库的一种变种, 由于没有使用到,没有详细研究。

有时候,会在目录中看到以".la"或".lo"结尾的文件,这些是GNU的工具libtool生成和使用的文件, 用来说明实际库文件的使用信息和以来关系,详细的内容会在以后automake autoconf and libtool的文档中介绍。

 

Chapter 3. 静态库的生成和使用

3.1. 制作最简单的静态库文件

编写如下两个文件,放在同一目录中:

  mylib.h  //静态库头文件
  void test();
   
   
  mylib.c  //静态库实现文件
  #include 
  void test()
  {
   printf("hello world./n");
  }
   

使用下边的命令生成静态库:

  gcc -c mylib.c
  ar rc libmy.a mylib.o
   

这样就生成了静态库libmy.a,注意一定要以lib*.a这样的格式命名,否则链接器ld不能识别。 使用命令"file libmy.a"看看它的格式,是一个档案文件。我们可以使用 nm查看它的内部构成:

  [root @Benson libtest]# nm libmy.a
   
  libmy.o:
    U printf
  00000000 T test
   

这表示静态库有模块libmy.o,在使用的时候,gcc会根据需要将函数名得到模块,然后从静态库中 提取出对应的".o"文件的内容,然后用来链接,就是使用单独的".o"文件一样。

3.2. 使用库文件

编写一个测试程序main.c,内容为:

  #include "mylib.h"
  
  int main(void)
  {
   test();
   return 0;
  }
   

由于需要调用libmy.a中的test函数,所以在编译时,需通过"-L -l"参数指定链接这个库:

  gcc -I./ -o main main.c -L./ -lmy
   

通过-I和-L参数制定了gcc的头文件和库文件搜索路径为当前目录,也可以根据需要指定为其他目录。

生成执行文件main后,执行命令"file main",可以看到:

  [root @Benson libtest]# file main
  main: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), for GNU/Linux 2.2.5,
  staticlly linked (uses shared libs), not stripped
   

运行,看到结果,成功。

Chapter 4. 动态库的生成和使用

 

4.1. 制作最简单的动态库文件

 

我们同样使用上边的mylib.h和mylib.c文件,但使用不同的命令生成库文件:

  gcc -fpic -shared libmy.so libmy.c
   

这样就生成了动态库文件libmy.so,-fpic这个选项指定是否使用 PIC,这个选项的使用需要系统平台的支持,一般建议添加,如果不支持,gcc会报错。

生成libmy.so后,使用"file libmy.so"命令,可以看到是一个ELF格式的文件,这说明共享库的 使用需要通过符号解析和重定位加载入内存才能使用。

4.2. 动态库的隐式调用

动态库有两种使用方法,隐式调用和显示调用。隐式调用的方法跟静态库的使用方法一样,都是通过 gcc的"-I -L"参数指定库文件的路径,如果同一个库文件,在系统中同时存在静态库和动态库,默认情况下 gcc主动链接动态库。但也可以通过gcc的"--static"选项,强制指定使用静态库。

  gcc -I./ -o main main.c -L./ -lmy  # 使用动态库生成main
  gcc main.c ./libmy.so -o main   # 使用动态库生成main,不同的地方在于指定了libmy.so
          的加载路径,这种用法很少用。详细的内容附录ld.so
               
  gcc --static -I./ -o main main.c -L./ -lmy  # 使用静态库生成main
   

输入"file main"命令,可以看到:

  [root @Benson libtest]# file main
  main: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), for GNU/Linux 2.2.5,
  dynamically linked (uses shared libs), not stripped
   
运行程序,报错:
  [root @Benson libtest]# ./main
  ./main: error while loading shared libraries: libmy.so: cannot open shared object file: 
  No such file or directory
   

没有找到libmy.so这个动态库,我这个库文件就在当前目录下?

4.3. 使动态库被系统共享的方法

前边已经说过动态库需要加载到内存中,才能使用。注意,链接和加载是两码事, 链接发生在生成可执行程序的阶段,加载发生在运行阶段。gcc的"-I -L"参数只是保证在链接阶段 指定文件路径,和加载无关,当然也有办法可以使链接阶段的路径信息在加载阶段起作用, 这些涉及到gcc的详细用法,和可执行文件的格式,附录中会简单介绍一些。运行时的加载的工作 不是由main完成的,而是由/lib/ld.so(不同平台会有些出入)完成。他有自己的查找动态连接库的规则, 所以在不更新ld.so的查找路径的情况下,会出现上边的错误。

根据ld.so的man手册,ld.so共有六种方式查找需要的动态库,这里只介绍常用的三种方式,详细的 内容见附录ld.so

  1. 按照环境变量LD_LIBRARY_PATH的内容查找(setuid类的程序排除)

  2. 在库高速缓存文件ld.so.conf中给出的路径查找

  3. 在/lib,/usr/lib目录下查找

相对应的,可以分别通过设置环境变量LD_LIBRARY_PATH,通过在ld.so.conf文件中添加路径和将 动态库文件复制到/lib、/usr/lib目录下,使动态库可以在加载时被找到。

  [root @Benson libtest]# export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/`pwd`
  [root @Benson libtest]# cat `pwd` >> /etc/ld.so.conf ; ldconfig
  [root @Benson libtest]# cp libmy.so /lib -f
   

以上三种方式都可以工作。关于ldconfig的使用详解,见 附录ldconfig

此时再执行main,可以正常运行了。

通过ldd命令,可以看到main程序依赖于libmy.so。

  [root @Benson libtest]# ldd main
  libtest.so => not found
  libc.so.6 => /lib/tls/libc.so.6 (0x42000000)
  /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
   

4.4. 动态库的显式调用

动态库的显示调用,也需要库文件被加载到内存中。但是使用方法和静态库完全不同。需要系统 调用——dlopen dlsym dlclose dlerror 的支持。可以使用同一个libmy.so文件,但是需要新的测试文件 :

  /* main.c 测试动态库显式调用的程序 */
  #include         //用于动态库管理的系统头文件  
  #include "libmy.h"   //要把函数的头文件包含进来,否则编译时会报错
  int main(void)
  {
   /* 声明对应的函数的函数指针 */
   void (*pTest)();   
   
   /* 加载动态库 */
   void *pdlHandle = dlopen("libtest.so", RTLD_LAZY);
   
   /* 错误处理 */
   if(pdlHandle == NULL )
   {
    printf("Failed load library/n");
    return -1;
   }
   
   char* pszErr = dlerror();
   if(pszErr != NULL)
   {
    printf("%s/n", pszErr);
    return -1;
   }
   /* 获取函数的地址 */
   pTest = dlsym(pdlHandle, "test");
   pszErr = dlerror();
   if(pszErr != NULL)
   {
    printf("%s/n", pszErr);
    dlclose(pdlHandle);
    return -1;
   }
   /* 实现函数调用 */
   pTest ();
   /*程序结束时关闭动态库 */
   dlclose(pdlHandle);
   return 0;
  }
   

编译时需要加入-ldl选项:

  [root @Benson libtest]# gcc -ldl -o main main.c
   

运行程序,成功。这种方式,不需要在链接阶段指定动态库的位置。在运行阶段,调用要使用的函数

通过ldd命令,可以看到main程序不再依赖于libmy.so,转为 依赖libdl.so

  libdl.so.2 => /lib/libdl.so.2 (0x40034000)
  libc.so.6 => /lib/tls/libc.so.6 (0x42000000)
  /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
   

Chapter 5. 链接时同时使用动态库和静态库

当一个执行程序链接时需要多个库文件时,通常情况下,我们要么统一采用静态链接,要么统一采用动态链接。 实际上,我们可以通过gcc的选项分别指定每种库的链接方式:-Wl -Bstatic;-Wl -Bdynamic。

  gcc -o main main.c -Wl -Bstatic -lc -Wl -Bdynamic -test
 

上边这个例子中制定了静态链接libc和动态连接libtest。而实际上gcc选项-Wl表示将后边的参数传给链接器 ,-Bstatic和-Bdynamic是ld的选项。

Appendix A. 附 录

A.1. ldconfig

ldconfig用来维护ld.so加载的动态库的路径信息和依赖信息,路径信息存放在/etc/ld.so.conf中 ,/etc/ld.so.cache中存放了/lib /usr/lib 和/etc/ld.conf包含的路径内的所有库信息。 可以通过ldd命令查看动态库的依赖信息。

ldconfig有两种常用的使用方法:

  1. 将需要查找的目录加入/etc/ld.so.conf,然后运行ldconfig更新,更新结果永久有效

  2. ldconfig + 路径,这种方式能直接更新指定的目录,但是结果在下次执行ldconfig 时,将会失效。

ldconfig的选项比较有用的是-v和-n,其他的可参考man手册:

  • -v 或 --verbose 详细的显示扫描的目录及搜索到的动态连接库,还有它所创建的 链接的名字

  • -n 使扫描时,不扫描默认目录/lib、/usr/lib和/etc/ld.so.conf包含的路径

A.2. ldd

ldd命令很简单,用来显示文件所依赖的动态库文件,同样-v选项显示详细信息。

A.3. ar

ar命令用来创建归档文件,修改归档文件,和提取归档文件中的模块。这里归档文件就是静态库, 模块就是里边的各个".o"文件。在静态库中每个".o"文件的内容,权限,时间戳,文件所有者,文件所属组 都将被保留,并且可在提取时回复。ar通过在静态库中创建模块列表来维护内容结构。可以通过"nm -s" 命令查看这个列表内容。ar命令的选项很多,可通过查看man手册获取全部内容,这里我举几个例子来说明 它的主要功能:

  1. 创建静态库文件:

      ar -rc libtest.a libmy.o libtest.o
       

    选项分为"r" 和 "c"。-r 表示将后边的".o"模块加入库文件中;-c 表示当库文件不存在时创建。 同样,向存在的库文件添加新的模块时,只需要-r 选项即可。

  2. 查看静态库中的模块:

      [root @Benson libtest]# ar -t libtest.a
      libmy.o
      libtest.o
       
  3. 删除静态库中的模块:

      ar -d libtest.a libmy.o
       
    此时在查看库的模块,libmy.o就不存在了。
  4. 提取静态库中的模块:

      [root @Benson libtest]# ls 
      libtest.a
      [root @Benson libtest]# ar -x libtest.a libmy.o ; ls
      libtest.a libmy.o
       

    从libtest.a中提取出了libmy.o文件

A.4. nm

nm 命令用来查看目标文件中的符号信息,nm命令对每个符号显示如下内容:

  • 每个符号的值。

  • 当指定-S选项时,显示函数符号对应的函数体的大小。

  • 每个符号的类型,如,U表示该符号没有在库中定义;T表示该符号在库中定义。还有其他的内容涉及 到目标文件的格式,这里就不详细介绍。

  • 每个符号的名字。

nm的选项中,"-s"选项用来显示静态库文件的列表信息。其他选项可查看man手册。

A.5. ranlib

为静态库生成索引信息,并将索引信息保存在静态库文件中,它是ar -s命令的变形。带有索引的 静态库能够加快连接速度,并允许库中的函数相互调用,而不需要考虑函数位置的先后。

A.6. PIC

PIC(Position Independent Code),位置无关编码。是一种库文件的编码组织方式, 其特点是方便系统装载。是否支持这种格式的动态库,与硬件系统平台有关。

简单的说,这种格式的库文件包含两个符号GOT(_GLOBAL_OFFSET_TABLE_)和PLT (Procedure Linkage Table)。调用函数实际调用的是PLT里的地址,访问全局变量通过GOT里边的地址 这些地址在未运行时不确定,在加载运行后,才被解析出来,所以可以加载到进程地址空间的任何地方。

摘自网络的一篇文章,含有更多信息:

PIC code radically differs from conventional code in the way it calls functions and operates on data variables.

It will access these functions and data through an indirection table, the "Global Offset Table" (GOT), by software convention accessible using the reserved name "_GLOBAL_OFFSET_TABLE_".

The exact mechanism used for this is hardware architecture dependent, but usually a special machine register is reserved for setting up the location of the GOT when entering a function.

The rationale behind this indirect addressing is to generate code that can be independently accessed of the actual load address.

In a true PIC library without relocations in the text segment, only the symbols exported in the "Global Offset Table" need updating at run-time depending on the current load address of the various shared libraries in the address space of the running process.

Likewise, procedure calls to globally defined functions are redirected through the "Procedure Linkage Table" (PLT) residing in the data segment of the core image. Again, this is done to avoid run-time modifications to the text segment.

The linker-editor allocates the Global Offset Table and Procedure Linkage Table when combining PIC object files into an image suitable for mapping into the process address space. It also collects all symbols that may be needed by the run-time link-editor and stores these along with the image's text and data bits. Another reserved symbol, _DYNAMIC is used to indicate the presence of the run-time linker structures. Whenever _DYNAMIC is relocated to 0, there is no need to invoke the run-time link- editor. If this symbol is non-zero, it points at a data structure from which the location of the necessary relocation- and symbol information can be derived. This is most notably used by the start-up module, crt0, crt1S and more recently Scrt1. The _DYNAMIC structure is conventionally located at the start of the data segment of the image to which it pertains.

On most architectures, when you compile source code to object code, you need to specify whether the object code should be position independent or not. There are occasional architectures which don't make the distinction, usually because all object code is position independent by virtue of the Application Binary Interface (ABI), or less often because the load address of the object is fixed at compile time, which implies that shared libraries are not supported by such a platform). If an object is compiled as position independent code (PIC), then the operating system can load the object at any address in preparation for execution. This involves a time overhead, in replacing direct address references with relative addresses at compile time, and a space overhead, in maintaining information to help the runtime loader fill in the unresolved addresses at runtime. Consequently, PIC objects are usually slightly larger and slower at runtime than the equivalent non-PIC object. The advantage of sharing library code on disk and in memory outweigh these problems as soon as the PIC object code in shared libraries is reused.

PIC compilation is exactly what is required for objects which will become part of a shared library. Consequently, libtool builds PIC objects for use in shared libraries and non-PIC objects for use in static libraries. Whenever libtool instructs the compiler to generate a PIC object, it also defines the preprocessor symbol, `PIC', so that assembly code can be aware of whether it will reside in a PIC object or not.

Typically, as libtool is compiling sources, it will generate a `.lo' object, as PIC, and a `.o' object, as non-PIC, and then it will use the appropriate one of the pair when linking executables and libraries of various sorts. On architectures where there is no distinction, the `.lo' file is just a soft link to the `.o' file.

In practice, you can link PIC objects into a static archive for a small overhead in execution and load speed, and often you can similarly link non-PIC objects into shared archives.

When you use position-independent code, relocatable references are generated as an indirection that use data in the shared object's data segment. The text segment code remains read-only, and all relocation updates are applied to corresponding entries within the data segment.

If a shared object is built from code that is not position-independent, the text segment will usually require a large number of relocations to be performed at runtime. Although the runtime linker is equipped to handle this, the system overhead this creates can cause serious performance degradation.

You can identify a shared object that requires relocations against its text segment using tools such as 'readelf -d foo' and inspect the output for any TEXTREL entry. The value of the TEXTREL entry is irrelevant. Its presence in a shared object indicates that text relocations exist.

A.7. ld.so

/lib/ld.so是系统的动态库加载器,属于Glibc编译生成的,与系统平台密切相关。加载目标代码 涉及的内容很多,但是这里主要围绕ld.so的动态库搜索过程,做全面的解释。

ld.so在加载动态库的时候,分先后顺序,按以下六步搜索:

  1. 可执行文件自身的动态段(.dynamic section)中DT_NEED入口中包含的路径。

    首先应该了解到,每个执行文件的数据是按照段来存储的,有代码段,数据段,BSS段等。那么 这里的动态段也是其中的一部分。我们借助readelf命令来查看这部分信息:

      [root @Benson libtest]# readelf -d main
      Tag        Type                         Name/Value
      0x00000001 (NEEDED)                     Shared library: [liba.so]
      0x00000001 (NEEDED)                     Shared library: [libc.so.6]
      0x0000000c (INIT)                       0x8048340
      0x0000000d (FINI)                       0x80484e4
      0x00000004 (HASH)                       0x8048128
      0x001000005 (STRTAB)                     0x8048240
      0x00000006 (SYMTAB)                     0x8048170
      0x0000000a (STRSZ)                      172 (bytes)
      0x0000000b (SYMENT)                     16 (bytes)
      0x00000015 (DEBUG)                      0x0
      0x00000003 (PLTGOT)                     0x80495fc
      0x00000002 (PLTRELSZ)                   16 (bytes)
      0x00000014 (PLTREL)                     REL
      0x00000017 (JMPREL)                     0x8048330
      0x00000011 (REL)                        0x8048328
      0x00000012 (RELSZ)                      8 (bytes)
      0x00000013 (RELENT)                     8 (bytes)
      0x6ffffffe (VERNEED)                    0x8048308
      0x6fffffff (VERNEEDNUM)                 1
      0x6ffffff0 (VERSYM)                     0x80482ec
      0x00000000 (NULL)                       0x0
       

    readelf命令,能够查看ELF格式的文件的所有信息,在这里先不详细介绍。对于上边显示的信息 我们注意前两行,类型是NEEDED,后便对应的是内容,这些就是DT_NEED部分的内容。而内容部分的 liba.so和libc.so.6是没有路径信息的。所以ld.so不能根据这里的信息加载liba.so库。

    但如果使用如下的命令生成main程序:

      [root @Benson libtest]# gcc -o main main.o ./liba.so
      [root @Benson libtest]# readelf -d main
      0x00000001 (NEEDED)                     Shared library: [./liba.so]
      0x00000001 (NEEDED)                     Shared library: [libc.so.6]
      0x0000000c (INIT)                       0x8048340
      0x0000000d (FINI)                       0x80484e4
      0x00000004 (HASH)                       0x8048128
      0x001000005 (STRTAB)                     0x8048240
      0x00000006 (SYMTAB)                     0x8048170
      0x0000000a (STRSZ)                      172 (bytes)
      0x0000000b (SYMENT)                     16 (bytes)
      0x00000015 (DEBUG)                      0x0
      0x00000003 (PLTGOT)                     0x80495fc
      0x00000002 (PLTRELSZ)                   16 (bytes)
      0x00000014 (PLTREL)                     REL
      0x00000017 (JMPREL)                     0x8048330
      0x00000011 (REL)                        0x8048328
      0x00000012 (RELSZ)                      8 (bytes)
      0x00000013 (RELENT)                     8 (bytes)
      0x6ffffffe (VERNEED)                    0x8048308
      0x6fffffff (VERNEEDNUM)                 1
      0x6ffffff0 (VERSYM)                     0x80482ec
      0x00000000 (NULL)                       0x0
       

    此时liba.so就有了路径信息,那么ld.so就会在当前路径下查找liba.so。

  2. 可执行文件自身的动态段(.dynamic section)中DT_RPATH入口中包含的路径(存在的话)。 这种方式的详细信息我没有找到,由于不常用,我将这个问题暂时保留。

  3. 环境变量LD_LIBRARY_PATH路径(setuid类的程序排除)。

  4. 可执行文件自身的动态段(.dynamic section)中LD_RUNPATH入口给出的路径(存在的话)这种方式 的详细信息也暂时没有。但同第二种方式,当有链接选项--ignore-rpath LIST时会把LIST中 的RPATH和RUNPATH信息忽略掉。

  5. 库高速缓存文件ld.so.conf中给出的路径。这里补充一点,在做LFS的时候可能会遇到,ld.so使用 那个缓冲文件,可以通过在编译Glibc的时候,使用命令"echo 'slibdir= 路径' >> configpams"来更改。

  6. 默认系统路径/lib,/usr/lib。

这部分的内容涉及的知识非常的广,属于链接器和加载器这部分的内容。根据我们的工作需要,目前 了解到这种程度已经足够。顺便再补充一点小技巧:通过设置LD_TRACE_LOADED_OBJECTS=y环境变量,可以 跟踪库的加载过程。

A.8. 一个有趣而可怕的试验

在了解了以上的所有内容后,我们可以根据掌握的知识做一个有趣,可怕的试验,因为这可能会 引起一些安全隐患。

首先创建两个库文件liba.so和libb.so。

  /* 公用的头文件内容 */
  /* test.h */
  void test(void);
  
  /*************************************************/
  
  /* liba.c的内容 */
  
  #include 
  
  void test(void)
  {
   printf("hello world/n");
  }
  
  /*************************************************/
  
  /* libb.c的内容 */
  
  #include 
  
  void test(void)
  {
   printf("another say hello world/n");
  }
   
  [root @Benson libtest]# gcc -fpic -shared -o liba.so liba.c
  [root @Benson libtest]# gcc -fpic -shared -o libb.so libb.c
   

编写测试程序main,并链接liba.so:

  #include "test.h"
  
  int main(void)
  {
   test();
   return 0;
  }
   
  [root @Benson libtest]# gcc -o main main.c -L./ -la
  [root @Benson libtest]# ./main
  hello world
   

我们尝试这样的命令,然后在运行main,会得到有趣的结果:

  [root @Benson libtest]# sed "s#liba/.so#libb.so#" main > main2
  [root @Benson libtest]# chmod +x main2
  [root @Benson libtest]# ./main2
  another say hello world
   

运行结果好像被篡改了是吧。那么如果将这种方法用在setuid命令上,那不是要天下打乱了?还好 linux系统早就做出了限制,首先,setuid这类命令的权限控制非常严格,其次通过设置ld.so,使 LD_LIBRARY_PATH环境变量对于此类程序不起作用。但是我们自己的程序呢?

你可能感兴趣的:(程序人生)