如何使用 GCC 和 ld 删除未使用的 C/C++ 符号?
Posted
技术标签:
【中文标题】如何使用 GCC 和 ld 删除未使用的 C/C++ 符号?【英文标题】:How to remove unused C/C++ symbols with GCC and ld? 【发布时间】:2011-10-04 23:35:21 【问题描述】:我需要严格优化我的可执行文件的大小(ARM
开发)和
我注意到在我当前的构建方案中 (gcc
+ ld
) 未使用的符号没有被剥离。
arm-strip --strip-unneeded
用于生成的可执行文件/库不会改变可执行文件的输出大小(我不知道为什么,也许它根本不能)。 p>
(如果存在) 修改我的构建管道的方式是什么,以便从结果文件中删除未使用的符号?
我什至不会想到这一点,但我目前的嵌入式环境不是很“强大”,而且
即使从2M
中保存500K
也会带来非常好的加载性能提升。
更新:
不幸的是,我使用的当前gcc
版本没有-dead-strip
选项,而ld
的-ffunction-sections... + --gc-sections
对结果输出没有任何显着差异。
我很震惊这甚至成为了一个问题,因为我确信 gcc + ld
应该自动去除未使用的符号(他们为什么还要保留它们?)。
【问题讨论】:
你怎么知道没有使用符号? 未在任何地方引用 => 未在最终应用程序中使用。我认为在编译/链接时构建调用图应该不会很困难。 您是想通过删除无效的符号来减小 .o 文件的大小,还是在加载到可执行内存后尝试减小实际代码占用空间的大小?您说“嵌入”的事实暗示了后者;你问的问题似乎集中在前者。 @Ira 我正在尝试减小输出可执行文件的大小,因为 (作为示例) 如果我尝试移植一些使用boost
库的现有应用程序,生成的 .exe
文件包含许多未使用的目标文件,并且由于我当前嵌入式运行时的规范,启动 10mb
应用程序比启动 500k
应用程序需要更长的时间。
@Yippie:你想摆脱代码以最小化加载时间;您要删除的代码是未使用的方法/等。从图书馆。是的,您需要构建一个调用图来执行此操作。这并不容易;它必须是一个全局调用图,它必须是保守的(不能删除可能会被使用的东西)并且必须是准确的(所以你有一个接近理想的调用图,所以你真的知道什么不是用过的)。最大的问题是做一个全局的、准确的调用图。不知道有多少编译器可以做到这一点,更不用说链接器了。
【参考方案1】:
对于 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
(注意 -Os 是一个额外的编译器标志,告诉 GCC 优化大小)
【讨论】:
请注意,根据 GCC 的选项描述(我测试过),这会减慢可执行文件的速度。 对于mingw
,当静态静态链接 libstdc++ 和 libgcc 与标志 -static
时,这不起作用。链接器选项-strip-all
有很大帮助,但生成的可执行文件(或 dll)仍然比 Visual Studio 生成的要大 4 倍。关键是,我无法控制 libstdc++
的编译方式。应该有一个 ld
唯一选项。【参考方案2】:
如果要相信this thread,您需要将-ffunction-sections
and -fdata-sections
提供给gcc,它会将每个函数和数据对象放在其自己的部分中。然后你给 GNU ld 和 --gc-sections
以删除未使用的部分。
【讨论】:
@MSalters:这不是默认设置,因为它违反了 C 和 C++ 标准。突然全局初始化没有发生,这让一些程序员感到非常惊讶。 @MSalters:仅当您通过非标准的破坏行为选项时,您建议将其设为默认行为。 @MSalters:如果您可以制作一个运行静态初始化程序的补丁,当且仅当副作用对于程序的正确运行是必需的,那将是非常棒的。不幸的是,我认为完美地做到这一点通常需要解决停止问题,所以你可能需要在有时包含一些额外的符号方面犯错。这基本上就是艾拉在他的问题中所说的。 (顺便说一句:“对程序的正确操作不是必需的”是“未使用”的不同定义,而不是该术语在标准中的使用方式) @BenVoigt 在 C 中,全局初始化不能有副作用(初始化器必须是常量表达式) @Matt:但在 C++ 中并非如此......而且它们共享相同的链接器。【参考方案3】:您需要检查您的文档以了解您的 gcc 和 ld 版本:
但是对我来说(OS X gcc 4.0.1)我发现这些用于 ld
-dead_strip
删除入口点或导出符号无法访问的函数和数据。
-dead_strip_dylibs
删除入口点或导出符号无法访问的动态库。也就是说,抑制在链接期间不提供符号的 dylib 的加载命令命令的生成。当链接到由于某些间接原因(例如 dylib 具有重要的初始化程序)在运行时需要的 dylib 时,不应使用此选项。
还有这个有用的选项
-why_live symbol_name
记录对 symbol_name 的引用链。仅适用于
-dead_strip
。它可以帮助调试为什么你认为应该删除的东西没有被删除。
在 gcc/g++ 手册中还有一个注释,只有在编译时启用优化时才会执行某些类型的死代码消除。
虽然这些选项/条件可能不适用于您的编译器,但我建议您在文档中查找类似的内容。
【讨论】:
这似乎与mingw
无关。
-dead_strip
不是gcc
选项。
-dead_strip
由 gcc
传递给链接器,但仅在 Darwin (macOS) 上受支持。
那是因为 macOS 没有 gcc,它有 clang。而且 clang 有一个兼容层,你可以称之为 gcc。【参考方案4】:
编程习惯也有帮助;例如将static
添加到在特定文件之外无法访问的函数;为符号使用较短的名称(可能会有所帮助,可能不会太多);尽可能使用const char x[]
; ...this paper,虽然它谈到动态共享对象,但可以包含一些建议,如果遵循这些建议,可以帮助您缩小最终的二进制输出大小(如果您的目标是 ELF)。
【讨论】:
为符号选择较短的名称有何帮助? 如果符号没有被剥离,ça va sans dire——但现在似乎需要说出来。 @fuz 该论文讨论的是动态共享对象(例如 Linux 上的.so
),因此必须保留符号名称,以便像 Python 的 ctypes
FFI 模块这样的 API 可以使用它们来在运行时按名称查找符号。【参考方案5】:
答案是-flto
。您必须将它传递给您的编译和链接步骤,否则它不会做任何事情。
它实际上运行良好 - 将我编写的微控制器程序的大小减少到以前大小的 50% 以下!
不幸的是,它确实看起来有点错误 - 我有一些事情没有正确构建的实例。这可能是由于我正在使用的构建系统(QBS;它非常新),但无论如何我建议您尽可能只在最终构建中启用它,并彻底测试该构建。
【讨论】:
"-Wl,--gc-sections" 不适用于 MinGW-W64,"-flto" 适合我。谢谢 输出程序集很奇怪-flto
我不明白它在幕后做了什么。
我相信-flto
不会将每个文件编译为程序集,而是将它们编译为 LLVM IR,然后最终链接将它们编译为就好像它们都在一个编译单元中一样。这意味着它可以消除未使用的函数和内联非static
的函数,可能还有其他东西。见llvm.org/docs/LinkTimeOptimization.html【参考方案6】:
虽然不是严格意义上的符号,但如果要考虑大小 - 始终使用 -Os
和 -s
标志进行编译。 -Os
优化生成的代码以获得最小的可执行文件大小,-s
从可执行文件中删除符号表和重定位信息。
有时 - 如果需要小尺寸 - 使用不同的优化标志可能 - 也可能不 - 有意义。例如切换-ffast-math
和/或-fomit-frame-pointer
有时甚至可以为您节省几十个字节。
【讨论】:
只要你遵守语言标准,大多数优化调整仍然会产生正确的代码,但我有-ffast-math
在完全符合标准的 C++ 代码中造成严重破坏,所以我永远不会推荐它.【参考方案7】:
在我看来,Nemo 提供的答案是正确的。如果这些说明不起作用,则问题可能与您使用的 gcc/ld 版本有关,作为练习,我使用详细说明 here
编译了一个示例程序#include <stdio.h>
void deadcode() printf("This is d dead codez\n");
int main(void) printf("This is main\n"); 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)
【讨论】:
【参考方案8】:strip --strip-unneeded
仅对可执行文件的符号表进行操作。它实际上并没有删除任何可执行代码。
标准库通过将它们的所有函数拆分为单独的目标文件来实现您所追求的结果,这些目标文件使用ar
组合在一起。如果您随后将生成的存档链接为库(即,将选项 -l your_library
赋予 ld),则 ld 将仅包含目标文件,因此也包含实际使用的符号。
您还可以找到对此similar question 使用的一些回复。
【讨论】:
库中单独的目标文件仅在进行静态链接时才相关。使用共享库时,整个库都会被加载,但当然不会包含在可执行文件中。【参考方案9】:我不知道这是否有助于解决您当前的困境,因为这是一个最近的功能,但您可以以全局方式指定符号的可见性。在编译时传递-fvisibility=hidden -fvisibility-inlines-hidden
可以帮助链接器稍后摆脱不需要的符号。如果您正在生成可执行文件(而不是共享库),则无需再做任何事情。
the GCC wiki 上提供了更多信息(以及针对例如库的细粒度方法)。
【讨论】:
【参考方案10】:来自 GCC 4.2.1 手册,-fwhole-program
部分:
假设当前编译单元代表正在编译的整个程序。除了
main
和那些由属性externally_visible
合并的公共函数和变量之外的所有公共函数和变量都成为静态函数,并且在效果上会被过程间优化器更积极地优化。虽然此选项等效于为由单个文件组成的程序正确使用static
关键字,但与选项--combine
结合使用时,此标志可用于编译大多数较小规模的 C 程序,因为函数和变量对整体而言是局部的组合编译单元,不适用于单个源文件本身。
【讨论】:
是的,但这可能不适用于任何类型的增量编译,并且可能会有点慢。 @Timmmm:我怀疑你在想-flto
。
是的!后来我发现(为什么没有任何答案?)。不幸的是,它似乎有点错误,所以我只推荐它用于最终构建,然后对构建进行大量测试!【参考方案11】:
您可以在目标文件(例如可执行文件)上使用条带二进制文件从其中删除所有符号。
注意:它会更改文件本身并且不会创建副本。
【讨论】:
以上是关于如何使用 GCC 和 ld 删除未使用的 C/C++ 符号?的主要内容,如果未能解决你的问题,请参考以下文章