linux+C环境下动态管理头文件和库文件

本文共分三个部分:首先给出了一个简单的Makefile的例子,然后分别详细阐述了头文件和库文件的相关知识

1.一个Makefile的例:

-L是指定链接时搜索路径,比如-L./等;

-l是指定具体的库文件,比如-lhello,是指定./目录下的libhello.so或libhello.a 文件

-I或-include是指定头文件目录

-Wl,-rpath=./:../是指定运行时搜索路径

  1. # 指令编译器和选项  
  2. CC = gcc  
  3. CFLAGS = -Wall -std=gnu99  
  4.   
  5. # 目标文件  
  6. TARGET = test  
  7. # C文件  
  8. SRCS = test.c  
  9. # 头文件查找路径  
  10. INC = -I. -I../
  11. # 库文件和库查找路径  
  12. DLIBS = -ltest  
  13. LDFLAGS = -L./lib  -L.
  14. # 指定运行时的库文件路径  
  15. RPATH = -Wl,-rpath=./lib:../  
  16.   
  17. # 目标文件  
  18. OBJS = $(SRCS:.c=.o)  
  19.   
  20. # 链接为可执行文件  
  21. $(TARGET):$(OBJS)  
  22. [tab]$(CC) -o $@ $^ $(LDFLAGS) $(DLIBS) $(RPATH)  
  23.   
  24. clean:  
  25. [tab]rm -rf $(TARGET) $(OBJS)  
  26.   
  27. # 连续动作,请清除再编译链接,最后执行  
  28. exec:clean $(TARGET)  
  29. [tab]@echo 开始执行  
  30. [tab]./$(TARGET)  
  31. [tab]@echo 执行结束  
  32.   
  33. # 编译规则 $@代表目标文件 $< 代表第一个依赖文件  
  34. %.o:%.c  
  35. [tab]$(CC) $(CFLAGS) $(INC) -o $@ -c $<  
    【1】DLIBS =  -ltest 指定共享库,请注意共享库的名称为 libtest.so,而-l参数只取test部分,去掉前缀lib和后缀.so。
    【2】LDFLAGS = -L./lib -L./ 指定共享库路径,请注意上一步中已经把共享库复制到lib目录中。
    【3】INC = -I./lib -I../  指定libtest.h目录,也可把libtest.h复制到test.c所在的目录。
    【4】$(CC) -o $@ $^ $(LDFLAGS) $(DLIBS) 链接过程指定共享库查找路径,指定共享库名称。
    【5】第【1】和第【2】点只在链接过程有效,在执行过程中需要通过-Wl,-rpath=<path>指定共享库路径。
      [6]   gcc -c -fPIC test.c -o test.o
        gcc -shared -fPIC -o libtest.so test.o

ar rc libhello.a hello.o

       该命令将hello.o添加到静态库文件libhello.a,ar命令就是用来创建、修改库的,也可以从库中提出单个模块,参数r表示在库中插入或者替换模块,c表示创建一个库


    

2.关于头文件:

头文件是一种文本文件,使用文本编辑器将代码编写好之后,以扩展名.h保存就行了。头文件中一般放一些重复使用的代码,例如函数声明、变量声明、常数定义、宏的定义等等。当使用#include语句将头文件引用时,相当于将头文件中所有内容,复制到#include处。#include有两种写法形式,分别是:
#include <> : 直接到系统指定的某些目录中去找某些头文件。
#include “” : 先到源文件所在文件夹去找,然后再到系统指定的某些目录中去找某些头文件。

gcc寻找头文件的路径(按照1->2->3的顺序)
    1. 在gcc编译源文件的时候,通过参数-I指定头文件的搜索路径,如果指定路径有多个路径时,则按照指定路径的顺序搜索头文件。命令形式如:“gcc -I /path/where/theheadfile/in sourcefile.c“,这里源文件的路径可以是绝对路径,也可以是相对路径。eg:
设当前路径为/root/test,include_test.c如果要包含头文件“include/include_test.h“,有两种方法:
1) include_test.c中#include “include/include_test.h”或者#include “/root/test/include/include_test.h”,然后gcc include_test.c即可
2) include_test.c中#include 或者#include ,然后gcc –I include include_test.c也可
 
    2. 通过查找gcc的环境变量C_INCLUDE_PATH/CPLUS_INCLUDE_PATH/OBJC_INCLUDE_PATH来搜索头文件位置。
 
    3. 再找内定目录搜索,分别是
/usr/include
/usr/local/include
/usr/lib/gcc-lib/i386-linux/2.95.2/include
最后一行是gcc程序的库文件地址,各个用户的系统上可能不一样。
    gcc在默认情况下,都会指定到/usr/include文件夹寻找头文件。
    gcc还有一个参数:-nostdinc,它使编译器不再系统缺省的头文件目录里面找头文件,一般和-I联合使用,明确限定头文件的位置。在编译驱动模块时,由于非凡的需求必须强制GCC不搜索系统默认路径,也就是不搜索/usr/include要用参数-nostdinc,还要自己用-I参数来指定内核头文件路径,这个时候必须在Makefile中指定。


    4. 当#include使用相对路径的时候,gcc最终会根据上面这些路径,来最终构建出头文件的位置。如#include 就是包含文件/usr/include/sys/types.h


3.关于库文件

路径分两种,一种是链接时的搜索路径,另一种是运行时的搜索路径

动态库添加方式有如下几种:

提前批:dlopen,但是一般不用,该方法只有在调用dlopen打开相应动态库时才会发现少库

1.ELF可执行文件中动态段中DT_RPATH所指定的路径。这实际上是通过一种不算很常用,却比较实用的方法所设置的:编译目标代码时,可以对gcc加入链接参数-L./指定链接时的搜索路径,加入“-Wl,-rpath”指定运行时动态库搜索路径;-Wl, 表示后面的参数将传给 link 程序 ld (因为 gcc 可能会自动调用ld )。这里通过 gcc 的参数 "-Wl,-rpath=./:../," 指定(如例 3 所示)。当指定多个动态库搜索路径时,路径之间用冒号 " : " 分隔。  
2.环境变量LD_LIBRARY_PATH指定的动态库搜索路径;这种方式是一次指定了链接和运行时的搜索路径  
3./etc/ld.so.cache中所缓存的动态库路径(如果支持ld.so.cache的话)。这可以通过修改配置文件/etc/ld.so.conf中指定的动态库搜索路径来改变;这种方式在改变之后需要执行sudo ldconfig 命令才可生效  
4.默认的动态库搜索路径/lib;  
5.默认的动态库搜索路径/usr/lib


linux 下有动态库和静态库,动态库以.so为扩展名,静态库以.a为扩展名。二者都使用广泛。本文主要讲动态库方面知识。

   
   基本上每一个linux 程序都至少会有一个动态库,查看某个程序使用了那些动态库,使用 ldd命令查看 
  1. # ldd /bin/ls
  2. linux-vdso.so.=> (0x00007fff597ff000)
  3. libselinux.so.=> /lib64/libselinux.so.(0x00000036c2e00000)
  4. librt.so.=> /lib64/librt.so.(0x00000036c2200000)
  5. libcap.so.=> /lib64/libcap.so.(0x00000036c4a00000)
  6. libacl.so.=> /lib64/libacl.so.(0x00000036d0600000)
  7. libc.so.=> /lib64/libc.so.(0x00000036c1200000)
  8. libdl.so.=> /lib64/libdl.so.(0x00000036c1600000)
  9. /lib64/ld-linux-x86-64.so.(0x00000036c0e00000)
  10. libpthread.so.=> /lib64/libpthread.so.(0x00000036c1a00000)
  11. libattr.so.=> /lib64/libattr.so.(0x00000036cf600000)
   这么多so,是的。使用ldd显示的so,并不是所有so都是需要使用的,下面举个例子
main.cpp
  1. #include <stdio.h>
  2. #include <iostream>
  3. #include <string>
  4. using namespace std;

  5. int main ()
  6. {
  7.    cout << "test" << endl;
  8.    return 0;
  9. }
   使用缺省参数编译结果
  1. # g++ -o demo main.cpp
  2. # ldd demo
  3.     linux-vdso.so.=> (0x00007fffcd1ff000)
  4.         libstdc++.so.=> /usr/lib64/libstdc++.so.(0x00007f4d02f69000)
  5.         libm.so.=> /lib64/libm.so.(0x00000036c1e00000)
  6.         libgcc_s.so.=> /lib64/libgcc_s.so.(0x00000036c7e00000)
  7.         libc.so.=> /lib64/libc.so.(0x00000036c1200000)
  8.         /lib64/ld-linux-x86-64.so.(0x00000036c0e00000)
   如果我链接一些so,但是程序并不用到这些so,又是什么情况呢,下面我加入链接压缩库,数学库,线程库
  1. # g++ -o demo -lz -lm -lrt main.cpp
  2. # ldd demo
  3.         linux-vdso.so.=> (0x00007fff0f7fc000)
  4.         libz.so.1 => /lib64/libz.so.1 (0x00000036c2600000)
  5.         librt.so.1 => /lib64/librt.so.1 (0x00000036c2200000)
  6.         libstdc++.so.=> /usr/lib64/libstdc++.so.(0x00007ff6ab70d000)
  7.         libm.so.6 => /lib64/libm.so.6 (0x00000036c1e00000)
  8.         libgcc_s.so.=> /lib64/libgcc_s.so.(0x00000036c7e00000)
  9.         libc.so.=> /lib64/libc.so.(0x00000036c1200000)
  10.         libpthread.so.=> /lib64/libpthread.so.(0x00000036c1a00000)
  11.         /lib64/ld-linux-x86-64.so.(0x00000036c0e00000)
  看看,虽然没有用到,但是一样有链接进来,那看看程序启动时候有没有去加载它们呢
  1. # strace ./demo
  2.     execve("./demo", ["./demo"], [/* 30 vars */]) = 0
  3.     ... = 0
  4.     open("/lib64/libz.so.1", O_RDONLY) = 3
  5.     ...
  6.     close(3) = 0
  7.     open("/lib64/librt.so.1", O_RDONLY) = 3
  8.     ...
  9.     close(3) = 0
  10.     open("/usr/lib64/libstdc++.so.6", O_RDONLY) = 3
  11.     ...
  12.     close(3) = 0
  13.     open("/lib64/libm.so.6", O_RDONLY) = 3
  14.     ...
  15.     close(3) = 0
  16.     open("/lib64/libgcc_s.so.1", O_RDONLY) = 3
  17.     ...
  18.     close(3) = 0
  19.     open("/lib64/libc.so.6", O_RDONLY) = 3
  20.     ...
  21.     close(3) = 0
  22.     open("/lib64/libpthread.so.0", O_RDONLY) = 3
  23.     ...
  24.     close(3) = 0
  25.     ...
  看,有加载,所以必定会影响进程启动速度,所以我们最后不要把无用的so编译进来,这里会有什么影响呢?
   大家知不知道linux从程序(program或对象)变成进程(process或进程),要经过哪些步骤呢,这里如果详细的说,估计要另开一篇文章。简单的说分三步:
    1、fork进程,在内核创建进程相关内核项,加载进程可执行文件;
    2、查找依赖的so,一一加载映射虚拟地址
    3、初始化程序变量。
  可以看到,第二步中dll依赖越多,进程启动越慢,并且发布程序的时候,这些链接但没有使用的so,同样要一起跟着发布,否则进程启动时候,会失败,找不到对应的so。所以我们不能像上面那样,把一些毫无意义的so链接进来,浪费资源。但是开发人员写makefile 一般有没有那么细心,图省事方便,那么有什么好的办法呢。继续看下去,下面会给你解决方法。
  先 使用 ldd -u demo 查看不需要链接的so,看下面,一面了然,无用的so全部暴露出来了吧
  1. # ldd -u demo
  2. Unused direct dependencies:
  3.         /lib64/libz.so.1
  4.         /lib64/librt.so.1
  5.         /lib64/libm.so.6
  6.         /lib64/libgcc_s.so.1
  使用  - Wl , - - as - needed 编译选项
  1. # g++ -Wl,--as-needed -o demo -lz -lm -lrt main.cpp
  2. # ldd demo
  3.         linux-vdso.so.=> (0x00007fffebfff000)
  4.         libstdc++.so.=> /usr/lib64/libstdc++.so.(0x00007ff665c05000)
  5.         libc.so.=> /lib64/libc.so.(0x00000036c1200000)
  6.         libm.so.=> /lib64/libm.so.(0x00000036c1e00000)
  7.         /lib64/ld-linux-x86-64.so.(0x00000036c0e00000)
  8.         libgcc_s.so.=> /lib64/libgcc_s.so.(0x00000036c7e00000)
  9. # ldd -u demo
  10. Unused direct dependencies:

  呵呵,办法很简单省事吧,本文主要讲so依赖的一些问题,下一篇将介绍so的路径方面一些不为人知的小秘密

我们知道linux链接so有两种途径:显示和隐式。所谓显示就是程序主动调用dlopen打开相关so;这里需要补充的是,如果使用显示链接,上篇文章讨论的那些问题都不存在。首先,dlopen的so使用ldd是查看不到的。其次,使用dlopen打开的so并不是在进程启动时候加载映射的,而是当进程运行到调用dlopen代码地方才加载该so,也就是说,如果每个进程显示链接a.so;但是如果发布该程序时候忘记附带发布该a.so,程序仍然能够正常启动,甚至如果运行逻辑没有触发运行到调用dlopen函数代码地方。该程序还能正常运行,即使没有a.so.

 

  既然显示加载这么多优点,那么为什么实际生产中很少码农使用它呢, 主要原因还是起使用不是很方便,需要开发人员多写不少代码。所以不被大多数码农使用,还有一个重要原因应该是能提前发现错误,在部署的时候就能发现缺少哪些so,而不是等到实际上限运行的时候才发现缺东少西。

 

  下面举个工作中最常碰到的问题,来引申出本篇内容吧。

写一个最简单的so, tmp.cpp

1.    int test()

2.    {

3.      return 20;

4.    }

  编译=>链接=》运行, 下面main.cpp 内容请参见上一篇文章。

[stevenrao] g++ -o demo -L/tmp -ltmp main.cpp

[stevenrao]$ ldd demo

        libtmp.so => not found

   这个错误是最常见的错误了。运行程序的时候找不到依赖的so。一般人使用方法是修改LD_LIBRARY_PATH这个环境变量

   export LD_LIBRARY_PATH=/tmp

[stevenrao]$ ./demo

test

   这样就OK了, 不过这样export 只对当前shell有效,当另开一个shell时候,又要重新设置。可以把export LD_LIBRARY_PATH=/tmp 语句写到 ~/.bashrc中,这样就对当前用户有效了,写到/etc/bashrc中就对所有用户有效了。

   前面链接时候使用 -L/tmp/ -ltmp 是一种设置相对路径方法,还有一种绝对路径链接方法

[stevenrao]$ g++ -o demo  /tmp/libtmp.so main.cpp

[stevenrao]$ ./demo

  test

[stevenrao]$ ldd demo

        linux-vdso.so.1 =>  (0x00007fff083ff000)

        /tmp/libtmp.so (0x00007f53ed30f000) 

   绝对路径虽然申请设置环境变量步骤,但是缺陷也是致命的,这个so必须放在绝对路径下,不能放到其他地方,这样给部署带来很大麻烦。所以应该禁止使用绝对路径链接so

   

   搜索路径分两种,一种是链接时候的搜索路径,一种是运行时期的搜索路径。像前面提到的 -L/tmp/ 是属于链接时期的搜索路径,即给ld程序提供的编译链接时候寻找动态库路径;而LD_LIBRARY_PATH则既属于链接期搜索路径,又属于运行时期的搜索路径。

   

   这里需要介绍链-rpath链接选项,它是指定运行时候都使用的搜索路径。聪明的同学马上就想到,运行时搜索路径,那它记录在哪儿呢。也像. LD_LIBRARY_PATH那样,每部署一台机器就需要配一下吗。呵呵,不需要..,因为它已经被硬编码到可执行文件内部了。看看下面演示

 

1.   [stevenrao] $ g++ -o demo -L /tmp/ -ltmp main.cpp

2.   [stevenrao] $ ./demo

3.   ./demo: error while loading shared libraries: libtmp.so: cannot open shared object file: No such file or directory

4.   [stevenrao] $ g++ -o demo -Wl,-rpath /tmp/ -L/tmp/ -ltmp main.cpp

5.   [stevenrao] $ ./demo

6.   test

7.   [stevenrao] $ readelf -d demo

8.    

9.   Dynamic section at offset 0xc58 contains 26 entries:

10.    Tag        Type                         Name/Value

11.   0x0000000000000001 (NEEDED)             Shared library: [libtmp.so]

12.   0x0000000000000001 (NEEDED)             Shared library: [libstdc++.so.6]

13.   0x0000000000000001 (NEEDED)             Shared library: [libm.so.6]

14.   0x0000000000000001 (NEEDED)             Shared library: [libgcc_s.so.1]

15.   0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]

16.   0x000000000000000f (RPATH)              Library rpath: [/tmp/]

17.   0x000000000000001d (RUNPATH)            Library runpath: [/tmp/]

   看看是吧,编译到elf文件内部了,路径和程序深深的耦合到一起



下面详细讲解第一种方式:

首先编写一个最简单的动态共享库,源代码pirnt.c如下:  
     1  #include  
     2  
     3  void print_foo()  
     4  {  
     5      printf("fooooooooo\n");  
     6  }  
注意将它编译成共享库:  
# gcc print.c -shared -o libprint.so  
# file libprint.so  
libprint.so: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), not stripped  
调用该共享库main.c代码如下:  
     1  #include  
     2  
     3  extern void print_foo();  
     4  
     5  int main()  
     6  {  
     7      print_foo();  
     8          
     9      return 0;  
    10  }  
编译之后的运行结果如下: 

# gcc main.c -L./ -lprint -o pfoo  
# ./pfoo  
./pfoo: error while loading shared libraries: libprint.so: cannot open shared object file: No such file or directory  
这便是典型的找不到动态库的错误。通常我们可以通过设置环境变量LD_LIBRARY_PATH来指定动态库的搜索路径(即上面的方法2),比如这样就可以正确运行了:  
# export LD_LIBRARY_PATH=./  
# ./pfoo  
fooooooooo  
但这种方法有一个明显的缺点:一旦LD_LIBRARY_PATH被设定,则在这个环境变量生效的范围之内,所有其他的ELF可执行程序也会按照这个顺序去搜索动态库,这样势必会造成搜索时的一些浪费。  
我们也可以使用另外一种方案来解决这种问题,即利用参数“-Wl,-rpath”在编译时指定运行时的搜索路径(即上面的方法1),如下所示:  
# unset LD_LIBRARY_PATH  
# echo $LD_LIBRARY_PATH  
# gcc main.c -L./ -lprint -o pfoo_r -Wl,-rpath=./  
# ./pfoo  
./pfoo: error while loading shared libraries: libprint.so: cannot open shared object file: No such file or directory  
# ./pfoo_r  
fooooooooo 

事实上我们可以通过readelf工具来查看两个文件的差异:  
# readelf -d pfoo  
Dynamic segment at offset 0x514 contains 21 entries:  
  Tag        Type                         Name/Value  
0x00000001 (NEEDED)                     Shared library: [libprint.so]  
0x00000001 (NEEDED)                     Shared library: [libc.so.6]  
0x0000000c (INIT)                       0x8048344  
0x0000000d (FINI)                       0x80484e0  
0x00000004 (HASH)                       0x8048128  
0x00000005 (STRTAB)                     0x8048240  
0x00000006 (SYMTAB)                     0x8048170  
0x0000000a (STRSZ)                      178 (bytes)  
0x0000000b (SYMENT)                     16 (bytes)  
0x00000015 (DEBUG)                      0x0  
0x00000003 (PLTGOT)                     0x80495f8  
0x00000002 (PLTRELSZ)                   16 (bytes)  
0x00000014 (PLTREL)                     REL  
0x00000017 (JMPREL)                     0x8048334  
0x00000011 (REL)                        0x804832c  
0x00000012 (RELSZ)                      8 (bytes)  
0x00000013 (RELENT)                     8 (bytes)  
0x6ffffffe (VERNEED)                    0x804830c  
0x6fffffff (VERNEEDNUM)                 1  
0x6ffffff0 (VERSYM)                     0x80482f2  
0x00000000 (NULL)                       0x0  
[root@localhost ldpath]# readelf -d pfoo_r  
Dynamic segment at offset 0x518 contains 22 entries:  
  Tag        Type                         Name/Value  
0x00000001 (NEEDED)                     Shared library: [libprint.so]  
0x00000001 (NEEDED)                     Shared library: [libc.so.6]  
0x0000000f (RPATH)                      Library rpath: [./]  
0x0000000c (INIT)                       0x8048348  
0x0000000d (FINI)                       0x80484e4  
0x00000004 (HASH)                       0x8048128  
0x00000005 (STRTAB)                     0x8048240  
0x00000006 (SYMTAB)                     0x8048170  
0x0000000a (STRSZ)                      181 (bytes)  
0x0000000b (SYMENT)                     16 (bytes)  
0x00000015 (DEBUG)                      0x0  
0x00000003 (PLTGOT)                     0x8049604  
0x00000002 (PLTRELSZ)                   16 (bytes)  
0x00000014 (PLTREL)                     REL  
0x00000017 (JMPREL)                     0x8048338  
0x00000011 (REL)                        0x8048330  
0x00000012 (RELSZ)                      8 (bytes)  
0x00000013 (RELENT)                     8 (bytes)  
0x6ffffffe (VERNEED)                    0x8048310  
0x6fffffff (VERNEEDNUM)                 1  
0x6ffffff0 (VERSYM)                     0x80482f6  
0x00000000 (NULL)                       0x0  
“readelf -d”可以用来查看ELF文件的动态节(Dynamic Section)。对比pfoo 和pfoo_r的结果我们可以发现,pfoo_r中多出来了RPATH项,指定”Library rpath: [./]”。通过这种方式,我们可以用非常小的代价(仅增加几乎可以忽略的空间开销),对每个ELF文件都指定最优化的搜索路径,达到提升性能的目的。这是我们比较推荐的一种方法。当然了,具体如果操作依赖于具体的软件系统的情况,简单的系统中直接将所有的库都放到/lib下也未尝不是一种简单易行的优化方案。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值