搜索头文件

在 C 语言中使用到函数库的时候,涉及到了库内符号的引用(也就是对头文件的 include )和对库(也就是对于 .a 或者 .so 文件)的链接。

GCC 对头文件的搜索有 3 个层次,按照优先级高低可以分为:

  1. -I 指定的目录下搜索。
  2. 在环境变量 C_INCLUDE_PATH 中包含的目录下搜索。
  3. 在 gcc 默认的搜索路径下搜索。

需要强调的是,这种只是会搜索指定路径下的头文件,并不会递归搜索子文件下的头文件(所以为什么要叫做搜索,应该直接叫做指定)。所以如果涉及了子文件夹,那么应当在头文件中有所显示,比如说我想引用 /usr/include/openssl/aes.h 这个头文件文件下的内容,而在编译时的选项是 -I/usr/include/ ,那么我在源码中就应该这样书写

#include <openssl/aes.h>

但是如果我的编译选项是 -I/usr/include/openssl/ ,那么在源码中只需要这样写:

#include <ase.h>

如果我们希望查看 gcc 默认了哪些头文件搜索路径,可以使用如下命令,这个命令执行了预编译过程,所以可以看出有哪些文件被包括进去了:

gcc -xc -E -v -

在我电脑上的是这样的:

/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include
/usr/local/include
/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/include-fixed
/usr/include

可以说非常合理,存在 /usr/include/, /usr/local/include 两个常见的头文件文件夹,同时还有两个和编译器相关的文件夹。

此外,如果在源码中使用如下 include 格式:

#include "headerfile.h"

那么在前面三个层次之上,还会增加一个层级,就是在“源文件当前目录”搜索头文件,而且这种搜索层次的优先级最高。

链接库

库的搜索逻辑与头文件的类似,也是有 3 个层次:

  1. -L 指定的目录下搜索。
  2. 在环境变量 LIBRARY_PATH 中包含的目录下搜索。
  3. 在 gcc 默认的搜索路径下搜索。

其中 gcc 的默认路径可以使用如下命令查询:

gcc -print-search-dirs

结果也非常合理,存在 /usr/lib/, /usr/local/lib 两个常见的库文件文件夹,同时还有编译器相关的文件夹:

usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/:/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/../../../../x86_64-pc-linux-gnu/lib/x86_64-pc-linux-gnu/13.2.1/:/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/../../../../x86_64-pc-linux-gnu/lib/../lib/:/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/../../../x86_64-pc-linux-gnu/13.2.1/:/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/../../../../lib/:/lib/x86_64-pc-linux-gnu/13.2.1/:/lib/../lib/:/usr/lib/x86_64-pc-linux-gnu/13.2.1/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/../../../../x86_64-pc-linux-gnu/lib/:/usr/lib/gcc/x86_64-pc-linux-gnu/13.2.1/../../../
:/lib/:/usr/lib/

除此之外,GCC 还可以直接使用 -l 参数指定链接器链接的库,如

gcc demo.c -ldl -lpthread -lssl -lcrypto 

-l 参数会在系统标准库路径中查找一种特定格式的文件,名为 lib<library>.a 或者 lib<library>.so 。不得不说有些库的名字非常短,比如说 -lm 其实说的是一个叫做 m 的库,而不是两个编译器参数 -l, -m 连写,不要造成困扰。

头文件搜索路径和库的搜索路径都是在 gcc 编译前 ./Configure 中设定的。

静态库的制作

静态库的本质是多个 object 文件的归档文件,我们这样制作:

# 编译
gcc -c a.c -o a.o
gcc -c b.c -o b.o
# 创建静态库
ar rcs libdemo.a a.o b.o

其中 -c 表示只编译,不链接。 ar 是一个用于创建静态库, r 对于 libdemo.a 库中的同名 object 的更新, c 表示对于 libdemo.a 的创建, s 指为库文件生成一个符号索引,以加快链接过程。

链接顺序

tldr: 按照如下顺序进行链接,就一般不会出现问题:

Object 文件,高级库,基础库

需要注意的是,不仅库有链接顺序的问题,object 和库之间也有链接顺序问题(但是 object 文件之间没有顺序问题)。

链接的本质是符号与实现的对应关系。在链接中,我们会维护一个 “实现列表” 和一个 “未实现的符号列表” ,当我们加载 object 的时候,会修改这两个列表,可以想见,不同的 object 文件链接顺序并不会造成影响。即使有一个符号先于它的实现出现,那么这个符号也会被记录在“未实现的符号列表”中,那么等到读取到它的实现的时候,就可以完成对应关系了;实现先于符号出现也是同理。

出现问题的在于库文件,这是因为库文件中有多个 object 文件,而链接并不是将整个库文件里的全部 object 文件都链接上去,而是它只挑选程序会用到的那些实现所在的 object 文件进行链接。那么我们如何知道那些实现是被程序用到的呢?我们只能查看 “未实现的符号列表” ,相当于这个列表是一个 “需求表”

正因如此,为了能链接到所有应该被链接的 object 实现,所以我们要保证 “需求表” 里是完整的所有需求,正因如此,我们必须做到符号要先于实现,这样才能链接到 object 。而当实现先于符号的时候,因为没有这个需求,所以对应的实现并不会被链接。