这是您的场景的实现:
交流
#include <stdio.h>
void xx(void)
{
puts(__func__);
}
void yy(void)
{
puts(__func__);
}
b.c
#include <stdio.h>
void nn(void)
{
puts(__func__);
}
void mm(void)
{
puts(__func__);
}
c.c
#include <stdio.h>
void qq(void)
{
puts(__func__);
}
void rr(void)
{
puts(__func__);
}
test.c
extern void xx(void);
int main(void)
{
xx();
return 0;
}
将所有*.c文件编译成*.o文件:
$ gcc -Wall -c a.c b.c c.c test.c
制作静态库stat.a,包含a.o、b.o、c.o:
$ ar rcs stat.a a.o b.o c.o
链接程序test,输入test.o和stat.a:
$ gcc -o test test.o stat.a
运行:
$ ./test
xx
我们看一下stat.a中目标文件的符号表:
$ nm stat.a
a.o:
0000000000000000 r __func__.2250
0000000000000003 r __func__.2254
U _GLOBAL_OFFSET_TABLE_
U puts
0000000000000000 T xx
0000000000000013 T yy
b.o:
0000000000000000 r __func__.2250
0000000000000003 r __func__.2254
U _GLOBAL_OFFSET_TABLE_
0000000000000013 T mm
0000000000000000 T nn
U puts
c.o:
0000000000000000 r __func__.2250
0000000000000003 r __func__.2254
U _GLOBAL_OFFSET_TABLE_
U puts
0000000000000000 T qq
0000000000000013 T rr
xx、yy 的定义 (T) 在成员 stat.a(a.o) 中。 nn、mm 的定义
在stat.a(b.o)。 qq、rr的定义在stat.a(c.o)中。
让我们看看在程序test的符号表中还定义了哪些符号:
$ nm test | egrep 'T (xx|yy|qq|rr|nn|mm)'
000000000000064a T xx
000000000000065d T yy
xx,在程序中被调用,被定义。 yy,没有被调用,也是
定义。 nn、mm、qq和rr,没有一个被调用过,都是缺席的。
这就是你观察到的。
我想知道为什么符号qq 和rr 没有被导出?
什么是静态库,例如stat.a,它在链接中的特殊作用是什么?
这是一个ar archive,通常 - 但不一定 - 不包含任何内容
但目标文件。您可以将此类存档提供给链接器,以便从中选择
它需要的目标文件(如果有)进行链接。链接器需要那些对象
档案中的文件,这些文件为已被定义的符号提供了定义
在它已经链接的输入文件中引用但尚未定义。这
链接器从存档中提取需要的目标文件并将它们输入到
链接,就像它们被单独命名的输入文件和静态库一样
根本没有提到。
所以链接器对输入静态库所做的与它所做的不同
带有输入目标文件。任何输入对象文件无条件地链接到输出文件
(无论是否需要)。
有鉴于此,让我们重做test 的链接并进行一些诊断(-trace) 以显示什么
文件实际上是链接的:
$ gcc -o test test.o stat.a -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
test.o
(stat.a)a.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
除了 gcc 添加的 C 程序链接的所有样板文件
默认情况下,链接中ours的唯一文件是两个目标文件:
test.o
(stat.a)a.o
联动:
$ gcc -o test test.o stat.a
与链接完全相同:
$ gcc -o test test.o a.o
让我们考虑一下。
-
test.o 是第一个链接器输入。此目标文件已无条件链接到程序中。
-
test.o 包含对 xx 的引用(特别是函数调用),但没有定义函数 xx。
- 所以链接器现在需要找到
xx 的定义来完成链接。
- 下一个链接器输入是静态库
stat.a。
- 链接器在
stat.a 中搜索包含xx 定义的目标文件。
- 它找到
a.o。它从存档中提取a.o 并将其链接到程序中。
- 链接中没有其他未解析的符号引用,
链接器可以在
stat.a(b.o) 或stat(c.o) 中找到定义。所以这些都不是
目标文件被提取和链接。
通过提取链接(仅)stat.a(a.o),链接器获得了定义
xx 中的一个,它需要解析 test.o 中的函数调用。但是a.o 也包含
yy 的定义。因此,该定义也链接到程序中。
nn、mm、qq 和 rr 没有在程序中定义,因为它们都没有
在链接到程序的目标文件中定义。
这就是你第一个问题的答案。你的第二个是:
是否有任何方法可以防止加载除xx 之外的任何其他符号?
至少有两种方式。
只需在源中定义xx、yy、nn、mm、qq、rr中的每一个
文件本身。然后编译目标文件xx.o、yy.o、nn.o、mm.o、qq.o、rr.o
并将它们全部归档到stat.a。然后,如果链接器需要找到一个
stat.a 中定义xx 的目标文件,它将找到xx.o,提取并链接它,
并且xx单独的定义将被添加到链接中。
还有另一种方法,不需要您在每个源代码中只编写一个函数
文件。这种方式取决于这样一个事实,即由
编译器,由各种节组成,这些节实际上是
链接器区分并合并到输出文件中的单元。经过
默认情况下,每种符号都有一个标准的 ELF 部分。这
编译器将所有函数定义放在一个 code 部分中,并且
适当的 data 部分中的所有数据定义。你的原因
程序test 的链接包含xx 和yy 的定义是
编译器已将这两个定义放在a.o 的单个代码部分中,
因此链接器可以将该代码段合并到程序中,也可以不合并:它可以
只链接xx和yy的定义,或者两者都不链接,所以有义务
链接两者,即使只需要xx。我们来看看a.o的代码段的反汇编。默认情况下
代码部分被称为.text:
$ objdump -d a.o
a.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <xx>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # b <xx+0xb>
b: e8 00 00 00 00 callq 10 <xx+0x10>
10: 90 nop
11: 5d pop %rbp
12: c3 retq
0000000000000013 <yy>:
13: 55 push %rbp
14: 48 89 e5 mov %rsp,%rbp
17: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 1e <yy+0xb>
1e: e8 00 00 00 00 callq 23 <yy+0x10>
23: 90 nop
24: 5d pop %rbp
25: c3 retq
您可以在.text 部分看到xx 和yy 的定义。
但是你可以要求编译器放置每个全局符号的定义
在它自己的部分在目标文件中。然后链接器可以分离代码
任何其他函数定义的部分,您可以询问链接器
丢弃输出文件中未使用的任何部分。让我们试试吧。
再次编译所有源文件,这一次要求每个符号有一个单独的部分:
$ gcc -Wall -ffunction-sections -fdata-sections -c a.c b.c c.c test.c
现在再看a.o的反汇编:
$ objdump -d a.o
a.o: file format elf64-x86-64
Disassembly of section .text.xx:
0000000000000000 <xx>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # b <xx+0xb>
b: e8 00 00 00 00 callq 10 <xx+0x10>
10: 90 nop
11: 5d pop %rbp
12: c3 retq
Disassembly of section .text.yy:
0000000000000000 <yy>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # b <yy+0xb>
b: e8 00 00 00 00 callq 10 <yy+0x10>
10: 90 nop
11: 5d pop %rbp
12: c3 retq
现在我们在a.o 中有两个 代码段:.text.xx,只包含xx 的定义,
和.text.yy,仅包含yy 的定义。链接器可以合并任何一个
将这些部分合并到一个程序中,而不是合并其他部分。
重建stat.a
$ rm stat.a
$ ar rcs stat.a a.o b.o c.o
重新链接程序,这一次要求链接器丢弃未使用的输入段
(-gc-sections)。我们还将要求它跟踪它加载的文件 (-trace)
并为我们打印地图文件(-Map=mapfile):
$ gcc -o test test.o stat.a -Wl,-gc-sections,-trace,-Map=mapfile
/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
test.o
(stat.a)a.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
-trace 的输出与之前完全相同。但是再次检查我们的哪个
符号在程序中定义:
$ nm test | egrep 'T (xx|yy|qq|rr|nn|mm)'
000000000000064a T xx
只有xx,这是你想要的。
程序的输出和之前一样:
$ ./test
xx
最后看看地图文件。在您看到的顶部附近:
地图文件
...
Discarded input sections
...
...
.text.yy 0x0000000000000000 0x13 stat.a(a.o)
...
...
链接器能够丢弃冗余代码段.text.yy
输入文件stat.a(a.o)。这就是为什么yy 的冗余定义是
不再在程序中。