C++链接是否足够智能以避免未使用的库链接?

ida*_*hmu 43 c++ linker

我还远未完全理解C++链接器的工作原理,我对此有一个特定的问题.

说我有以下内容:

Utils.h

namespace Utils
{
    void func1();
    void func2();
}
Run Code Online (Sandbox Code Playgroud)

Utils.cpp

#include "some_huge_lib" // needed only by func2()

namespace Utils
{
    void func1() { /* do something */ }
    void func2() { /* make use of some functions defined in some_huge_lib */ }
}
Run Code Online (Sandbox Code Playgroud)

main.cpp中

int main()
{
  Utils::func1()
}
Run Code Online (Sandbox Code Playgroud)

我的目标是生成尽可能小的二进制文件.

我的问题是,是否some_huge_lib会包含在输出对象文件中?

Mar*_* A. 37

包含或链接大型库通常不会有所作为,除非你使用那些东西.链接器应该执行死代码消除,从而确保在构建时不会获得包含大量未使用代码的大型二进制文件(阅读编译器/链接器手册以了解更多信息,这不是C++标准强制执行的).

包含大量的头文件也不会增加你的二进制文件大小(但它可能会大大增加你的编译时间,cfr.预编译头文件).一些例外代表全局对象和动态库(那些不能被剥离).我还建议阅读这篇关于将代码分成多个部分的文章(仅限gcc).

关于性能的最后一个注意事项:如果你使用了很多依赖于位置的代码(即代码不能只映射到具有相对偏移的任何地址,但需要通过重定位或类似的表来进行'hotpatching')那么将会有启动成本.

  • 这只适用于静态链接.共享库需要包含可见符号.对于gcc:https://gcc.gnu.org/ml/gcc-help/2003-08/msg00128.html (7认同)

Tom*_*ner 23

这取决于一个很大的什么工具,然后切换,以链接和编译使用.

首先,如果链接some_huge_lib作为共享库,则需要在链接共享库时解析所有代码和依赖项.所以,是的,它会被拉到某个地方.

如果您链接some_huge_lib为存档,那么 - 它取决于.读者的理智是将func1和func2放在单独的源代码文件中是很好的做法,在这种情况下,通常链接器将能够忽略未使用的目标文件及其依赖项.

但是,如果您在同一个文件中同时拥有这两个函数,那么在某些编译器上,您需要告诉它们为每个函数生成单独的部分.有些编译器会自动执行此操作,有些编译器根本不执行此操作.如果你没有这个选项,拉入func1将获取func2的所有代码,并且需要解决所有依赖项.

  • 这是正确的答案.不同的链接器表现不同,并受所用标志的影响. (4认同)

Adi*_*vit 7

将每个函数视为图中的节点.
每个节点都与一段二进制代码相关联 - 节点功能的编译二进制代码.
如果一个节点(函数)依赖于(调用)另一个节点(函数),则在两个节点之间存在链接(有向边).

静态库主要是此类节点的列表(+索引).

程序起始节点main()功能.
连接器从穿过图main(),并链接到那些从到达的可执行文件的所有节点main().这就是它被称为链接器的原因(链接映射可执行文件中的函数调用地址).

未使用的函数,没有来自图中节点的链接main().
因此,这种断开连接的节点不可访问,并且不包括在最终可执行文件中.

可执行文件(与静态库相对)主要是可从中找到的所有节点的列表main()(+索引和启动代码等).


ach*_*ach 5

除了其他回复之外,必须说通常链接器是按部分而不是功能工作的.

编译器通常可以配置它是否将所有目标代码放入一个整体部分或将其拆分为多个较小的部分.例如,用于打开拆分的GCC选项是-ffunction-sections(用于代码)和-fdata-sections(用于数据); MSVC选项是/Gy(两者都有).-fnofunction-sections,-fnodata-sections,/Gy-分别把所有代码或数据到一个部分.

您可以"玩"在两种模式下编译模块,然后转储它们(objdump对于GCC,dumpbin对于MSVC)以查看生成的对象文件结构.

一旦编译器形成一个部分,对于链接器它就是一个单元.节定义符号并引用其他节中定义的符号.链接器将在各部分之间建立依赖关系图(从多个根开始),然后解散或完全保留它们中的每一个.因此,如果某个部分中有已使用和未使用的函数,则将保留未使用的函数.

两种模式都有利有弊.转换拆分意味着较小的可执行文件,但较大的目标文件和较长的链接时间.

还必须注意的是,在C++中,与C不同,在某些情况下放宽了一个定义规则,并且允许函数或数据对象的多个定义(例如,在内联函数的情况下).规则的制定方式允许链接器选择任何定义.

从部分的角度来看,将内联函数与非内联函数放在一起意味着在典型的使用场景中,链接器通常会被强制保留每个内联函数的几乎每个定义; 这意味着过多的代码膨胀.因此,无论编译器命令行选项如何,这些函数和数据通常都放在它们自己的部分中.

更新:正如@janm在他的评论中正确提醒的那样,还必须指示链接器通过指定--gc-sections(GNU)或/opt:ref(MS)来删除未引用的部分.