程序编译与运行时头文件或动态链接库的查找(一)

当考虑怎样总结这个头文件及动态链接库的查找问题时,我想到了一个程序从生到死的历程。写过很多程序,编译过很多程序,也运行过很多程序,对一个程序的从生到死,感觉很简单,也就没有做更多的或者说深入的思考与研究。也许我们习惯了在windows环境下的编程,在那里我们有很好的IDE,它能把一个工程组织得很好,直接点编译生成一个可执行文件,然后直接双击这个.exe文件或者创建一个快捷方式运行这个程序。以前可能我们也听说过,源码先要编译,然后链接,然后装载、运行,可我们很少去考虑这背后到底都发生了些什么,似乎也不用考虑那么多,因为我们的IDE实在是太智能了,因为我们已经很习惯了使用windows环境,因为在windows下安装个软件,我们几乎只需要点击下一步就可以了。

        这段时间在做linux系统下opencv2.0到ARM开发板的移植,在这里,我有很多问题不得不考虑,问题如下:

             程序在编译时,源码所需要的库(静态库和动态库)及头文件编译器是去哪找的?(库及头文件的查找)

             当输入一个命令时,系统时如何找到这个命令的?(命令的查找)

             程序在运行时,它所需要的库是去哪找的?(动态链接库的查找)

       这就是一个程序的由生到死的过程中需要考虑的几个问题!

       在linux系统下,我们常常要自己通过源码安装一些库,装一些软件,第一件事该想到的,编译生成后的头文件,库或者程序我们该放到哪,是放到/lib  /usr/lib  /usr/include  /usr/bin  /usr/local/lib  /usr/local/include    /usr/local/bin等目录下吗?可能有些书会说自己安装的程序一般放在/usr/local目录下,放到这下面我可以省心的不用去修改一些环境变量或配置文件了,但接下来我们可能会想,要是我以后想删除我前面安装的软件呢?你还能想起你以前安装这个软件时它到底安装了哪些文件了吗?几乎不可能吧!

         所以当我想安装一个软件时我希望像在windows下一样,把这个软件安装在一个单独的目录下,比如说我要安装opencv2.0,那么我就在/usr/local目录下创建一个目录opnecv2.0,然后把所有相关的都安装到/usr/local/opencv2.0目录下,这样如果我以后不想要这个库时我就可以直接删掉这个文件夹就可以了。可在这里我们就有些问题不得不考虑了:如果我们把opencv安装到了/usr/local/opencv2.0这个目录下了,那编译器在编译包含有opencv2.0的库或头文件时,编译器能找着这些头文件和库吗?如果这是一个可运行文件,我在其它目录下运行这个文件,系统能找到这个文件吗?它是如何找到这个文件的呢?当其它包含有opencv有关函数的程序时,它是如何找到这些库的呢?由这就引发了我上面提到的三个问题。

         下面我们先来看第一个问题:程序在编译时,源码所需要的库及头文件编译器是去哪找的?

         在这里,其实有库的查找,和头文件的查找,下面先来讲头文件的查找。我们在写一个比较大型的程序时,总是喜欢把这些函数还有一些数据结构的声明放在一个文件中,我们把这种文件称为头文件,文件名以.h后缀结尾。在一些源文件里,我们可能要包含自己写的头文件,还有一些标准库的头文件比如说stdio.h等等。在编译的预处理阶段,预处理程序会将这些头文件的内容插到相应的include指令处,现在的问题是编译器是如何找到这些头文件的。

         1. 在编译时,我们可以用-I(i的大写)选项来指定头文件所在的目录,如:

test.h内容如下:

  1. Struct student  
  2. {  
  3.     int  age;  
  4. };  

main.c 内容如下:

  1. #include<stdio.h>  
  2. #include<test.h>  
  3. int main()  
  4. {  
  5.     structstudent st;  
  6.     st.age= 25;  
  7.     printf(“st.age=%d\n”,st.age);  
  8.     return0;  

         可以把test.h放在与main.c同一个目录下,编译命令如下:

        [email protected]:~/tmp/workSpace/testincludedir$  gcc main.c -I./

        如果把test.h放在/usr/include/xgytest目录***意,xgytest是我自己建的一个目录

       编译命令如下:[email protected]:~/tmp/workSpace/testincludedir$  gcc main.c –I/usr/include/xgytest

       注意:在-I后可以有空格也可以没有空格,另外也可以指定多个目录,例如,tesh.h放在当前文件夹下,还有一个teacher.h放在 ./include目录下,则可以这样编译:

      [email protected]:~/tmp/workSpace/testincludedir$  gcc main.c -I ./ -I ./include/

           2. 设置gcc的环境变量C_INCLUDE_PATH、CPLUS_INCLUDE_PATH 、CPATH。

        C_INCLUDE_PATH编译 C 程序时使用该环境变量。该环境变量指定一个或多个目录名列表,查找头文件,就好像在命令行中指定 -isystem 选项一样。会首先查找 -isystem 指定的所有目录。

         CPLUS_INCLUDE_PATH编译 C++ 程序时使用该环境变量。该环境变量指定一个或多个目录名列表,查找头文件,就好像在命令行中指定 -isystem 选项一样。会首先查找 -isystem 指定的所有目录。

          CPATH 编译 C 、 C++ 和 Objective-C 程序时使用该环境变量。该环 境变量指定一个或多个目录名列表,查找头文件,就好像在命令行中指定-l 选项一样。会首先查找-l 指定的所有目录。

          假设test.h放在/usr/include/xgytest,则对C_INCLUDE_PATH做如下设置:

export C_INCLUDE_PATH=$C_INCLUDE_PATH:/usr/include/xgytest

详细请况可以参考如下文章:

http://blog.csdn.net/katadoc360/article/details/4151286

 http://blog.csdn.net/dlutxie/article/details/8176164

3. 查找默认的路径/usr/include   /usr/local/include等

/usr/lib/gcc-lib/i386-linux/2.95.2/include

/usr/lib/gcc-lib/i386-linux/2.95.2/……/……/……/……/include/g++-3

/usr/lib/gcc-lib/i386-linux/2.95.2/……/……/……/……/i386-linux/include

库文件但是如果装gcc的时候,是有给定的prefix的话,那么就是
/usr/include
prefix/include
prefix/xxx-xxx-xxx-gnulibc/include
prefix/lib/gcc-lib/xxxx-xxx-xxx-gnulibc/2.8.1/include

 总结一下gcc在编译源码时是如何寻找所需要的头文件的:

   1.  首先gcc会从-Idir   -isystem dir   -Bprefix    -sysroot  dir     --sysroot=dir    -iquote dir选项指定的路径查找(这些选项先指定的会先搜索,有特例的情况请参考前面的链接)

    2. 然后找gcc的环境变量:C_INCLUDE_PATH、CPLUS_INCLUDE_PATH 、CPATH、GCC_EXEC_PREFIX等。(这些环境变量搜索的先后顺序不确定,有待确认)

   3. 然后查找GCC安装的目录(可以通过gcc  -print-search-dirs查询)

   4.  然后再按照下面列出的顺序查找系统默认的目录:/usr/include      /usr/local/include

         

程序在编译时,编译器又是如何查找所需要的库的呢?这里的库既包括静态库又包括动态库。在这里,我们得先了解两个概念:库的链接时路径和运行时路径。

        现代连接器在处理动态库时将链接时路径(Link-time path)和运行时路径(Run-time path)分开,用户可以通过-L指定连接时库的路径,通过-R(或-rpath)指定程序运行时库的路径,大大提高了库应用的灵活性。

我们来看几个例子:

pos.c文件的内容如下:

  1. #include<stdio.h>  
  2. void pos()  
  3. {  
  4.     printf("the directory is .//n");  
  5. }  

main.c文件的内容如下:

  1. #include<stdio.h>  
  2. intmain()  
  3. {  
  4.     pos();  
  5.     return 0;  
  6. }  

接下来看如下执行的命令:

我们来分析下上面图片中的命令:生成的动态链接库libpos.so放在了当前的路径下,接着用gcc main.c  –lpos 来链接这个库却发现ld找不着这个库!然后我加了一个-L选项,指出这个库在当前路径下,结果编译通过,可在运行刚编译生成的a.out时又出现了错误!这就是运行是的链接错误!运行时的链接问题在后面将有介绍。用ldd命令可以查看一个可执行文件依懒于哪些库。注意-lpos, 这里的-l是L的小写,另外也可以写成-l  pos即中间有一个空格,但有没有空格是有一点区别的,有空格的只搜索与POSIX兼容的库,一般建议使用没有空格的。

         另外我们可以把刚才编译生成的libpos.so拷到默认的路径/lib  /usr/lib /usr/local/lib路径下,然后直接执行gcc main.c –lpos也可以通过编译。

         在这里补充说明一点:Linux下 的库文件在命名时有一个约定,那就是库文件应该以lib三个字母开头,由于所有的库文件都遵循了同样的规范,因此在用-l(L的小写字母)选项指定链接的库文件名时可以省去 lib三个字母,也就是说GCC在对-lfoo进行处理时,会自动去链接名为libfoo.so的文件。

每个共享库也有一个实名,其真正包含有库的代码,组成如下:

so+.+子版本号+.+发布号(最后的句点和发布号是可选项。)

另外,共享库还有一个名称,一般用于编译连接,称为连名(linkername),它可以被看作是没有任何版本号的so名。在上面的讨论中,我一直是以动态库(或者说共享库)为例的,其实对于静态库也一样,只是在这里又有一个问题,如果在同一个目录下既有动态库,又有静态库,且它俩的文件名也一样,只是后缀不一样,那链接器在链接时是链接动态库还是链接静态库呢?如果我要指定链接动态库或者静态库又该如何做呢?

           让我们来看看下面执行的命令(注意下,为了方便,我开了两个终端):

通过上面的这些命令,也许就能回答我刚提出的两个问题了。

在这里,我还想看下编译时gcc是否会查LD_LIBRARY_PATH环境变量,还有/etc/ld.so.conf文件指定的路径,命令如下:

从上面的命令可以看出,编译时,编译器不会查找LD_LIBRARY_PATH,还有/etc/ld.so.conf文件中指定的路径。下面来总结下:

 

程序在编译链接时,编译器是按照如下顺序来查找动态链接库(共享库)和静态链接库的:

1.  gcc会先按照-Ldir    -Bprefix选项指定的路径查找

2. 再找gcc的环境变量GCC_EXEC_PREFIX

3. 再找gcc的环境变量LIBRARY_PATH

4. 然后查找GCC安装的目录(可以通过gcc  -print-search-dirs查询)

5.  然后查找默认路径/lib

6.  然后查找默认路径/usr/lib

7.  最后查找默认路径/usr/local/lib

8.  在同一个目录下,如果有相同文件名的库(只是后缀不同),那么默认链接的是动态链接库,可以用-static选项显示的指定链接静态库。

 

 第二个问题:当输入一个命令时,系统时如何找到这个命令的?(命令的查找)

    如果我们输入一个命令时带入路径时一般是不会不什么疑问的,因为此时我们执行的就是指定路径下程序。当我们只输入一个命令名时会发生什么情况呢?

当我们键入命令名时,linux系统更确切的说应该是shell按照如下顺序搜索:

1.  Shell首先检查命令是不是保留字(比如for、do等)

2.  如果不是保留字,并且不在引号中,shell接着检查别名表,如果找到匹配则进行替换,如果别名定义以空格结尾,则对下一个词作别名替换,接着把替换的结果再跟保留字表比较,如果不是保留字,则shell转入第3步。

3.  然后,shell在函数表中查找该命令,如果找到则执行。

4.  接着shell再检查该命令是不是内部命令(比如cd、pwd)

5.  最后shell在PATH中搜索以确定命令的位置

6.  如果还是找不到命令则产生“command not found”错误信息。

这里要注意一点:系统在按PATH变量定义的路径搜索文件时,先搜到的命令先执行。例如,我的PATH变量如下:

[email protected]:~# echo $PATH

/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:

如果在不同的目录中有两个ls文件,例如/usr/local/sbin/ls, /usr/local/bin/ls,那么在使用ls的时候,会执行/usr/local/sbin/ls,因为在PATH中哪个目录先被查询,则哪个目录下的文件就会先执行。

 

第三个问题:程序在运行时,它所需要的库是去哪找的?(动态链接库的查找)

        在这里我没有提到头文件的查找,因为头文件只在编译的时候才会用到,编译完后就不需要头文件了!另外,这里的库指的是动态链接库,静态链接库在链接后是不需要了的,因为链接时链接器会把静态库中的代码插入到相应的函数的调用处,所以程序在运行时不再需要静态库,而对于动态库来说,链接时,并没有将动态库中的任何代码或数据拷贝到可执行文件中,而只是拷贝了一些重定位与符号表信息!所以程序在运行时才需要链接时所使用的动态链接库以执行动态链接库中的代码!这个可以参考《深入理解计算机系统》第七章。

 

程序运行时动态库的搜索路径搜索的先后顺序是:

1.编译目标代码时指定的动态库搜索路径(指的是用-wl,rpath或-R选项而不是-L);

example: gcc -Wl,-rpath,/home/arc/test,-rpath,/lib/,-rpath,/usr/lib/,-rpath,/usr/local/lib test.c

2.环境变量LD_LIBRARY_PATH指定的动态库搜索路径;

3.配置文件/etc/ld.so.conf中指定的动态库搜索路径;

4.默认的动态库搜索路径/lib;

5.默认的动态库搜索路径/usr/lib。

在上述1、2、3指定动态库搜索路径时,都可指定多个动态库搜索路径,其搜索的先后顺序是按指定路径的先后顺序搜索的。

 

上面这个的具体内容可以参考:

http://hi.baidu.com/kkernel/blog/item/ce31bb34a07e6b46251f14cf.html

         在这里补充说明下:gcc的-Wl,rpath选项可以设置动态库所在路径,也就是编译生成的该程序在运行时将到-Wl,rpath所指定的路径下去寻找动态库,如果没找到则到其它地方去找,并且这个路径会直接写在elf文件(就是生成的可执行文件)中,这样可以免去设置LD_LIBRARY_PATH。注意,gcc参数设定时-Wl,rpath,/path/to/lib, 中间不能有空格。

gcc -o pos main.c -L. -lpos -Wl,-rpath,./

上面这个命令的意思是:编译main.c时在当前目录下查找libpos.so这个库,生成的文件名为pos,当执行pos这个文件时,在当前目录下查找所需要的动态库文件。

可以像下面这个命令一样指定查找多个路径:

gcc -Wl,-rpath,/home/arc/test,-rpath,/lib/,-rpath,/usr/lib/,-rpath,/usr/local/libtest.c

更改/etc/ld.so.conf文件后记得一定要执行命令:ldconfig!该命令会将/etc/ld.so.conf文件中所有路径下的库载入内存中。

 

下面对编译时库的查找与运行时库的查找做一个简单的比较:

1. 编译时查找的是静态库或动态库,而运行时,查找的只是动态库。

2. 编译时可以用-L指定查找路径,或者用环境变量LIBRARY_PATH,而运行时可以用-Wl,rpath或-R选项,或者修改/etc/ld.so.conf文件或者设置环境变量LD_LIBRARY_PATH.

3. 编译时用的链接器是ld,而运行时用的链接器是/lib/ld-linux.so.2.

4. 编译时与运行时都会查找默认路径:/lib  /usr/lib

5. 编译时还有一个默认路径:/usr/local/lib,而运行时不会默认找查该路径。

           如果安装的包或程序没有放在默认的路径下,则使用mancommand查找command的帮助时可能查不到,这时可以修改MANPATH环境变量,或者修改/etc/manpath.config文件。如果使用了pkg-config这个程序来对包进行管理,那么有可能要设置PKG_CONFIG_PATH环境变量,这个可以参考:http://www.linuxsir.org/bbs/showthread.php?t=184419

             /etc/ld.so.cache 是一个非文本的数据文件,不能直接编辑,它是根据 /etc/ld.so.conf 中设置的搜索路径由 /sbin/ldconfig 命令将这些搜索路径下的共享库文件集中在一起而生成的(ldconfig 命令要以 root 权限执行)。因此,为了保证程序执行时对库的定位,在 /etc/ld.so.conf 中进行了库搜索路径的设置之后,还要运行 /sbin/ldconfig 命令,更新 /etc/ld.so.cache 文件。ldconfig的作用就是将/etc/ld.so.conf 指定的路径下的库文件缓存到/etc/ld.so.cache 。因此当安装完一些库文件(例如刚安装好glib),或者修改ld.so.conf增加新的库路径后,需要运行一下/sbin/ldconfig 使所有的库文件都被缓存到ld.so.cache中,不然修改的内容就等于没有生效。

        修改/etc/ld.so.conf后,当系统重新启动后,所有基于 GTK2 的程序在运行时都将使用新安装的 GTK+ 库。由于 GTK+ 版本的改变,有时会给应用程序带来兼容性的问题,造成某些程序运行不正常。为了避免出现这些情况,在 GTK+ 及其依赖库的安装过程中对于库的搜索路径的设置将采用环境变量的方式 export LD_LIBRARY_PATH=/opt/gtk/lib:$LD_LIBRARY_PATH

写在最后的话

    一个程序的从生到死会发生很多很多的故事,在这里,我只是从一个角度探讨了其中的冰山一角,还有许许多多的问题需要去理解,比如说:编译链接时,各个文件是如何链接到一起的?程序运行时,动态库已经被加载到内存中,程序又是如何准确找到动态库在内存中的位置的?动态库的链接器/lib/ld-linux.so.2自己本身也是一个动态库,那么它又是如何被载入内存的呢?更深入的想一下,可以认为ld-linux.so.2是随内核一起载入内存的,那内核又是如何载入内存的呢?如果说内核是由bootloader载入的,那bootloader又是如何载入内存的呢?也许你该想到BIOS了。其中的一些问题可以参考《深入理解计算机系统》这本书。