如何用GCC和ld删除未使用的C/ c++符号

How to remove unused C/C++ symbols with GCC and ld?

本文关键字:c++ 符号 未使用 删除 何用 GCC ld      更新时间:2023-10-16

我需要严格优化可执行文件的大小(ARM开发)和我注意到,在我目前的构建方案(gcc + ld)未使用的符号没有被剥离。

使用arm-strip --strip-unneeded的结果可执行文件/库不会改变可执行文件的输出大小(我不知道为什么,也许它根本不能)

(如果存在)如何修改我的构建管道,以便从结果文件中剥离未使用的符号?


我甚至不会想到这一点,但我目前的嵌入式环境不是很"强大"即使将500K2M中保存出来,也会使加载性能得到非常好的提升。

更新:

不幸的是,目前我使用的gcc版本没有-dead-strip选项,ld-ffunction-sections... + --gc-sections对结果输出没有任何显着差异。

我很震惊,这甚至成为一个问题,因为我确信gcc + ld应该自动剥离未使用的符号(为什么他们甚至必须保留它们?)

对于GCC,这分两个阶段完成:

首先编译数据,但告诉编译器在翻译单元内将代码分成单独的部分。对于函数、类和外部变量,可以使用以下两个编译器标志:

-fdata-sections -ffunction-sections

使用链接器优化标志将翻译单元链接在一起(这会导致链接器丢弃未引用的部分):

-Wl,--gc-sections

因此,如果您有一个名为test.cpp的文件,其中声明了两个函数,但是其中一个是未使用的,那么您可以使用以下命令忽略未使用的那个gcc(g++):

gcc -Os -fdata-sections -ffunction-sections test.cpp -o test -Wl,--gc-sections

(注意- o是一个额外的编译器标志,它告诉GCC优化大小)

如果要相信这个线程,您需要向GCC提供-ffunction-sections-fdata-sections,它们将把每个函数和数据对象放在自己的部分中。然后将--gc-sections交给GNU ld来删除未使用的部分。

您将需要检查您的文档以查看您的gcc &ld:

然而,对于我(OS X gcc 4.0.1),我发现这些

-dead_strip

删除入口点或导出符号无法访问的函数和数据。

-dead_strip_dylibs

删除入口点或导出符号无法访问的动态库。也就是说,禁止为在链接期间不提供符号的dylib生成加载命令命令。当由于某些间接原因(例如dylib有重要的初始化项)在运行时需要链接dylib时,不应使用此选项。

这个有用的选项

-why_live symbol_name

记录对symbol_name的引用链。仅适用于-dead_strip。它可以帮助调试为什么您认为应该删除的死条没有被删除。

在gcc/g++手册中也有一个注释,某些类型的死代码消除只有在编译时启用优化时才会执行。

虽然这些选项/条件可能不适合你的编译器,我建议你在你的文档中寻找类似的东西。

编程习惯也有帮助;例如,在特定文件之外不访问的函数中添加static;使用较短的符号名称(可能有一点帮助,但可能不会太大);尽可能使用const char x[];…本文虽然讨论的是动态共享对象,但也包含一些建议,如果遵循这些建议,可以帮助减小最终二进制输出的大小(如果目标是ELF)。

答案是-flto。您必须将它传递给编译和链接步骤,否则它不会做任何事情。

它实际上工作得很好-将我编写的微控制器程序的大小减少到不到以前大小的50% !

不幸的是,它似乎有点bug -我有事情没有被正确构建的实例。这可能是由于我使用的构建系统(QBS;它是非常新的),但无论如何,我建议您只在可能的情况下在最终构建中启用它,并彻底测试该构建。

在我看来Nemo提供的答案是正确的。如果这些指令不起作用,问题可能与您使用的gcc/ld版本有关,作为练习,我使用这里详细的指令编译了一个示例程序

#include <stdio.h>
void deadcode() { printf("This is d dead codezn"); }
int main(void) { printf("This is mainn"); return 0 ; }

然后我使用越来越激进的死代码删除开关编译代码:

gcc -Os test.c -o test.elf
gcc -Os -fdata-sections -ffunction-sections test.c -o test.elf -Wl,--gc-sections
gcc -Os -fdata-sections -ffunction-sections test.c -o test.elf -Wl,--gc-sections -Wl,--strip-all

这些编译和链接参数产生的可执行文件的大小分别为8457、8164和6160字节,最大的贡献来自于'strip-all'声明。如果您不能在您的平台上生成类似的缩减,那么可能您的gcc版本不支持此功能。我在Linux Mint 2.6.38-8-generic x86_64上使用gcc(4.5.2-8ubuntu4), ld(2.21.0.20110327)

虽然不是严格的符号,如果要大小-总是编译-Os-s标志。-Os优化了最小可执行文件大小的结果代码,-s从可执行文件中删除了符号表和重定位信息。

有时候——如果需要较小的尺寸——使用不同的优化标志可能——也可能没有——有意义。例如,切换-ffast-math和/或-fomit-frame-pointer有时可以节省几十个字节。

strip --strip-unneeded仅对可执行文件的符号表进行操作。它实际上并不删除任何可执行代码。

标准库通过将其所有函数拆分为单独的目标文件来实现您所追求的结果,这些目标文件使用ar进行组合。如果将生成的存档链接为一个库(即。将-l your_library选项赋给ld),那么ld将只包含实际使用的目标文件,因此也只包含实际使用的符号。

您还可以找到对这个类似的使用问题的一些回答。

来自GCC 4.2.1手册,章节-fwhole-program:

假设当前编译单元代表正在编译的整个程序。所有的公共函数和变量,除了main和那些被externally_visible属性合并的函数和变量,都变成了静态函数,并在一定程度上得到了过程间优化器更积极的优化。虽然这个选项相当于在由单个文件组成的程序中正确使用static关键字,但与--combine选项结合使用,这个标志可以用于编译大多数较小规模的C程序,因为函数和变量对整个组合编译单元都是局部的,而不是对单个源文件本身。

我不知道这是否有助于解决您当前的困境,因为这是最近的功能,但是您可以以全局方式指定符号的可见性。在编译时传递-fvisibility=hidden -fvisibility-inlines-hidden可以帮助链接器稍后去掉不需要的符号。如果您正在生成可执行文件(而不是共享库),则无需再做任何事情。

在GCC wiki上可以获得更多信息(以及例如库的细粒度方法)。

建议使用所有可选代码构建静态库,并将编译单元减少到保存小任务所需的最小值(也建议作为unix设计中的模式)

当你链接代码并指定一个静态库(.a存档)时,链接器只处理从初始crt0.o代码引用的所有编译模块,这可以在没有任何分段编译代码的情况下实现。

我们已经在我们的代码中这样做了,可能不是最优的好处,但允许我们继续开发良好的内存占用和节省大量未使用的代码,但永远不会发生像让编译器调查那样的问题。我总是使用这个引理:如果功能不是必需的,就不要绑定它

-fdata-sections -ffunction-sections -Wl,--gc-sections最小示例分析

这些选项被提到:https://stackoverflow.com/a/6770305/895245,我只是想确认他们的工作和检查一点如何与objdump

我们得出的结论与其他帖子提到的相似:

  • 如果一个section的任何符号被使用,那么整个section将被加入,即使一些其他符号根本没有被使用
  • 内联使符号不被视为已使用
  • -flto导致未使用的符号被删除,即使在同一编译单元中使用了其他符号

单独文件,仅-O3

notmain.c

int i1 = 1;
int i2 = 2;
int f1(int i) {
    return i + 1;
}
int f2(int i) {
    return i + 2;
}

c

extern int i1;
int f1(int i);
int main(int argc, char **argv) {
    return f1(argc) + i1;
}

只编译-O3:

gcc -c -O3 notmain.c
gcc -O3 notmain.o main.c

拆卸notmain.o:

objdump -D notmain.o

输出包含:

Disassembly of section .text:
0000000000000000 <f1>:
   0:   f3 0f 1e fa             endbr64
   4:   8d 47 01                lea    0x1(%rdi),%eax
   7:   c3                      ret
   8:   0f 1f 84 00 00 00 00    nopl   0x0(%rax,%rax,1)
   f:   00
0000000000000010 <f2>:
  10:   f3 0f 1e fa             endbr64
  14:   8d 47 02                lea    0x2(%rdi),%eax
  17:   c3                      ret
Disassembly of section .data:
0000000000000000 <i2>:
   0:   02 00                   add    (%rax),%al
        ...
0000000000000004 <i1>:
   4:   01 00                   add    %eax,(%rax)
        ...

拆卸notmain.o:

objdump -D a.out

输出包含:

Disassembly of section .text:
0000000000001040 <main>:
    1040:       f3 0f 1e fa             endbr64
    1044:       48 83 ec 08             sub    $0x8,%rsp
    1048:       e8 03 01 00 00          call   1150 <f1>
    104d:       03 05 c1 2f 00 00       add    0x2fc1(%rip),%eax        # 4014 <i1>
    1053:       48 83 c4 08             add    $0x8,%rsp
    1057:       c3                      ret
    1058:       0f 1f 84 00 00 00 00    nopl   0x0(%rax,%rax,1)
    105f:       00
0000000000001150 <f1>:
    1150:       f3 0f 1e fa             endbr64
    1154:       8d 47 01                lea    0x1(%rdi),%eax
    1157:       c3                      ret
    1158:       0f 1f 84 00 00 00 00    nopl   0x0(%rax,%rax,1)
    115f:       00
0000000000001160 <f2>:
    1160:       f3 0f 1e fa             endbr64
    1164:       8d 47 02                lea    0x2(%rdi),%eax
    1167:       c3                      ret
Disassembly of section .data:
0000000000004010 <i2>:
    4010:       02 00                   add    (%rax),%al
        ...
0000000000004014 <i1>:
    4014:       01 00                   add    %eax,(%rax)
i2f2都出现在最终的输出文件中,尽管它们没有被使用。

即使我们将-Wl,--gc-sections添加到:

gcc -O3 -Wl,--gc-sections notmain.o main.c

尝试删除未使用的部分,这不会改变任何东西,因为在目标文件notmain.o中,i2出现在与i1 (.data)相同的部分中,f2出现在与f1 (.text)相同的部分中,它们被使用,因此将它们的整个部分带到最终文件中。

-fdata-sections -ffunction-sections -Wl,--gc-sections

我们将编译命令修改为:
gcc -c -O3 -fdata-sections -ffunction-sections notmain.c
gcc -O3 -Wl,--gc-sections notmain.o main.c

拆卸notmain.o:

objdump -D notmain.o

输出包含:

Disassembly of section .text.f1:
0000000000000000 <f1>:
   0:   f3 0f 1e fa             endbr64
   4:   8d 47 01                lea    0x1(%rdi),%eax
   7:   c3                      ret
Disassembly of section .text.f2:
0000000000000000 <f2>:
   0:   f3 0f 1e fa             endbr64
   4:   8d 47 02                lea    0x2(%rdi),%eax
   7:   c3                      ret
Disassembly of section .data.i2:
0000000000000000 <i2>:
   0:   02 00                   add    (%rax),%al
        ...
Disassembly of section .data.i1:
0000000000000000 <i1>:
   0:   01 00                   add    %eax,(%rax)

因此,我们看到了如何根据符号本身的名称来命名自己的section。

拆卸notmain.o:

objdump -D a.out

输出包含:

Disassembly of section .text:
0000000000001040 <main>:
    1040:       f3 0f 1e fa             endbr64
    1044:       48 83 ec 08             sub    $0x8,%rsp
    1048:       e8 03 01 00 00          call   1150 <f1>
    104d:       03 05 b5 2f 00 00       add    0x2fb5(%rip),%eax        # 4008 <i1>
    1053:       48 83 c4 08             add    $0x8,%rsp
    1057:       c3                      ret
    1058:       0f 1f 84 00 00 00 00    nopl   0x0(%rax,%rax,1)
    105f:       00
0000000000001150 <f1>:
    1150:       f3 0f 1e fa             endbr64
    1154:       8d 47 01                lea    0x1(%rdi),%eax
    1157:       c3                      ret
Disassembly of section .data:
0000000000004008 <i1>:
    4008:       01 00                   add    %eax,(%rax)

,不包含i2f2。这是因为这次每个符号都在自己的section中,所以-Wl,--gc-sections能够删除每个未使用的符号。

内联使符号不被视为已使用

为了测试内联的效果,让我们将测试符号移动到与main.c相同的文件:

main2.c

int i1 = 1;
int i2 = 2;
int f1(int i) {
    return i + 1;
}
int f2(int i) {
    return i + 2;
}
int main(int argc, char **argv) {
    return f1(argc) + i1;
}

然后:

gcc -c -O3 main2.c
gcc -O3 -Wl,--gc-sections -o main2.out main2.o

拆卸main2.o:

objdump -D main2.o

输出包含:

Disassembly of section .text:
0000000000000000 <f1>:
   0:   f3 0f 1e fa             endbr64
   4:   8d 47 01                lea    0x1(%rdi),%eax
   7:   c3                      ret
   8:   0f 1f 84 00 00 00 00    nopl   0x0(%rax,%rax,1)
   f:   00
0000000000000010 <f2>:
  10:   f3 0f 1e fa             endbr64
  14:   8d 47 02                lea    0x2(%rdi),%eax
  17:   c3                      ret
Disassembly of section .data:
0000000000000000 <i2>:
   0:   02 00                   add    (%rax),%al
        ...
0000000000000004 <i1>:
   4:   01 00                   add    %eax,(%rax)
        ...
Disassembly of section .text.startup:
0000000000000000 <main>:
   0:   f3 0f 1e fa             endbr64
   4:   8b 05 00 00 00 00       mov    0x0(%rip),%eax        # a <main+0xa>
   a:   8d 44 38 01             lea    0x1(%rax,%rdi,1),%eax
   e:   c3                      ret

有趣的是main是如何在一个单独的.text.startup部分上的,可能是为了允许其余的文本被GC'ed。

我们还看到f1lea 0x1(%rax,%rdi,1),%eax上完全内联(直接添加1),而由于我不理解的原因,i1仍然在mov 0x0(%rip),%eax上使用等待重定位,参见:链接器做什么?拆下main2.out后,重新定位就清楚了。

拆卸main2.out:

objdump -D main2.out

输出包含:

Disassembly of section .text:
0000000000001040 <main>:
    1040:       f3 0f 1e fa             endbr64
    1044:       8b 05 c2 2f 00 00       mov    0x2fc2(%rip),%eax        # 400c <i1>
    104a:       8d 44 38 01             lea    0x1(%rax,%rdi,1),%eax
    104e:       c3                      ret
    104f:       90                      nop
Disassembly of section .data:
0000000000004008 <i2>:
    4008:       02 00                   add    (%rax),%al
        ...
000000000000400c <i1>:
    400c:       01 00                   add    %eax,(%rax)

f1f2被完全删除,因为f1是内联的,因此不再标记为已使用,所以整个.text部分被删除。

如果我们强制f1不内联:

int __attribute__ ((noinline)) f1(int i) {
    return i + 1;
}

f1f2都将出现在main2.out上。

不同目标文件的节是分开的,即使它们具有相同的名称

很明显,例如:

notmain2.c

int i3 = 3;
int i4 = 4;
int f3(int i) {
    return i + 3;
}
int f4(int i) {
    return i + 4;
}

然后:

gcc -c -O3 notmain.c
gcc -c -O3 notmain2.c
gcc -O3 -Wl,--gc-sections notmain.o notmain2.o main.c
objdump -D a.out

不包含f3f4,尽管包含了f1f2,并且它们都没有称为.text的章节。

-fdata-sections -ffunction-sections -Wl,--gc-sections可能的缺点:链接速度较慢

我们应该找到一些基准,但这是可能的,因为当一个符号引用来自同一编译单元的另一个符号时,需要进行更多的重定位,因为它们不再存在于独立的section中。

-flto导致符号被删除,即使在同一编译单元中使用了其他符号

同样,无论LTO是否会导致内联发生,都会发生这种情况。考虑:

notmain.c

int i1 = 1;
int i2 = 2;
int __attribute__ ((noinline)) f1(int i) {
    return i + 1;
}
int f2(int i) {
    return i + 2;
}

c

extern int i1;
int f1(int i);
int main(int argc, char **argv) {
    return f1(argc) + i1;
}

编译和反汇编:

gcc -c -O3 -flto notmain.c
gcc -O3 -flto notmain.o main.c
objdump -D a.out

反汇编包含:

Disassembly of section .text:
0000000000001040 <main>:
    1040:       f3 0f 1e fa             endbr64
    1044:       e8 f7 00 00 00          call   1140 <f1>
    1049:       83 c0 01                add    $0x1,%eax
    104c:       c3                      ret
0000000000001140 <f1>:
    1140:       8d 47 01                lea    0x1(%rdi),%eax
    1143:       c3                      ret

f2不存在。因此,即使使用了f1, f2也被删除了。

我们还注意到i1i2都不见了。编译器似乎认识到i1从未真正修改过,只是"内联"。在:add $0x1,%eax处为常数1

相关问题:GCC LTO执行跨文件死代码消除吗?由于某种原因,代码消除不会发生,如果你用-O0编译目标文件:为什么GCC在用- 0编译目标文件时不使用LTO进行函数死代码消除?

在Ubuntu 23.04 amd64, GCC 12.2.0上测试。

您可以在对象文件上使用条带二进制文件(例如:可执行文件)以从其中剥离所有符号。