【问题标题】:How to prevent all symbols from static library to load and why other symbols from same .o file get exported to test while linking static library如何防止加载静态库中的所有符号以及为什么在链接静态库时将来自同一 .o 文件的其他符号导出以进行测试
【发布时间】:2024-01-23 03:01:01
【问题描述】:

假设有三个c文件,比如a.c包含函数xx()yy()b.c包含nn()mm()c.c包含qq()rr()

我用a.ob.oc.o 制作了一个静态库stat.a。如果我将stat.a 链接到调用xx() 的测试中,则符号yy() 也会被导出:nm test 具有符号xxyy

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

【问题讨论】:

  • 从最终的可执行文件中去掉符号?
  • 能否显示一些代码(mcve)[*.com/help/mcve]

标签: c linux linker static-libraries


【解决方案1】:

这是您的场景的实现:

交流

#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.ob.oc.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

xxyy 的定义 (T) 在成员 stat.a(a.o) 中。 nnmm 的定义 在stat.a(b.o)qqrr的定义在stat.a(c.o)中。

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

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

xx,在程序中被调用,被定义。 yy没有被调用,也是 定义。 nnmmqqrr,没有一个被调用过,都是缺席的。

这就是你观察到的。

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

什么是静态库,例如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 的定义。因此,该定义也链接到程序中。 nnmmqqrr 没有在程序中定义,因为它们都没有 在链接到程序的目标文件中定义。

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

是否有任何方法可以防止加载除xx 之外的任何其他符号?

至少有两种方式。

只需在源中定义xxyynnmmqqrr中的每一个 文件本身。然后编译目标文件xx.oyy.onn.omm.oqq.orr.o 并将它们全部归档到stat.a。然后,如果链接器需要找到一个 stat.a 中定义xx 的目标文件,它将找到xx.o,提取并链接它, 并且xx单独的定义将被添加到链接中。

还有另一种方法,不需要您在每个源代码中只编写一个函数 文件。这种方式取决于这样一个事实,即由 编译器,由各种组成,这些节实际上是 链接器区分并合并到输出文件中的单元。经过 默认情况下,每种符号都有一个标准的 ELF 部分。这 编译器将所有函数定义放在一个 code 部分中,并且 适当的 data 部分中的所有数据定义。你的原因 程序test 的链接包含xxyy 的定义是 编译器已将这两个定义放在a.o 的单个代码部分中, 因此链接器可以将该代码段合并到程序中,也可以不合并:它可以 只链接xxyy的定义,或者两者都不链接,所以有义务 链接两者,即使只需要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 部分看到xxyy 的定义。

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

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

$ 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 的冗余定义是 不再在程序中。

【讨论】:

【解决方案2】:
  1. 我想知道为什么符号 qq 和 rr 没有导出?

你必须通知链接器你的意图How to force gcc to link an unused static library

gcc -L./ -o test test.c -Wl,--whole-archive stat.a -Wl,--no-whole-archive

  1. 有什么方法可以防止加载 xx 以外的任何其他符号?

来自How do I include only used symbols when statically linking with gcc?

gcc -ffunction-sections -c a.c

gcc -L./ -o test test.c -Wl,--gc-sections stat.a

【讨论】:

  • 这个 -ffunction 没有找到工作。假设 ac 包含 xx() 和 yy(),并且 ao 是使用标志 ffunction-sections 和 stat.a 由 ao 制成的,然后它链接到一个测试.c 对编译进行二进制测试。使用 nm 测试验证时显示符号 xx 和 yy。@rantan pan
最近更新 更多