与C中的静态库链接

linking with static library in C

嗨,我是 C 语言和链接的初学者,我正在阅读一本关于链接静态库的问题:

Let a and b denote object modules or static libraries in the current directory, and let a→b denote that a depends on b, in the sense that b defines a symbol that is referenced by a. For each of the following scenarios, show the minimal command line (i.e., one with the least number of object file and library arguments) that will allow the static linker to resolve all symbol references:

p.o → libx.a → liby.a and liby.a → libx.a →p.o

而书上给出的答案是:

gcc p.o libx.a liby.a libx.a

我很困惑,答案不应该是:

gcc p.o libx.a liby.a libx.a p.o

否则 libx.a 中的未定义符号如何被 p.o 解析?

万一你的C教材没说清楚,链接 作者试图以此来说明的行为 练习不是 C 标准强制要求的,实际上是行为 GNU binutils 链接器 ld - Linux 中的默认系统链接器, 通常由 gcc|g++|gfortran 等代表您调用 - 并且可能 但不一定您可能遇到的其他链接器的行为。

如果你给了我们正确的练习,作者可能是一个不懂静态的人 链接和编写有关它的教科书一样好,或者可能只是没有 小心翼翼地表达自己。

除非我们链接一个程序,默认情况下链接器不会 甚至坚持解析所有符号引用。所以大概我们是 链接程序(不是共享库),如果答案:

 gcc p.o libx.a liby.a libx.a

其实书上是这么写的,程序就是这样。

但是一个程序必须有一个main功能。 main 函数在哪里 它与p.olibx.aliby.a的关联关系是什么?这个 很重要,我们没有被告知。

所以我们假设p代表程序,并且main函数在 在 p.o 中定义最少。奇怪的是 liby.a 依赖 on p.o 其中 p.o 是程序的主要目标模块,它甚至是 在静态库的成员中定义的 main 函数很奇怪。

假设这么多,这里有一些源文件:

p.c

#include <stdio.h>

extern void x(void);

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

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

x.c

#include <stdio.h>

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

y.c

#include <stdio.h>

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

通话x.c

extern void x(void);

void callx(void)
{
    x();
}

通话y.c

extern void y(void);

void cally(void)
{
    y();
}

通话p.c

extern void p(void);

void callp(void)
{
    p();
}

将它们全部编译成目标文件:

 $ gcc -Wall -Wextra -c p.c x.c y.c callx.c cally.c callp.c

并制作静态库libx.aliby.a:

$ ar rcs libx.a x.o cally.o callp.o
$ ar rcs liby.a y.o callx.o

现在,p.olibx.aliby.a满足练习条件:

 p.o → libx.a → liby.a and liby.a → libx.a →p.o

因为:

  • p.o指代但不定义x,即 在 libx.a.

  • 中定义
  • libx.a定义cally,引用但不定义y, 在 liby.a

  • 中定义
  • liby.a定义callx,引用但不定义x, 在 libx.a.

  • 中定义
  • libx.a定义callp,引用但不定义p, 在 p.o.

  • 中定义

我们可以用nm确认:

 $ nm p.o
 0000000000000000 r __func__.2252
                  U _GLOBAL_OFFSET_TABLE_
 0000000000000013 T main
 0000000000000000 T p
                  U puts
                  U x

p.o 定义 p (=T p) 并引用 x (=U x)

$ nm libx.a

x.o:
0000000000000000 r __func__.2250
                 U _GLOBAL_OFFSET_TABLE_
                 U puts
0000000000000000 T x

cally.o:
0000000000000000 T cally
                 U _GLOBAL_OFFSET_TABLE_
                 U y

callp.o:
0000000000000000 T callp
                 U _GLOBAL_OFFSET_TABLE_
                 U p

libx.a 定义 x (=T x) 和引用 y (=U y) 和 参考文献 p ( = U p)

$ nm liby.a

y.o:
0000000000000000 r __func__.2250
                 U _GLOBAL_OFFSET_TABLE_
                 U puts
0000000000000000 T y

callx.o:
0000000000000000 T callx
                 U _GLOBAL_OFFSET_TABLE_
                 U x

liby.a 定义 y (=T y) 并引用 x (=U x)

现在教材链接肯定成功了:

$ gcc p.o libx.a liby.a libx.a
$ ./a.out
x

但它是最短的链接吗?不,这是:

$ gcc p.o libx.a
$ ./a.out
x

为什么?让我们重新运行与诊断的链接以显示我们的对象 文件实际链接:

$ gcc p.o libx.a -Wl,-trace
/usr/bin/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
p.o
(libx.a)x.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

他们是:

 p.o
 (libx.a)x.o

p.o 首先链接到程序中,因为输入 .o 文件是 总是链接,无条件。

然后是libx.a。读 static-libaries 了解链接器如何处理它。链接 p.o 后,它有 只有一个未解决的参考 - 对 x 的参考。它检查了 libx.a 以寻找 定义 x 的目标文件。它找到了 (libx.a)x.o。它从 libx.a 中提取了 x.o 并链接它,然后 完成1

涉及liby.a的所有依赖关系:-

  • (libx.a)cally.o 取决于 (liby.a)y.o
  • (liby.a)callx.o 取决于 (libx.a)x.o

与链接无关,因为链接不需要任何 liby.a.

中的目标文件

鉴于作者所说的是正确答案,我们可以逆向工程 他们正在努力陈述的练习。就是这样:

  • 一个目标模块 p.o 定义了 main 引用了一个符号 x 它 没有定义,x定义在静态库的成员x.olibxz.a

  • (libxz.a)x.o引用了一个它没有定义的符号y,而y定义在一个静态库的成员y.oliby.a

  • (liby.a)y.o引用了一个它没有定义的符号z,而z定义在[=110=的成员z.o中].

  • (liby.a)y.o引用了一个它没有定义的符号pp定义在p.o

  • 使用p.olibxz.aliby.a的最小联动命令是什么 那会成功吗?

新源文件:

p.c

Stays as before.

x.c

#include <stdio.h>

extern void y();

void cally(void)
{
    y();
}

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

y.c

#include <stdio.h>

extern void z(void);
extern void p(void);

void callz(void)
{
    z();
}

void callp(void)
{
    p();
}

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

z.c

#include <stdio.h>

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

新的静态库:

$ ar rcs libxz.a x.o z.o
$ ar rcs liby.a y.o

现在联动:

$ gcc p.o libxz.a
libxz.a(x.o): In function `cally':
x.c:(.text+0xa): undefined reference to `y'
collect2: error: ld returned 1 exit status

失败,同样如此:

$ gcc p.o libxz.a liby.a
liby.a(y.o): In function `callz':
y.c:(.text+0x5): undefined reference to `z'
collect2: error: ld returned 1 exit status

和:

$ gcc p.o liby.a libxz.a
libxz.a(x.o): In function `cally':
x.c:(.text+0xa): undefined reference to `y'
collect2: error: ld returned 1 exit status

和(您自己选择):

$ gcc p.o liby.a libxz.a p.o
p.o: In function `p':
p.c:(.text+0x0): multiple definition of `p'
p.o:p.c:(.text+0x0): first defined here
p.o: In function `main':
p.c:(.text+0x13): multiple definition of `main'
p.o:p.c:(.text+0x13): first defined here
libxz.a(x.o): In function `cally':
x.c:(.text+0xa): undefined reference to `y'
collect2: error: ld returned 1 exit status

因未定义引用错误 多重定义错误而失败。

但是课本答案:

$ gcc p.o libxz.a liby.a libxz.a
$ ./a.out
x

现在是对的。

作者试图描述两者之间的相互依赖 程序链接中的静态库,却摸索出这样一个相互依赖的事实 仅当链接 需要 至少一个来自 each 库的目标文件时才能存在 指的是由 other 库中的目标文件定义的一些符号。

从更正的练习中吸取的教训是:

  • 链接器输入中出现的目标文件foo.o永远不需要出现 不止一次,因为它将 无条件地 链接,并且当它是 链接它提供的任何符号的定义 s 将用于解析 所有其他链接器输入产生的对 s 的引用。如果 foo.o 是 输入两次你只能得到多重定义的错误 s.

  • 但是链接中静态库之间存在相互依赖的地方 可以通过输入其中一个库两次来解决。因为一个目标文件 从 静态库 中提取并链接当且仅当该目标文件是 需要 来定义链接器试图定义的未解析符号引用 在输入库的时候。所以在更正的例子中:

    • p.o输入无条件链接
    • x 成为未解析的引用。
    • libxz.a 已输入。
    • (libxz.a)x.o 中找到 x 的定义。
    • (libxz.a)x.o 被提取并链接。
    • x 已解决。
    • 但是(libxz.a)x.o指的是y
    • y 成为未解析的引用。
    • liby.a 已输入。
    • (liby.a)y.o 中找到 y 的定义。
    • (liby.a)y.o 被提取并链接。
    • y 已解决。
    • 但是(liby.a)y.o指的是z
    • z 成为未解析的引用。
    • libxz.a再输入.
    • libxz.a(z.o)
    • 中找到 z 的定义
    • libxz.a(z.o) 被提取并链接。
    • z已解决。


[1] 正如 -trace 输出所示,严格来说链接不是 完成,直到 (libx.a)x.o 之后的所有样板也被链接, 但是每个 C 程序链接都是相同的样板文件。