Total Pageviews

Friday, 4 March 2022

库/头文件搜索路径, 相关GCC参数

全文概要


在Linux系统中,命令行下编译C/C++代码是不二选择,GCC则是编译工具首选,提供了丰富的编译、调试特性,功能十分强大。本文介绍了使用GCC编译时,与头文件及库文件搜索路径相关的命令行参数及环境变量。

高级语言,因为其抽象性所以用户友好,但也正因于此,需要翻译成机器指令才能让CPU理解并最终执行。
对于C/C++程序而言,翻译是一个过程,包含如下步骤:

  1. 预处理(*.c -> *.i)
  2. 编译(*.i -> *.s)
  3. 汇编(*.s -> *.o)
  4. 链接(*.o -> ELF)

链接最后生成一个二进制的可执行文件,装载入内存方可执行。各个步骤的具体细节正在学习之中,将在之后的博文中介绍。

本文重点关注于介绍在使用GCC编译时常用的一些命令行参数及所需配置的环境变量,如指定头文件搜索路径、静态库搜索路径、动态库搜索路径等。

实验平台


操作系统:Ubuntu 16.04.4
GCC版本:5.4.0

静态库


静态库文件命名以lib开头,后缀为.a
静态库代码在编译时期即载入可执行程序,但可能导致可执行文件的体积过大。

.o文件创建静态库:

1
2
$ gcc -c hello.c
$ ac cr libhello.a hello.o # 静态库名为libhello.a

编译时链接静态库,假设静态库放在当前目录(.)下:

1
$ gcc -o hello main.c -L. -lhello

注意:-lhello放置的位置很重要,因为main.c中用到了libhello.a中定义的方法hello(),则必须将-lhello放在main.c之后,否则ld报错:

1
2
3
4
$ gcc -o hello -L. -lhello main.c 
/tmp/xxxxxxxx.o: In function 'main':
main.c:(.text+0xa): undefined reference to 'hello'
collect2: error: ld returned 1 exit status

链接时,链接器ld会将静态库中的相关代码全部放入最终形成的可执行文件中,因此即使在得到可执行文件后立即删除静态库文件,执行可执行文件时也不会有任何问题,因为可执行文件所依赖的静态库相关代码全部以副本形式保存在了可执行文件中。

动态库


动态库文件命名以lib开头,后缀为.so.x.y,其中x为主版本号,y为副版本号。
共享库的代码是在可执行文件运行时才载入内存的,在编译过程中仅是简单地引用,因此可执行文件的体积较小。

C与C++的标准库均为动态库,在Ubuntu 16.04.4下,路径如下:

1
2
/lib/x86_64-linux-gnu/libc.so.6
/usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21

.o文件创建动态库:

1
2
$ gcc -fPIC -c hello.c 
$ gcc -fPIC -shared libhello.so.1.2 hello.o # 要用"-fPIC"内容无关代码这一选项,必须在生成hello.o文件时也要指定该选项

编译时链接动态库,假设动态库放在当前目录(.)下:

1
$ gcc -o hello main.c -L. -lhello

注意:-lhello放置的位置很重要,理由和静态库部分一样。

执行:

1
$ ./hello

发现再次报错:

1
$ ./hello: error while loading shared libraries: libhello.so.1.2: cannot open shared object file: No such file or directory

根据错误提示可知:在运行时未能找到动态库libhello.so.1.2。你或许会问,编译都成功了,说明链接器确实找到了所依赖的动态库的位置,为什么在运行时却又找不到了呢?

这是因为:可执行文件在被装载运行时,会在系统默认的相关路径下去查找所依赖的动态库文件,如/usr/lib/lib等目录。若能在这些路径下找到库,则将其装载,程序成功运行。否则就将出现上述错误导致程序终止。

解决方案:配置环境变量LD_LIBRARY_PATH,用于指定运行时的动态库的实际路径,使得程序在运行时能够找到并装载动态库。

1
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:. # 在当前终端中临时配置环境变量,新增当前目录(即".")为动态库运行时搜索位置

这里其实还有另外一个环境变量LIBRARY_PATH,用于指定编译链接时的动态库的实际路径,效果类似于后文马上要介绍的GCC参数-Ldir,但两者同时存在时,会优先搜索-Ldir指定的路径。

此外,跟动态库搜路路径相关的还有一个配置文件/etc/ld.conf,也会指定一些库搜索路径,在Ubuntu 16.04.3系统下,该文件初始内容为:

1
include /etc/ld.so.conf.d/*.conf

进入到目录/etc/ld.so.conf.d下,发现有如下5个.conf文件:

1
2
3
4
5
fakeroot-x86_64-linux-gnu.conf # /usr/lib/x86_64-linux-gnu/libfakeroot
libc.conf # /usr/local/lib
x86_64-linux-gnu.conf # /lib/x86_64-linux-gnu 和 /usr/lib/x86_64-linux-gnu
x86_64-linux-gnu_EGL.conf -> /etc/alternatives/x86_64-linux-gnu_egl_conf
x86_64-linux-gnu_GL.conf -> /etc/alternatives/x86_64-linux-gnu_gl_conf

编辑/etc/ld.so.conf文件,在其中增加一行本机中boost库所在路径,如下:

1
2
/include /etc/ld.so.conf.d/*.conf
/usr/local/boost-1.55/lib

并执行:

1
sudo ldconfig # 根据配置文件/etc/ld.so.conf内容更新动态链接库搜索路径

即可新增该路径为链接器ld的动态库的搜索路径,这样依赖于boost的可执行文件在运行就可定位到其动态库,装载后成功运行。

至于环境变量LD_LIBRARY_PATH与配置文件/etc/ld.so.conf之间的关系,如是同时生效先后加载还是存在覆盖,暂时没有查到太权威的资料,因为本人就曾遇到过配置了LD_LIBRARY_PATH环境变量,并且ldd已经准确定位到某可执行文件所依赖的动态链接库的位置,但在运行该可执行文件时依然报错:

1
$ ./netsight: error while loading shared libraries: libboost_thread-mt.so.1.55.0: cannot open shared object file: No such file or directory

后来我将该boost库的路径/usr/local/boost-1.55/lib增加到配置文件/etc/ld.so.conf中,执行sudo ldconfig后,再次运行才成功。所以友情提示大家,凡是遇到运行时找不到动态库的问题时,最直接也是最有效的方法就是新增库搜索路径到配置文件/etc/ld.so.conf,一定记得最后sudo ldconfig一下。

注:在同名静态库和动态库同时存在时,GCC编译时默认优先链接动态库,但可通过-static选项强制其链接静态库。

GCC中与链接器ld相关的参数,还有一些参数也非常重要:

1
2
-rpath=dir,增加运行时库的搜索路径,即动态库的搜索路径,在编译链接和运行时均有效。
-rpath-link=dir,增加运行时库的搜索路径,即动态库的搜索路径,仅在编译链接时有效。

链接器ld会按照下列顺序搜索所需动态库:

  1. 由”-rpath-link=dir”指定的目录dir
  2. 由”-rpath=dir”指定的目录dir
  3. 由环境变量LD_RUN_PATH指定的目录
  4. 由环境变量LD_LIBRARY_PATH指定的目录
  5. 由环境变量DT_RUNPATH或DT_RPATH指定的目录
  6. 默认搜索路径,通常为/lib和/usr/lib
  7. /etc/ld.so.conf中指定的目录

库文件搜索路径


GCC默认的动态库搜索路径SEARCH_DIR可以通过ld –verbose命令查看。

GCC在编译文件时通过如下参数可新增库的搜索路径:

1
2
-Ldir:链接时在目录dir中查找由"-l"参数指定的库
-llibray:链接时搜索名称为library的库,该选项在编译整个命令中的位置会表现出不同(make difference),链接器ld会按照其书写的顺序搜索和处理库和目标文件(*.o)

使用实例参考上一节中在编译main.c新增libhello.alibhello.so.1.2库的路径:

1
$ gcc -o hello main.c -L. -lhello

那么我们如何知道一个可执行文件运行时依赖哪些库呢?
可以使用ldd命令,它可以查看一个可执行程序依赖的共享库,该命令不仅会输出共享库的名称,还有共享库在系统中的所在路径(也是运行时的动态库搜索路径)和在可执行文件(ELF)中的地址(0x0000xxxxxxxxxxxx,16x4=64位内存地址),对C++的标准库执行该命令,会发现其依赖于C标准库libc.so(即glibc)。

头文件搜索路径


在C/C++程序中,头文件(.h)即对外暴露的API接口,使用者通过在程序开始处#include头文件,即可使用头文件中声明的类与方法,而头文件中仅有类和方法的声明,定义则通常在打包在库中的源文件*.c*.cpp*.cc中。

GCC头文件搜索路径相关参数:

1
2
3
-I dir,增加头文件搜索路径dir,"-I"指定的目录会在系统默认搜索路径之前被搜索
-Idir,增加头文件搜索路径dir,是头文件搜索的第一顺位选择,多个"-I"参数则按照从左到右的顺序依次进行搜索
-isystem dir,增加头文件搜索顺序dir,顺序排在在"-I"之后,但是在系统默认搜索路径之前

-I dir是”Options Controlling the Preprocessor”,而-Idir是”Options for Directory Search”,效果等同。

相关环境变量则有C_INCLUDE_PATHCPLUS_INCLUDE_PATH,也可以通过配置这两个变量新增头文件搜索路径,使得C和C++源程序中能够找到其include的头文件。

注:用GCC参数选项指定的目录会在由环境变量指定的目录之前被优先搜索。什么参数都不指定的时候,GCC会优先搜索当前工作目录,以查找头文件和库。

参考资料


[1]Linux 动态库与静态库
[2]LIBRARY_PATH与LD_LIBRARY_PATH的区别
[3]ld Man Page
[4]GCC Environment Variables
[5]LD_LIBRARY_PATH VS LIBRARY_PATH - StackOverFlow
[6]GCC指定头文件和库文件搜索路径
[7]C++动态库与静态库

No comments:

Post a Comment