如何用GCC和ld删除未使用的C/ c++符号
How to remove unused C/C++ symbols with GCC and ld?
我需要严格优化可执行文件的大小(ARM
开发)和我注意到,在我目前的构建方案(gcc
+ ld
)未使用的符号没有被剥离。
使用arm-strip --strip-unneeded
的结果可执行文件/库不会改变可执行文件的输出大小(我不知道为什么,也许它根本不能)。
(如果存在)如何修改我的构建管道,以便从结果文件中剥离未使用的符号?
我甚至不会想到这一点,但我目前的嵌入式环境不是很"强大"即使将500K
从2M
中保存出来,也会使加载性能得到非常好的提升。
更新:
不幸的是,目前我使用的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)
i2
和f2
都出现在最终的输出文件中,尽管它们没有被使用。
即使我们将-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)
,不包含i2
和f2
。这是因为这次每个符号都在自己的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。
我们还看到f1
在lea 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)
和f1
和f2
被完全删除,因为f1
是内联的,因此不再标记为已使用,所以整个.text
部分被删除。
如果我们强制f1
不内联:
int __attribute__ ((noinline)) f1(int i) {
return i + 1;
}
则f1
和f2
都将出现在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
不包含f3
和f4
,尽管包含了f1
和f2
,并且它们都没有称为.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
也被删除了。
我们还注意到i1
和i2
都不见了。编译器似乎认识到i1
从未真正修改过,只是"内联"。在:add $0x1,%eax
处为常数1
。
相关问题:GCC LTO执行跨文件死代码消除吗?由于某种原因,代码消除不会发生,如果你用-O0
编译目标文件:为什么GCC在用- 0编译目标文件时不使用LTO进行函数死代码消除?
在Ubuntu 23.04 amd64, GCC 12.2.0上测试。
您可以在对象文件上使用条带二进制文件(例如:可执行文件)以从其中剥离所有符号。
- 在C++中,将大的无符号浮点数四舍五入为整数的最佳方法是什么
- 有符号的int和int-有没有一种方法可以在C++中区分它们
- 将无符号char*转换为std::istream*C++
- 如何在C++中将一个无符号的 int 转换为两个无符号的短裤?
- vscode g++链路故障:体系结构x86_64的未定义符号
- 从矢量<无符号字符>转换为字符* 包括垃圾数据
- 如何理解将半精度指针转换为无符号长指针和相关的内存对齐
- Visual studio代码重构似乎不起作用(例如,重命名符号-f2)
- 使用gcc从静态链接的文件中查找可选符号
- C++中无符号字符溢出
- 使用无符号字符数组有效存储内存
- C++:Application.cpp中抛出了未解析的外部符号(解决方案在问题的末尾,供未来的读者参考)
- VC++本机单元测试,找不到调试符号
- 为什么我必须在C++中添加一个赋值符号来声明一个数组
- 检查TCHAR数组输入是否为带符号整数C++
- 用符号版本替换对函数的所有调用
- 未解析的外部符号_MsiLocateComponentW@12.
- 如何打印boost多精度128位无符号整数
- C++模板函数,用于比较任何无符号整数和有符号整数
- 在 Mac 上使用 CMAKE 将 FFTW 和 FFTWPP 链接到项目中时未定义的符号