简述:
ld 不知道您的项目库的位置。您必须将其放入 ld 的已知目录或通过链接器的-L 参数指定库的完整路径。
为了能够构建您的程序,您需要将您的库放在/bin/ld 搜索路径中,并且还需要您的同事。为什么?查看详细答案。
详细说明:
首先,我们应该了解什么工具做什么:
- 编译器生成简单的
object files 带有未解析的符号(它在运行时不太关心符号)。
- 链接器将多个
object 和archive files 组合在一起,重新定位它们的数据并将符号引用绑定到一个文件中:可执行文件或库。
让我们从一些例子开始。例如,您有一个包含 3 个文件的项目:main.c、func.h 和 func.c。
main.c
#include "func.h"
int main() {
func();
return 0;
}
func.h
void func();
func.c
#include "func.h"
void func() { }
因此,当您将源代码 (main.c) 编译成目标文件 (main.o) 时,它还不能运行,因为它有未解析的符号。让我们从producing an executable工作流的开头开始(不详):
预处理器在其工作后产生以下main.c.preprocessed:
void func();
int main() {
func();
return 0;
}
还有以下func.c.preprocessed:
void func();
void func() { }
正如您在main.c.preprocessed 中看到的,与您的func.c 文件和void func() 的实现没有任何联系,编译器根本不知道它,它单独编译所有源文件。所以,为了能够编译这个项目,你必须使用cc -c main.c -o main.o和cc -c func.c -o func.o之类的东西来编译这两个源文件,这将产生两个目标文件,main.o和func.o。 func.o 已解析所有符号,因为它只有一个函数,其主体写在 func.c 内,但 main.o 尚未解析 func 符号,因为它不知道它在哪里实现。
让我们看看func.o里面有什么:
$ nm func.o
0000000000000000 T func
简单地说,它包含一个在文本代码部分中的符号,所以这是我们的func 函数。
让我们看看main.o:
$ nm main.o
U func
0000000000000000 T main
我们的main.o 有一个实现和解析的静态函数main,我们可以在目标文件中看到它。但是我们也看到func符号标记为未解析U,因此我们无法看到它的地址偏移量。
为了解决这个问题,我们必须使用链接器。它将获取所有目标文件并解析所有这些符号(在我们的示例中为void func();)。如果链接器无法做到这一点,它会抛出类似unresolved external symbol:void func() 的错误。如果您不将func.o 目标文件提供给链接器,则可能会发生这种情况。所以,让我们将我们拥有的所有目标文件提供给链接器:
ld main.o func.o -o test
链接器将通过main.o,然后通过func.o,尝试解析符号,如果没问题 - 将其输出到test 文件。如果我们查看生成的输出,我们将看到所有符号都已解析:
$ nm test
0000000000601000 R __bss_start
0000000000601000 R _edata
0000000000601000 R _end
00000000004000b0 T func
00000000004000b7 T main
到这里我们的工作就完成了。让我们看看动态(共享)库的情况。让我们从 func.c 源文件创建一个共享库:
gcc -c func.c -o func.o
gcc -shared -fPIC -Wl,-soname,libfunc.so.1 -o libfunc.so.1.5.0 func.o
瞧,我们有它。现在,让我们把它放到已知的动态链接库路径/usr/lib/:
sudo mv libfunc.so.1.5.0 /usr/lib/ # to make program be able to run
sudo ln -s libfunc.so.1.5.0 /usr/lib/libfunc.so.1 #creating symlink for the program to run
sudo ln -s libfunc.so.1 /usr/lib/libfunc.so # to make compilation possible
让我们的项目依赖于该共享库,方法是在编译和静态链接过程之后留下未解析的 func() 符号,创建可执行文件并将其(动态)链接到我们的共享库 (libfunc):
cc main.c -lfunc
现在,如果我们在其符号表中查找该符号,我们的符号仍然未解析:
$ nm a.out | grep fun
U func
但这不再是问题了,因为func 符号将在每个程序启动之前由动态加载器解析。好的,现在让我们回到理论。
实际上,库只是使用ar 工具和ranlib 工具创建的单个符号表放入单个存档中的目标文件。
编译器在编译目标文件时无法解析symbols。这些符号将被链接器替换为地址。所以解析符号可以通过两件事来完成:the linker 和dynamic loader:
-
链接器:ld,做 2 个工作:
a) 对于静态库或简单目标文件,此链接器将目标文件中的外部符号更改为真实实体的地址。例如,如果我们使用 C++ 名称修饰,链接器会将 _ZNK3MapI10StringName3RefI8GDScriptE10ComparatorIS0_E16DefaultAllocatorE3hasERKS0_ 更改为 0x07f4123f0。
b) 对于动态库,它仅检查符号是否可以解析(您尝试链接到正确的库),但不会用地址替换符号.如果符号无法解析(例如它们未在您链接到的共享库中实现) - 它会抛出 undefined reference to 错误并中断构建过程,因为您尝试使用这些符号但链接器找不到这样此时它正在处理的目标文件中的符号。否则,此链接器会向 ELF 可执行文件添加一些信息,即:
我。 .interp 部分 - 请求 interpreter - 在执行之前调用动态加载器,因此该部分仅包含动态加载器的路径。例如,如果您查看依赖于共享库 (libfunc) 的可执行文件,您将看到 interp 部分 $ readelf -l a.out:
INTERP 0x0000000000000238 0x0000000000400238 0x0000000000400238
0x000000000000001c 0x000000000000001c R 1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
二。 .dynamic 部分 - interpreter 将在执行之前查找的共享库列表。您可以通过ldd 或readelf 看到它们:
$ ldd a.out
linux-vdso.so.1 => (0x00007ffd577dc000)
libfunc.so.1 => /usr/lib/libfunc.so.1 (0x00007fc629eca000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fefe148a000)
/lib64/ld-linux-x86-64.so.2 (0x000055747925e000)
$ readelf -d a.out
Dynamic section at offset 0xe18 contains 25 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libfunc.so.1]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
请注意,ldd 还可以找到您文件系统中的所有库,而 readelf 仅显示您的程序需要哪些库。因此,所有这些库都将被动态加载器搜索(下一段)。
链接器在构建时工作。
动态加载程序:ld.so 或 ld-linux。它查找并加载程序所需的所有共享库(如果之前未加载),通过在程序启动之前将符号替换为真实地址来解析符号,准备程序运行,然后运行它。它在构建之后和运行程序之前工作。简单地说,动态链接意味着在每个程序启动之前解析可执行文件中的符号。
实际上,当您运行带有.interp 部分的ELF 可执行文件(它需要加载一些共享库)时,操作系统(Linux)首先运行的是解释器,而不是您的程序。否则你有一个未定义的行为 - 你的程序中有符号但它们不是由地址定义的,这通常意味着程序将无法正常工作。
您也可以自己运行动态加载程序,但这不是必需的(二进制是 /lib/ld-linux.so.2 用于 32 位架构精灵,/lib64/ld-linux-x86-64.so.2 用于 64 位架构精灵)。
为什么链接器在您的情况下声称/usr/bin/ld: cannot find -lblpapi3_64?因为它试图找到它已知路径中的所有库。如果它将在运行时加载,为什么它会搜索库?因为它需要检查该库是否可以解析所有需要的符号,并将其名称放入动态加载器的.dynamic 部分。实际上,.interp 部分几乎存在于每个 c/c++ 精灵中,因为 libc 和 libstdc++ 库都是共享的,编译器默认将任何项目动态链接到它们。您也可以静态链接它们,但这会扩大可执行文件的总大小。因此,如果找不到共享库,您的符号将保持未解析,并且您将 UNABLE 运行您的应用程序,因此它无法生成可执行文件。您可能会获得通常通过以下方式搜索库的目录列表:
- 在编译器参数中将命令传递给链接器。
- 通过解析
ld --verbose的输出。
- 通过解析
ldconfig的输出。
其中一些方法在here进行了解释。
动态加载器尝试使用以下方法查找所有库:
-
DT_RPATH ELF 文件的动态部分。
-
可执行文件的
DT_RUNPATH 部分。
-
LD_LIBRARY_PATH 环境变量。
-
/etc/ld.so.cache - 自己的缓存文件,其中包含先前在扩充库路径中找到的候选库的编译列表。
- 默认路径:在默认路径 /lib 中,然后是 /usr/lib。如果二进制文件使用
-z nodeflib 链接器选项链接,则跳过此步骤。
ld-linux search algorithm
另外,请注意,如果我们谈论共享库,它们的名称不是.so,而是.so.version 格式。当您构建您的应用程序时,链接器将查找.so 文件(通常是指向.so.version 的符号链接),但是当您运行您的应用程序时,动态加载程序会查找.so.version 文件。例如,假设我们有一个库test,根据semver,它的版本是1.1.1。在文件系统中它看起来像:
/usr/lib/libtest.so -> /usr/lib/libtest.so.1.1.1
/usr/lib/libtest.so.1 -> /usr/lib/libtest.so.1.1.1
/usr/lib/libtest.so.1.1 -> /usr/lib/libtest.so.1.1.1
/usr/lib/libtest.so.1.1.1
因此,为了能够编译,您必须拥有所有版本化文件(libtest.so.1、libtest.so.1.1 和 libtest.so.1.1.1)和一个 libtest.so 文件,但要运行您的应用程序,您必须只列出 3 个版本化库文件第一的。这也解释了为什么 Debian 或 rpm 软件包分别具有 devel-packages:普通的一个(仅包含已编译的应用程序运行它们所需的文件),它有 3 个版本库文件和一个只有符号链接文件的开发包使编译项目成为可能。
简历
毕竟:
- 您、您的同事和您的应用程序代码的 EACH 用户必须在其系统链接器路径中拥有所有库才能编译(构建您的应用程序)。否则,他们必须更改 Makefile(或编译命令)以通过添加
-L<somePathToTheSharedLibrary> 作为参数来添加共享库位置目录。
- 成功构建后,您还需要再次使用库才能运行程序。您的库将被动态加载器 (
ld-linux) 搜索,因此它需要位于 它的路径(见上文)或系统链接器路径中。在大多数 Linux 程序发行版中,例如来自 steam 的游戏,都有一个 shell 脚本设置 LD_LIBRARY_PATH 变量,该变量指向游戏所需的所有共享库。