简言之,
ld
不知道你的项目库位于何处。你需要将其放置在已知的目录中或通过
-L
参数指定链接器的库的完整路径。
要构建程序,你需要在
/bin/ld
的搜索路径中拥有你的库以及你的同事也需要拥有。为什么呢?请看详细回答。
首先,我们应该了解工具的作用:
1. 编译器生成带有未解析符号的简单对象文件(它在运行时并不太关心符号)。
2. 链接器将多个对象和存档文件组合起来,重新定位其数据,并将符号引用绑定到单个文件中:可执行文件或库。
让我们从一个例子开始。例如,你有一个由 3 个文件组成的项目:
main.c
、
func.h
和
func.c
。
main.c:
#include "func.h"
int main() {
func();
return 0;
}
func.h
void func();
func.c
void func() { }
当你将源代码(main.c)编译成目标文件(main.o)时,由于存在未解决的符号,因此它尚不能运行。让我们从“生成可执行文件”的工作流程开始(不包括细节):
在预处理器完成其工作后,会生成以下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的方法编译两个源文件,这将生成2个目标文件: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
,我们可以在目标文件中看到它。但是我们还看到了一个标记为未解决的
U
的
func
符号,因此我们无法看到它的地址偏移量。
为了解决这个问题,我们必须使用链接器。它将获取所有的目标文件并解析所有这些符号(例如我们示例中的
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/
sudo ln -s libfunc.so.1.5.0 /usr/lib/libfunc.so.1
sudo ln -s libfunc.so.1 /usr/lib/libfunc.so
让我们的项目依赖于共享库,通过在编译和静态链接过程中保留func()
符号未解决,创建一个可执行文件并将其(动态)链接到我们的共享库(libfunc
):
cc main.c -lfunc
现在,如果我们在符号表中查找该符号,我们仍然无法解决我们的符号:
$ nm a.out | grep fun
U func
现在,这已经不再是一个问题了,因为每个程序启动前都会由动态加载器解析func
符号。好的,现在让我们回到理论上。
事实上,库只是使用ar
工具放置到单个存档中的对象文件,并通过ranlib
工具创建的单个符号表。
编译器在编译对象文件时不会解析符号
。这些符号将由链接器替换为地址。因此,解析符号可以通过两种方式来完成:链接器
和动态加载器
:
The linker: ld
, does 2 jobs:
a) For static libs or simple object files, this linker changes external symbols in the object files to the addresses of the real entities. For example, if we use C++ name mangling linker will change _ZNK3MapI10StringName3RefI8GDScriptE10ComparatorIS0_E16DefaultAllocatorE3hasERKS0_
to 0x07f4123f0
.
b) For dynamic libs it only checks if the symbols can be resolved (you try to link with correct library) at all but does not replace the symbols by address. If symbols can't be resolved (for example they are not implemented in the shared library you are linking to) - it throws undefined reference to
error and breaks up the building process because you try to use these symbols but linker can't find such symbol in it's object files which it is processing at this time. Otherwise, this linker adds some information to the ELF
executable which is:
i. .interp
section - request for an interpreter
- dynamic loader to be called before executing, so this section just contains a path to the dynamic loader. If you look at your executable which depends on shared library (libfunc
) for example you will see the interp section $ readelf -l a.out
:
INTERP 0x0000000000000238 0x0000000000400238 0x0000000000400238
0x000000000000001c 0x000000000000001c R 1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
ii. .dynamic
section - a list of shared libraries which interpreter
will be looking for before executing. You may see them by ldd
or 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]
Note that ldd
also finds all the libraries in your filesystem while readelf only shows what libraries does your program need. So, all of these libraries will be searched by dynamic loader (next paragraph).
The linker works at build time.
Dynamic loader: ld.so
or ld-linux
. It finds and loads all the shared libraries needed by a program (if they were not loaded before), resolves the symbols by replacing them to real addresses right before the start of the program, prepares the program to run, and then runs it. It works after the build and before running the program. Less speaking, dynamic linking means resolving symbols in your executable before each program start.
实际上,当您运行具有
.interp
部分(它需要加载一些共享库)的
ELF
可执行文件时,操作系统(Linux)首先运行解释器而不是您的程序。否则,您会遇到未定义的行为-您的程序中有符号,但它们没有地址定义,这通常意味着该程序将无法正常工作。
您也可以自己运行动态加载器,但这是不必要的(32位架构elf的二进制文件为
/lib/ld-linux.so.2
,64位架构elf的二进制文件为
/lib64/ld-linux-x86-64.so.2
)。
为什么链接器在您的情况下声称找不到
/usr/bin/ld: cannot find -lblpapi3_64
?因为它尝试在已知路径中查找所有库。如果它在运行时加载库,为什么要搜索库?因为它需要检查是否可以通过此库解析所有所需的符号,并将其名称放入
.dynamic
部分以供动态加载器使用。实际上,几乎每个c/c++ elf都存在
.interp
部分,因为
libc
和
libstdc++
库都是共享的,并且编译器默认将任何项目动态链接到它们。您也可以将它们静态链接,但这会增加总可执行文件大小。因此,如果找不到共享库,则您的符号将保持未解析状态,并且您将无法运行应用程序,因此它无法生成可执行文件。您可以通过以下方式获取通常搜索库的目录列表:
- 在编译器参数中向链接器传递命令。
- 通过解析
ld --verbose
的输出。
- 通过解析
ldconfig
的输出。
其中一些方法在
此处有解释。
动态加载器通过使用以下内容来查找所有库:
- ELF文件的
DT_RPATH
动态部分。
- 可执行文件的
DT_RUNPATH
部分。
LD_LIBRARY_PATH
环境变量。
/etc/ld.so.cache
-自己的缓存文件,其中包含以前在增强库路径中找到的候选库的编译列表。
- 默认路径:在默认路径/lib中,然后是/usr/lib。如果二进制文件使用
-z nodeflib
链接器选项链接,则跳过此步骤。
ld-linux搜索算法。
此外,请注意,如果我们谈论的是共享库,则它们的命名不是
.so
,而是
.so.version
的格式。当您构建应用程序时,链接器将查找
.so
文件(通常是指向
.so.version
的符号链接),但当您运行应用程序时,动态加载器会查找
.so.version
文件。例如,假设我们有一个库
test
,其版本为
1.1.1
,根据
semver。在文件系统中,它看起来像:
/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
文件,但是为了运行您的应用程序,您只需要列出前三个版本化库文件。这也解释了为什么Debian或rpm软件包有单独的
devel
软件包:普通软件包(仅由已编译应用程序所需的文件组成,以便运行它们)包含3个版本化库文件和一个devel软件包,该软件包仅具有符号链接文件,使得编译项目成为可能。
总结
在做完以上步骤之后:
- 您、您的同事和您的应用程序代码的每个用户都必须在其系统链接器路径中拥有所有库,以便能够编译(构建您的应用程序)。否则,他们必须更改Makefile(或编译命令),通过添加
-L<somePathToTheSharedLibrary>
作为参数来添加共享库位置目录。
- 在成功构建后,您还需要再次使用您的库才能运行程序。动态加载器(
ld-linux
)将搜索您的库,因此它需要在其路径(请参见上文)或系统链接器路径中。在大多数Linux程序发行版中,例如来自Steam的游戏,都有一个shell脚本,该脚本设置LD_LIBRARY_PATH
变量,该变量指向游戏所需的所有共享库。