--start-group和--whole-archive在ld中有什么区别?

5
说实话,我认为通过查看 man ld,这个问题应该很容易。然而,通过阅读 manpage 并阅读其他人编写的代码,我发现当人们认为库的顺序可能存在问题时,他们会交替使用它们或同时使用它们。
我想知道这两个选项之间的区别是什么,以及在使用它们时的最佳实践是什么。
谢谢!
相关链接:

这些是完全不同的概念。因此,将它们交替使用的人会犯错误。--whole-archive 将整个库放入链接文件中,即使是不需要的部分也会被包含进去。使用组不会将整个库放入其中,而是一直解析符号,直到在该组中找不到新的未解析符号为止。 - geza
1个回答

13
在撰写本文时,Stackoverflow关于静态库的标签wiki告诉我们:

静态库是一组目标文件的归档文件。作为链接器输入时,链接器会提取它需要进行链接的目标文件。

所需的目标文件是那些在其他输入文件中被使用但没有定义的符号提供给链接器的目标文件。 从归档文件中提取所需的目标文件,并将它们与单独的输入文件一样精确地作为链接命令中的输入文件输入到链接中。

……

链接器通常支持一个选项(GNU ld:--whole-archive,MS link:/WHOLEARCHIVE)来覆盖静态库的默认处理方式,而是链接所有包含的目标文件,无论它们是否需要。

静态库对链接除了从中提取的目标文件什么也不起作用,这些目标文件在不同的链接中可能会有所不同。 与共享库相反,共享库是另一种完全不同角色的文件。

这应该可以说明--whole-archive的作用。 --whole-archive的范围延续到链接器命令行的末尾或出现--no-whole-archive1为止。
默认情况下,链接器仅在每个静态库在链接器输入的命令行序列中出现的每个点处检查静态库。 它不会向后重新检查静态库以便解析稍后在其他输入文件中发现的符号引用。 --start-group ... --end-group选项对默认行为进行更改。它指示链接器重复按照该顺序检查提到的静态库, 只要这样做可以产生任何新的新符号引用的解析。 --start-group ... --end-group对于链接器从...的静态库中选择目标文件没有影响。 它只会提取和链接它需要的对象文件,除非--whole-archive也在起作用。
总结一下: --start-group lib0.a ... libN.a --end-group 告诉链接器:lib0.a ... libN.a中继续搜索直到不再找到对象文件--whole-archive lib0.a ... libN.a --no-whole-archive告诉链接器:忘记你需要什么。只连接 lib0.a ... libN.a 中的所有对象文件
您可以看到,任何您成功使用--start-group lib0.a ... libN.a --end-group建立的链接都将使用--whole-archive lib0.a ... libN.a --no-whole-archive成功, 因为后者将连接所有必要的对象文件和所有不必要的对象文件,而无需区分它们。
但反过来并不成立。以下是一个简单的例子: x.c
#include <stdio.h>

void x(void)
{
    puts(__func__);
}

y.c

#include <stdio.h>

void y(void)
{
    puts(__func__);
}

main.c

extern void x(void);

int main(void)
{
    x();
    return 0;
}

编译所有源文件:

$ gcc -Wall -c x.c y.c main.c

制作一个静态库,将x.oy.o打包归档:

ar rcs libxy.a x.o y.o

尝试以错误的顺序将main.olibxy.a链接到程序中:

$ gcc -o prog libxy.a main.o
main.o: In function `main':
main.c:(.text+0x5): undefined reference to `x'
collect2: error: ld returned 1 exit status

那个失败了,因为在链接器太晚才发现main.o中对x的引用,无法找到libxy.a(x.o)中x的定义。它首先到达libxy.a并没有找到需要的任何目标文件。此时,它还没有将任何目标文件链接到程序中,所以需要解析的符号引用为0。考虑了libxy.a并未使用它,因此不再考虑它。
当然,正确的链接方式是:
$ gcc -o prog main.o libxy.a

但是,如果您没有意识到链接顺序是反过来的,您可以通过--whole-archive使链接成功:

$ gcc -o prog -Wl,--whole-archive libxy.a -Wl,--no-whole-archive main.o
$ ./prog
x

显然,你不能通过简单地这样做就使其成功。
$ gcc -o prog -Wl,--start-group libxy.a -Wl,--end-group main.o
main.o: In function `main':
main.c:(.text+0x5): undefined reference to `x'
collect2: error: ld returned 1 exit status

因为这与以下内容没有什么不同:

$ gcc -o prog libxy.a main.o

现在这里有一个链接的例子,默认行为下会失败,但是可以使用--start-group ... --end-group来使其成功。 a.c
#include <stdio.h>

void a(void)
{
    puts(__func__);
}

b.c

#include <stdio.h>

void b(void)
{
    puts(__func__);
}

ab.c

extern void b(void);

void ab(void)
{
    b();
}

ba.c

extern void a(void);

void ba(void)
{
    a();
}

abba.c

extern void ab(void);
extern void ba(void);

void abba(void)
{
    ab();
    ba();
}

main2.c

extern void abba(void);

int main(void)
{
    abba();
    return 0;
}

编译所有源码:

$ gcc -Wall a.c b.c ab.c ba.c abba.c main2.c

然后制作以下静态库:
$ ar rcs libbab.a ba.o b.o x.o
$ ar rcs libaba.a ab.o a.o y.o
$ ar rcs libabba.a abba.o

请注意,旧的目标文件x.oy.o在这里被归档。

在这里,libabba.a依赖于libbab.alibaba.a。具体而言, libabba.a(abba.o)引用了在libaba.a(ab.o)中定义的ab; 它还引用了在libbab.a(ba.o)中定义的ba。 因此,在链接顺序中,libabba.a必须出现在libbab.alibaba.a之前。

libbab.a依赖于libaba.a。具体而言,libbab.a(ba.o)引用了在libaba(a.o)中定义的a

但是,libaba.a也依赖于libbab.alibaba(ab.o)引用了在libbab(b.o)中定义的blibbab.alibaba.a之间存在循环依赖关系。因此,无论我们在默认链接中首先放置哪一个, 都将出现未定义引用错误。可以这样做:

$ gcc -o prog2 main2.o libabba.a libaba.a libbab.a
libbab.a(ba.o): In function `ba':
ba.c:(.text+0x5): undefined reference to `a'
collect2: error: ld returned 1 exit status

或者这样:
$ gcc -o prog2 main2.o libabba.a libbab.a libaba.a
libaba.a(ab.o): In function `ab':
ab.c:(.text+0x5): undefined reference to `b'
collect2: error: ld returned 1 exit status

循环依赖是一个问题,而--start-group ... --end-group则是解决这个问题的方法:

$ gcc -o prog2 main2.o libabba.a -Wl,--start-group libbab.a libaba.a -Wl,--end-group
$ ./prog2
b
a

因此,--whole-archive ... --no-whole-archive 也是一种解决方案:
$ gcc -o prog2 main2.o libabba.a -Wl,--whole-archive libbab.a libaba.a -Wl,--no-whole-archive
$ ./prog2
b
a

但这是一个不好的解决方案。让我们跟踪每种情况下实际链接到程序中的目标文件。

使用--start-group ... --end-group

$ gcc -o prog2 main2.o libabba.a -Wl,--start-group libbab.a libaba.a -Wl,--end-group -Wl,--trace
/usr/bin/x86_64-linux-gnu-ld: mode elf_x86_64
/usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/Scrt1.o
/usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/crti.o
/usr/lib/gcc/x86_64-linux-gnu/7/crtbeginS.o
main2.o
(libabba.a)abba.o
(libbab.a)ba.o
(libaba.a)ab.o
(libaba.a)a.o
(libbab.a)b.o
libgcc_s.so.1 (/usr/lib/gcc/x86_64-linux-gnu/7/libgcc_s.so.1)
/lib/x86_64-linux-gnu/libc.so.6
(/usr/lib/x86_64-linux-gnu/libc_nonshared.a)elf-init.oS
/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
libgcc_s.so.1 (/usr/lib/gcc/x86_64-linux-gnu/7/libgcc_s.so.1)
/usr/lib/gcc/x86_64-linux-gnu/7/crtendS.o
/usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/crtn.o

他们是:

main2.o
(libabba.a)abba.o
(libbab.a)ba.o
(libaba.a)ab.o
(libaba.a)a.o
(libbab.a)b.o

这些正是程序所需的内容。

使用--whole-archive ... --no-whole-archive

$ gcc -o prog2 main2.o libabba.a -Wl,--whole-archive libbab.a libaba.a -Wl,--no-whole-archive -Wl,-trace
/usr/bin/x86_64-linux-gnu-ld: mode elf_x86_64
/usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/Scrt1.o
/usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/crti.o
/usr/lib/gcc/x86_64-linux-gnu/7/crtbeginS.o
main2.o
(libabba.a)abba.o
(libbab.a)ba.o
(libbab.a)b.o
(libbab.a)x.o
(libaba.a)ab.o
(libaba.a)a.o
(libaba.a)y.o
libgcc_s.so.1 (/usr/lib/gcc/x86_64-linux-gnu/7/libgcc_s.so.1)
/lib/x86_64-linux-gnu/libc.so.6
(/usr/lib/x86_64-linux-gnu/libc_nonshared.a)elf-init.oS
/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
libgcc_s.so.1 (/usr/lib/gcc/x86_64-linux-gnu/7/libgcc_s.so.1)
/usr/lib/gcc/x86_64-linux-gnu/7/crtendS.o
/usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/crtn.o

它们是:

main2.o
(libabba.a)abba.o
(libbab.a)ba.o
(libbab.a)b.o
(libbab.a)x.o
(libaba.a)ab.o
(libaba.a)a.o
(libaba.a)y.o

与之前相同,加上以下内容:
(libbab.a)x.o
(libaba.a)y.o

存在死代码(而且可能还有更多,没有限制)。冗余函数x()y()在图像中被定义:

$ nm prog2 | egrep 'T (x|y)'
000000000000067a T x
00000000000006ac T y

要点

  • 如果可以,请使用默认链接来按依赖顺序放置输入。
  • 如果无法修复库之间的循环依赖,请使用--start-group ... --end-group。请注意,链接速度会受到影响。
  • 仅在您实际上需要链接...中所有静态库中的所有对象文件时才使用--whole-archive ... --no-whole-archive。否则,请执行前两个操作中的任何一个。


[1] 请注意,在由GCC调用链接器时,链接器的命令行实际上比您在GCC命令行中显式传递的链接选项更长,并且还会自动添加样板选项。因此,请始终使用--whole-archive后面加上--no-whole-archive,并使用--start-group后面加上--end-group


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