cpp:usr/bin/ld:无法找到-l<nameOfTheLibrary> 的库

17
我创建了一个cpp项目,使用了一个名为libblpapi3_64.so的lib文件。 这个文件来自我从互联网上下载的一个库。
我的项目运行没有任何错误,所以我将它更新到了Bitbucket。 然后,我的同事在他自己的电脑上下载并运行它。但是他遇到了一个错误: usr/bin/ld: 无法找到 -lblpapi3_64
实际上,我已经将它复制到了我的项目仓库中。我的意思是,在我的项目下创建了一个名为“lib”的文件夹,我使用的所有lib文件都在其中。
也有其他的lib文件,比如liblog4cpp.a,但它们都没有问题。只有libblpapi3_64.so会出现这个错误。
是因为它是一个.so文件而不是.a文件吗?还是有其他原因?
另外,libblpapi3_64.so的文件名是green,而其他文件(.a)的文件名则是white。我认为这不是链接文件,而是原始文件。
2个回答

55
简言之,ld 不知道你的项目库位于何处。你需要将其放置在已知的目录中或通过 -L 参数指定链接器的库的完整路径。
要构建程序,你需要在 /bin/ld 的搜索路径中拥有你的库以及你的同事也需要拥有。为什么呢?请看详细回答。
首先,我们应该了解工具的作用:
1. 编译器生成带有未解析符号的简单对象文件(它在运行时并不太关心符号)。 2. 链接器将多个对象和存档文件组合起来,重新定位其数据,并将符号引用绑定到单个文件中:可执行文件或库。
让我们从一个例子开始。例如,你有一个由 3 个文件组成的项目:main.cfunc.hfunc.cmain.c:
#include "func.h"
int main() {
    func();
    return 0;
}

func.h

void func();

func.c

#include "func.h"
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,我们可以在目标文件中看到它。但是我们还看到了一个标记为未解决的Ufunc符号,因此我们无法看到它的地址偏移量。
为了解决这个问题,我们必须使用链接器。它将获取所有的目标文件并解析所有这些符号(例如我们示例中的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工具创建的单个符号表。

编译器在编译对象文件时不会解析符号。这些符号将由链接器替换为地址。因此,解析符号可以通过两种方式来完成:链接器动态加载器

  1. 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.

  2. 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部分,因为libclibstdc++库都是共享的,并且编译器默认将任何项目动态链接到它们。您也可以将它们静态链接,但这会增加总可执行文件大小。因此,如果找不到共享库,则您的符号将保持未解析状态,并且您将无法运行应用程序,因此它无法生成可执行文件。您可以通过以下方式获取通常搜索库的目录列表:
  1. 在编译器参数中向链接器传递命令。
  2. 通过解析ld --verbose的输出。
  3. 通过解析ldconfig的输出。
其中一些方法在此处有解释。 动态加载器通过使用以下内容来查找所有库:
  1. ELF文件的DT_RPATH动态部分。
  2. 可执行文件的DT_RUNPATH部分。
  3. LD_LIBRARY_PATH环境变量。
  4. /etc/ld.so.cache-自己的缓存文件,其中包含以前在增强库路径中找到的候选库的编译列表。
  5. 默认路径:在默认路径/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.1libtest.so.1.1libtest.so.1.1.1)以及一个libtest.so文件,但是为了运行您的应用程序,您只需要列出前三个版本化库文件。这也解释了为什么Debian或rpm软件包有单独的devel软件包:普通软件包(仅由已编译应用程序所需的文件组成,以便运行它们)包含3个版本化库文件和一个devel软件包,该软件包仅具有符号链接文件,使得编译项目成为可能。 总结 在做完以上步骤之后:
  1. 您、您的同事和您的应用程序代码的每个用户都必须在其系统链接器路径中拥有所有库,以便能够编译(构建您的应用程序)。否则,他们必须更改Makefile(或编译命令),通过添加-L<somePathToTheSharedLibrary>作为参数来添加共享库位置目录。
  2. 在成功构建后,您还需要再次使用您的库才能运行程序。动态加载器(ld-linux)将搜索您的库,因此它需要在其路径(请参见上文)或系统链接器路径中。在大多数Linux程序发行版中,例如来自Steam的游戏,都有一个shell脚本,该脚本设置LD_LIBRARY_PATH变量,该变量指向游戏所需的所有共享库。

1
你的意思是-L(-I 是用于包含路径) - CristiFati
非常感谢。事实上,这是我第一次在Linux下工作。我的想法是,我的同事从Bitbucket下载我的项目,然后他可以立即使用它。所以当我编码时,我创建一个文件并将所有的lib文件放在其中。我的IDE是Netbeans,我右键单击我的项目,就可以在那个文件中添加lib文件。之后,我更新整个项目到Bitbucket。当我的同事下载并导入它到他的Netbeans时,我可以看到所有的lib文件都已经配置好了——就像我之前做的一样。所以你是告诉我,我所做的不起作用? - Yves
换句话说,我的同事是否可以立即编译它而无需任何配置? - Yves
这取决于你的同事是否在他的库路径中有这个库。我很快会相应地修改答案。 - Victor Polevoy

0
您可以查看我们的 Rblapi 包,该包也使用了这个库。
“如何使库可见”的基本问题实际上有两个答案:
  1. 使用 ld.so。最简单的方法是将 blpapi3_64.so 复制到 /usr/local/lib。然后如果调用 ldconfig 来更新缓存,你就应该准备好了。你可以通过 ldconfig -p | grep blpapi 进行测试,它应该会显示它。
  2. 在构建应用程序时使用 rpath 指令;这基本上编码了路径,并使您独立于 ld.so

非常感谢。事实上,这是我第一次在Linux下工作。我的想法是,我的同事从Bitbucket下载我的项目,然后他可以立即使用它。所以当我编码时,我创建一个文件并将所有的lib文件放在其中。我的IDE是Netbeans,我右键单击我的项目,就可以在那个文件中添加lib文件。之后,我更新整个项目到Bitbucket。当我的同事下载并导入它到他的Netbeans时,我可以看到所有的lib文件都已经配置好了——就像我之前做的一样。所以你是告诉我,我所做的不起作用? - Yves
换句话说,我的同事是否可以立即编译它而无需任何配置? - Yves
是的,如果你知道你在做什么,你可以实现这个。例如,我们的Rblpapi包就完全是这样工作的。 - Dirk Eddelbuettel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接