首页 > 解决方案 > 如何防止加载静态库中的所有符号以及为什么在链接静态库时将同一 .o 文件中的其他符号导出以进行测试

问题描述

假设有三个 c 文件,比如说contains a.cfunctions 和contains xx(),并且contains ,。yy()b.cnn()mm()c.cqq()rr()

我用和制作stat.a了一个静态a.o库。如果我链接到调用 的测试,那么符号也会被导出:同时具有符号和.b.oc.ostat.axx()yy()nm testxxyy

  1. 我想知道为什么符号qqrr没有被导出?
  2. xx除了被加载之外,还有什么方法可以防止任何其他符号吗?

标签: clinuxlinkerstatic-libraries

解决方案


这是您的场景的实现:

交流

#include <stdio.h>

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

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

公元前

#include <stdio.h>

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

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

抄送

#include <stdio.h>

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

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

测试.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.ostat.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

的定义 ( T)在 memberxx中。的定义 在. 的定义在.yystat.a(a.o)nnmmstat.a(b.o)qqrrstat.a(c.o)

让我们看看程序的符号表中还定义了哪些符号test

$ nm test | egrep 'T (xx|yy|qq|rr|nn|mm)'
000000000000064a T xx
000000000000065d T yy

xx,在程序中被调用,被定义。yy没有被调用,也被定义了。nn, mm,qqrr, 都没有被调用, 都是不存在的。

这就是你观察到的。

我想知道为什么符号qqrr没有得到导出?

什么是静态库,例如stat.a,它在链接中的特殊作用是什么?

它是一个通常——但不一定——只包含目标文件的档案ar您可以向链接器提供这样的档案,从中选择它需要的目标文件(如果有的话)以进行链接。链接器需要存档中的那些目标文件,这些目标文件为它已经链接的输入文件中已引用但尚未定义的符号提供定义。链接器从存档中提取所需的目标文件并将它们输入到链接中,就像它们是单独命名的输入文件并且根本没有提到静态库一样。

因此,链接器对输入静态库的处理与它对输入对象文件的处理不同。任何输入目标文件都无条件地链接到输出文件 (无论是否需要)。

鉴于此,让我们重做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 程序链接的所有样板文件外,链接中唯一的文件是两个目标文件:

test.o
(stat.a)a.o

联动:

$ gcc -o test test.o stat.a

与联动完全相同:

$ gcc -o test test.o a.o

让我们考虑一下。

  • test.o是第一个链接器输入。该目标文件被无条件地链接到程序中。
  • test.o包含对 function 的引用(特别是函数调用)xx但没有定义xx
  • 所以链接器现在需要找到一个定义xx来完成链接。
  • 下一个链接器输入是静态库stat.a
  • 链接器搜索stat.a包含xx.
  • 它发现a.o. 它从档案中提取a.o并将其链接到程序中。
  • 链接中没有其他未解析的符号引用,链接器可以在stat.a(b.o)or中找到定义stat(c.o)。因此,这些目标文件都没有被提取和链接。

通过提取链接(仅)stat.a(a.o),链接器获得了xx解析函数调用所需的定义test.o。而且a.o 包含 的定义yy。因此,该定义也链接到程序中。 nn, mm,qqrr没有在程序中定义,因为它们都没有在链接到程序的目标文件中定义。

这就是你第一个问题的答案。你的第二个是:

xx除了被加载之外,还有什么方法可以防止任何其他符号吗?

至少有两种方式。

一种是简单地在源文件中xx单独定义, yy, nn, mm, qq,中的每一个。然后编译目标文件, , , ,并将 它们全部归档到. 然后,如果链接器需要在该定义中找到一个目标文件,它将找到、提取并链接它,并且将单独的定义添加到链接中。rr xx.oyy.onn.omm.oqq.orr.ostat.astat.axxxx.oxx

还有另一种方法,不需要您在每个源文件中只编写一个函数。这种方式取决于编译器生成的 ELF 目标文件由多个部分组成,这些部分实际上是链接器区分并合并到输出文件中的单元。默认情况下,每种符号都有一个标准的 ELF 部分。编译器将所有函数定义放在一个代码段中,并将所有数据定义放在适当的数据段中。您的程序链接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

您可以在该部分中看到 和 的定义xxyy.text

但是您可以要求编译器将每个全局符号的定义 放在目标文件中它自己的部分中。然后链接器可以将任何函数定义的代码部分与任何其他部分分开,并且您可以要求链接器丢弃任何未在输出文件中使用的部分。让我们试试看。

再次编译所有源文件,这一次要求每个符号有一个单独的部分:

$ 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.xxxx.text.yyyy

重建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

最后看一下mapfile。在您看到的顶部附近:

地图文件

...
Discarded input sections
...
...
 .text.yy       0x0000000000000000       0x13 stat.a(a.o)
...
...

链接器能够丢弃.text.yy输入文件中的冗余代码部分stat.a(a.o)yy这就是程序中不再有多余定义的原因。


推荐阅读