链接器是否更倾向于静态符号还是动态符号?

9

我有两个头文件和两个cpp文件:

//f1.h
int f1();

//f1.cpp
include "f1.h"
int f1() {return 1;}

//f2.h
int f2();

//f2.cpp
#include "f2.h"
#include "f1.h"
int f2() {return f1() + 1;}

//main.cpp
#include "f2.h"
int main() {return f2();}

首先,我从f1f2编译共享对象,并根据该共享对象创建一个来自于main.cpp的二进制文件:

g++ -c -fPIC -shared f1.cpp f2.cpp
g++ -shared -fPIC -o libf.so f2.o f1.o
g++ -o dynamic main.cpp libf.so

现在我对做了一些更改(比如将的返回值改为<2>):
//f1.cpp#
include "f1.h"
int f1() {return 2;}

按照以下方式编译二进制文件:

g++ -o semistatic main.cpp f1.cpp libf.so

问题是“semistatic”二进制文件是否会使用来自“libf”的f1()的定义(其中f1返回1),还是它将使用静态链接符号(其中f1返回2)? 这在不同系统上是否有所不同,我能否指望它在单个系统中保持一致?

3
你已经找到了一个曲折的方法来破坏One Definition Rule。参见https://en.cppreference.com/w/cpp/language/definition - Richard Critten
为什么链接器不会抱怨有重复的符号? - Sander De Dycker
1个回答

3
正如已经指出的,您正在违反一次定义规则。这并不是世界末日,但在这种情况下,C++标准没有任何保证会发生什么,行为取决于链接器和加载器的实现细节。
工具链和操作系统非常不同,因此上述内容甚至无法在Windows上链接。但如果您谈论的是带有通常的链接器/加载器对的Linux,则行为将是使用更改后的版本,并且它将是每个Linux安装的相同版本。
这是链接器/加载器在Linux上的工作方式(例如,LD_PRELOAD-trick广泛使用此行为):
  • *.so中的符号是弱的,因此如果链接器在其他地方找到另一个定义(在您的情况下是在更新版本的f1.o中),则会忽略*.so中的定义。
  • 在运行时,如果符号已经绑定了,即已知另一个定义,则加载程序将忽略共享对象中的定义。在您的情况下,符号f1(由于名称混淆,它将具有不同的名称,但为简单起见,让我们忽略这一点)已经绑定到主程序中的定义,因此在调用*.so中的f1时将使用该定义。

然而,这种做法非常脆弱,一些小的变化就可能导致不同的结果。

A:将可见性更改为隐藏。

建议隐藏不属于公共接口的符号。

__attribute__ ((visibility ("hidden")))
int f1() {return 1;}

在这种情况下,使用的不是被覆盖版本,而是旧版本。区别在于,当链接器看到使用隐藏符号时,它不再将其委托给加载器来解析符号的地址,而是直接使用手头的地址。后来,我们无法更改调用哪个定义。
如果使f1成为内联函数,则会导致非常有趣的事情,因为在共享对象的某些部分中将使用旧版本,在某些部分中将使用新版本。 -fPIC防止未标记为inline的函数内联,因此上述内容仅适用于明确标记为内联的函数。
简而言之:这个技巧可以在Linux上使用。然而,在更大的项目中,您不想增加复杂性,并尝试坚持更可持续和简单的一个定义规则框架。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接