LD链接器问题:--whole-archive选项

74
我所见到的唯一真正使用--whole-archive链接器选项的方法是从静态库创建共享库。最近,我遇到了一些使用这个选项来链接内部静态库的Makefile。当然,这会导致可执行文件不必要地引入未被引用的目标代码。我对此的反应是这是错误的,我有什么遗漏吗?
我有第二个问题与我读到的关于整个存档选项有关,但我无法完全理解。大致意思是,在链接静态库时应该使用--whole-archive选项,如果可执行文件也链接了一个共享库,并且该共享库(部分)包含与静态库相同的目标代码。也就是说,共享库和静态库在目标代码方面有重叠。使用此选项将强制解析可执行文件中的所有符号(无论使用情况如何)。这应该避免目标代码重复。这很令人困惑,如果程序中引用了一个符号,必须在链接时独特地解析它,那么这个重复的业务是什么?(如果这段话不是非常清晰,请原谅我)
谢谢。
5个回答

106

在使用静态库链接可执行文件时,--whole-archive 有其合理的用途。其中一个例子是构建C++代码,其中全局实例会在它们的构造函数中 "注册"自己(警告:未经测试的代码):

handlers.h

typedef void (*handler)(const char *data);
void register_handler(const char *protocol, handler h);
handler get_handler(const char *protocol);

handlers.cc(libhandlers.a的一部分)

typedef map<const char*, handler> HandlerMap;
HandlerMap m;
void register_handler(const char *protocol, handler h) {
   m[protocol] = h;
}
handler get_handler(const char *protocol) {
   HandlerMap::iterator it = m.find(protocol);
   if (it == m.end()) return nullptr;
   return it->second;
}

http.cc (libhttp.a 的一部分)

#include <handlers.h>
class HttpHandler {
    HttpHandler() { register_handler("http", &handle_http); }
    static void handle_http(const char *) { /* whatever */ }
};
HttpHandler h; // registers itself with main!

main.cc

#include <handlers.h>
int main(int argc, char *argv[])
{
    for (int i = 1; i < argc-1; i+= 2) {
        handler h = get_handler(argv[i]);
        if (h != nullptr) h(argv[i+1]);
    }
}

请注意,http.cc 中没有 main.cc 需要的符号。如果您将其链接为:
g++ main.cc -lhttp -lhandlers

如果你没有将http处理程序链接到主可执行文件中,那么你将无法调用handle_http()。与之相反,当你按如下方式链接时:

g++ main.cc -Wl,--whole-archive -lhttp -Wl,--no-whole-archive -lhandlers

同样的“自我注册”风格在纯C语言中也是可行的,例如使用GNU扩展__attribute__((constructor))

3
如果可以构建libhttp.a,则证明该库中存在register_handler函数。那么这个函数如何引用main.cc中的register_handler呢?因此,在这种情况下,我们必须使用其他方法来实现您的想法。 - longbkit
2
@longbkit 我已更新答案,将处理程序拆分为一个较低级别的库,因为需要这样做。我抵制了将处理器类型从C函数指针更改为C++ std :: function的诱惑。 - Arthur Tacca
有没有办法将整个存档功能定位到特定的全局注册符号,而不是整个库? - David
2
@David 如果你想提取特定的符号集,而不是整个存档,那么显然 --whole-archive 不合适。请使用 -Wl,-u,needed_symbol - Employed Russian

10

--whole-archive 的另一个合法使用场景是,用于工具包开发人员将包含多个功能的库打包成单个静态库进行分发。在这种情况下,提供者不知道使用者将使用库的哪些部分,因此必须包含所有内容。


7
使用静态库时,如果没有使用 --whole-archive 参数,它会包含所有内容。这似乎是一件毫无意义的事情。 - WinterMute
4
完全不正确。当你制作一个库(无论是静态的还是动态的),它将包含所有命名的目标文件。 - Swiss Frank

7

在处理静态库和增量链接时,--whole-archive的另一个好的应用场景是:

假设:

  1. libA 实现了 a()b() 函数。
  2. 部分程序必须仅与 libA 链接,例如使用 --wrap 进行某些函数包装(经典例子是 malloc)。
  3. libC 实现了 c() 函数并使用 a()
  4. 最终程序使用 a()c()

增量链接步骤可以是:

ld -r -o step1.o module1.o --wrap malloc --whole-archive -lA
ld -r -o step2.o step1.o module2.o --whole-archive -lC
cc step3.o module3.o -o program

未插入 --whole-archive 会剥离函数 c(),而该函数被程序 program 使用,防止正确的编译过程。
当然,这是一个特定的情况,必须进行增量链接,以避免在所有模块中包装对 malloc 的所有调用,但是这是一个成功支持 --whole-archive 的案例。

7
我认为使用“—whole-archive”构建可执行文件可能不是您想要的(因为连接了不必要的代码并且会导致软件变得臃肿)。如果他们有充分的理由这样做,他们应该在构建系统中记录下来,否则你只能猜测其中原因。
至于您问题的第二部分。如果一个可执行文件同时链接了一个静态库和一个动态库,并且这个动态库具有与静态库(部分)相同的目标代码,则“-whole-archive”将确保在链接时首选静态库中的代码。当您进行静态链接时,这通常是您所期望的结果。

使用—whole-archive的一个原因:通常用于将存档文件(.o/.a)转换为共享库,强制每个对象都包含在生成的共享库中,以便在首次编译静态库时组合多个动态库。参考:Using LD, the GNU linker - Options - Kevin Chou

3

对于你的第一个问题(“为什么”),我曾经见过在公司内部库之间使用--whole-archive,主要是为了回避这些库之间的循环引用。这通常会隐藏库的不良架构,因此我不建议使用它。但是这是一种快速测试的快捷方式。

对于你的第二个问题,如果相同的符号在共享对象和静态库中都存在,链接器将使用它首次遇到的库来满足引用。
如果共享库和静态库具有完全相同的代码共享,那么这一切可能都能正常工作。但是,如果共享库和静态库具有相同符号的不同实现,则程序仍将编译,但根据库的顺序,其行为将有所不同。

强制从静态库加载所有符号是删除混淆的一种方法,以确定从何处加载哪些内容。但总体而言,这听起来像是解决错误问题;你大多数情况下不需要在不同的库之间使用相同的符号。


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