嗨,我是 C 和链接的初学者,我正在阅读一本关于与静态库链接有问题的书:
让 a 和 b 表示当前目录中的目标模块或静态库,让 a→b 表示 a 依赖于 b,因为 b 定义了一个被 a 引用的符号。对于以下每种情况,显示允许静态链接器解析所有符号引用的最小命令行(即,具有最少数量的目标文件和库参数的命令行):
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 标准规定的,实际上是 GNUbinutils
链接器的行为ld
- Linux 中的默认系统链接器,通常被调用代表您通过gcc|g++|gfortran
等 - 可能但不一定是您可能遇到的其他链接器的行为。
如果你准确地给了我们练习,作者可能是一个不太了解静态链接的人,而不是编写关于它的教科书的最佳人,或者可能只是没有非常小心地表达自己。
除非我们正在链接一个程序,否则默认情况下链接器甚至不会坚持解析所有符号引用。所以大概我们正在链接一个程序(不是共享库),如果答案是:
gcc p.o libx.a liby.a libx.a
实际上是教科书所说的,那么程序就是它必须的样子。
但是一个程序必须有一个main
功能。main
函数在哪里以及它与p.o
、libx.a
和 的链接关系是liby.a
什么?这很重要,我们没有被告知。
所以让我们假设p
代表program,并且 main 函数至少定义在p.o
. 奇怪的是,虽然liby.a
取决于程序的主要对象模块在p.o
哪里p.o
,但main
在静态库的成员中定义函数会更奇怪。
假设这么多,这里有一些源文件:
个人电脑
#include <stdio.h>
extern void x(void);
void p(void)
{
puts(__func__);
}
int main(void)
{
x();
return 0;
}
xc
#include <stdio.h>
void x(void)
{
puts(__func__);
}
yc
#include <stdio.h>
void y(void)
{
puts(__func__);
}
呼叫中心
extern void x(void);
void callx(void)
{
x();
}
呼叫.c
extern void y(void);
void cally(void)
{
y();
}
调用程序
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.a
和liby.a
:
$ ar rcs libx.a x.o cally.o callp.o
$ ar rcs liby.a y.o callx.o
现在,p.o
,libx.a
并liby.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
。阅读静态库以了解链接器如何处理它。链接后p.o
,它只有一个未解析的引用 - 对x
. 它检查libx.a
寻找定义x
. 它发现(libx.a)x.o
. 它提取x.o
自libx.a
并将其链接,然后将其完成。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.o
静态库的成员中定义libxz.a
(libxz.a)x.o
引用y
它没有定义的符号,并且y
在y.o
静态库的成员中定义liby.a
(liby.a)y.o
指的z
是它没有定义的符号,并z
在 的成员z.o
中定义libxz.a
。
(liby.a)y.o
指的p
是它没有定义的符号,并p
在p.o
什么是最小的联动指挥使用p.o
,libxz.a
,liby.a
会成功吗?
新的源文件:
个人电脑
Stays as before.
xc
#include <stdio.h>
extern void y();
void cally(void)
{
y();
}
void x(void)
{
puts(__func__);
}
yc
#include <stdio.h>
extern void z(void);
extern void p(void);
void callz(void)
{
z();
}
void callp(void)
{
p();
}
void y(void)
{
puts(__func__);
}
零点
#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
现在是对的。
作者试图描述一个程序链接中两个静态库之间的相互依赖,但发现这种相互依赖只有在链接需要每个库中至少有一个引用某个符号的目标文件时才能存在。由另一个库中的目标文件定义。
从修正后的练习中要吸取的教训是:
foo.o
出现在链接器输入中的目标文件永远不需要出现多次,因为它将被无条件链接,并且当它被链接时,它提供的任何符号的定义s
将用于解析s
对任何其他链接器产生的所有引用输入。如果foo.o
输入两次,您只能得到 . 的多重定义的错误s
。
但是如果链接中的静态库之间存在相互依赖,则可以通过两次输入其中一个库来解决。因为目标文件是从静态库中提取并链接的,当且仅当需要该目标文件来定义链接器在输入库时试图定义的未解析符号引用时。所以在更正的例子中:
p.o
是输入和无条件链接。x
成为未解决的参考。libxz.a
是输入。x
中找到(libxz.a)x.o
。(libxz.a)x.o
被提取和链接。x
已解决。(libxz.a)x.o
指y
。y
成为未解决的参考。liby.a
是输入。y
中找到(liby.a)y.o
。(liby.a)y.o
被提取和链接。y
已解决。(liby.a)y.o
指z
。z
成为未解决的参考。libxz.a
再次输入。z
见libxz.a(z.o)
libxz.a(z.o)
被提取和链接。z
已解决。-trace
输出所示,严格来说,直到所有后续的样板 (libx.a)x.o
也被链接后,才完成了 链接,但对于每个 C 程序链接,它都是相同的样板。
本文收集自互联网,转载请注明来源。
如有侵权,请联系[email protected] 删除。
我来说两句