为什么g++在动态链接时会检测到未定义的引用

Why does g++ detect undefined reference when dynamically linking

我可能搞错了动态 linking 的工作原理,因为我无法理解这一点。据我了解,当一个库被动态 linked 时,它的符号在运行时被解析。来自 this answer:

When you link dynamically, a pointer to the file being linked in (the file name of the file, for example) is included in the executable and the contents of said file are not included at link time. It's only when you later run the executable that these dynamically linked files are bought in and they're only bought into the in-memory copy of the executable, not the one on disk.

[...]

In the dynamic case, the main program is linked with the C runtime import library (something which declares what's in the dynamic library but doesn't actually define it). This allows the linker to link even though the actual code is missing.

Then, at runtime, the operating system loader does a late linking of the main program with the C runtime DLL (dynamic link library or shared library or other nomenclature).

我很困惑为什么 g++ 似乎期望共享对象在动态 link 反对它时存在。当然,我希望库的名称是必要的,以便它可以在运行时加载,但为什么在这个阶段需要 .so 呢?此外,g++ 在对库进行 link 时抱怨未定义的引用。

我的问题是:

  1. 如果仅在运行时加载库,为什么 g++ 在动态 link 时似乎需要共享对象?我了解 -l 标志如何可能需要指定共享对象的名称,以便它可以在运行时加载,但我认为不必提供 .so 的路径在 link 时间 (-L) 或 .so 本身。
  2. 为什么 g++ 在动态 linking 时尝试解析符号?没有什么能阻止我在 link 时获得完整的 .so,然后在运行时提供不同的(不完整的).so,这会导致程序在尝试使用未定义的符号时崩溃。

我做了一个可重现的例子:

目录结构:

.
├── main.cpp
└── test
    ├── usertest.cpp
    └── usertest.h

文件内容:

test/usertest.h

#ifndef USERTEST_H_4AD3C656_8109_11E8_BED5_5BE6E678B346
#define USERTEST_H_4AD3C656_8109_11E8_BED5_5BE6E678B346

namespace usertest
{
    void helloWorld();

    // This method is not defined anywhere
    void byeWorld();
};

#endif /* USERTEST_H_4AD3C656_8109_11E8_BED5_5BE6E678B346 */

test/usertest.cpp

#include "usertest.h"
#include <iostream>

void usertest::helloWorld()
{
    std::cout << "Hello, world\n";
}

main.cpp

#include "test/usertest.h"

int main()
{
    usertest::helloWorld();
    usertest::byeWorld();
}

用法

$ cd test
$ g++ -c -fPIC usertest.cpp
$ g++ usertest.o -shared -o libusertest.so
$ cd ..
$ g++ main.cpp -L test/ -lusertest
$ LD_LIBRARY_PATH="test" ./a.out

预期行为

我希望在尝试启动 a.out 时一切都会崩溃,因为它无法在 libusertest.so.

中找到必要的符号

实际行为

a.out 的构建在 link 时失败,因为找不到 byeWorld():

/tmp/ccVNcRRY.o: In function `main':
main.cpp:(.text+0xa): undefined reference to `usertest::byeWorld()'
collect2: error: ld returned 1 exit status

使用 ELF 格式确实不需要知道哪些符号属于哪个库,因为实际的符号解析发生在程序执行时。按照惯例,尽管 ld 在生成二进制文件时仍会解析符号。这是为了您的方便,当您缺少符号时,您会立即得到反馈,因为在这种情况下,您的程序很可能无法运行。

使用 --warn-unresolved-symbols 标志,您可以将这种情况下的 ld 行为从错误更改为警告:

$ g++ -Wl,--warn-unresolved-symbols main.cpp -lusertest

应该发出警告但仍会创建可执行文件。请注意,您仍然需要提供库名称,否则 ld 将不知道在哪里寻找所需的符号。

在 Windows,链接器需要确切地知道哪个符号属于哪个库,以便生成必要的导入表。所以不可能构建带有未解析符号的PE二进制文件。

作为一种安全措施,executable 的代码段始终是只读的,因此您不能让程序在 运行 时修改自己的代码。正如其他人所提到的,linker 正在做的是生成每个库提供的符号列表。

你建议这个过程可以推迟到 运行 时间,但这意味着如果你在 link 时间提供的库列表不完整,你的二进制文件可能会在你每次启动它时崩溃.当您可以在 link 时间简单地检查时,为什么要冒险呢? 将符号解析推迟到 运行 时间意味着每次你 运行 你的程序都会在所有未解析的符号的所有依赖项中执行相同的搜索。 此外,如果您不必在 link 时提供库列表,则意味着它必须在 运行 时尝试 所有 可能的库.您将如何解析由多个库定义的符号?

据我所知(以一种非常简单的方式),动态 linker 在 运行 时所做的是保留一个哈希 table 将这些符号转换为地址(函数指针)在动态 linked 库中映射到您的程序地址 space 后。在您的 executable 中,linker 需要知道哪个库提供每个符号(函数、变量等)来执行此解析。

因此,在这个非常简化的解释中,您对 usertest::helloWorld(); 的调用被转换为类似 dynamic_resolve("usertest::helloWorld", "libusertest.so")(); 的内容,其中 dynamic_resolve 接收符号名称和库名称,以及 returning 一个函数指针。在内部,dynamic_resolve(虚构的名字)正在做的是加载库 "libusertest.so",检索库中函数的地址,将其缓存在哈希 table 中,然后 return函数指针。它可能正在使用 these 系统调用。第一次调用后,由于结果缓存在哈希 table 中并且库已经加载,所有后续调用都便宜得多。