将Linux上的共享lib与重复但已修改的类/结构链接会导致segfault
Linking shared lib on Linux with duplicate yet modified class/struct causes segfault
我很难理解在运行时加载动态库时到底会发生什么,以及动态链接器如何识别和处理"相同的符号";。
我读过其他与符号链接有关的问题,并观察了所有典型的建议(使用外部"C",链接库时使用-fPIC等)。据我所知,到目前为止,我的具体问题还没有讨论。论文";如何编写共享库";https://www.akkadia.org/drepper/dsohowto.pdf确实讨论了解决库符号依赖关系的过程,这可能解释了我下面的例子中发生的事情,但遗憾的是,它没有提供解决方法。
我发现一篇帖子,最后一条(不幸的)未回复的评论与我的问题非常相似:
加载具有相同符号的两个共享库时是否存在符号冲突
唯一的区别是:在我的例子中,符号是一个自动生成的构造函数。
以下是设置(Linux):
- 程序"主";使用一些库类声明";"假人";具有4个成员变量,并通过dlopen()动态加载共享库,并使用dlsym()解析两个简单函数
- 共享库";从";还使用具有类"的库;Dummy";,但在具有5个成员变量(额外字符串)的较新版本中
- 当从master调用共享库的函数时,访问Dummy-segfault类中新添加的字符串成员-显然该字符串没有正确初始化
我的假设是:类Dummy的构造函数已经存在于内存中,因为master本身使用此函数,并且在加载共享库时,它不会加载自己版本的构造函数,而是简单地重新使用master的现有版本。通过这样做,额外的字符串变量在构造函数中没有正确初始化,并对其进行访问。
当在从机中初始化Dummy变量d时调试到汇编程序代码中时,实际上正在调用主机内存空间内的Dummy构造函数。
问题:
-
动态链接器(dlopen()?)认识到,用于编译master的类Dummy应该与编译到Slave的Dummy相同,尽管它是在库中提供的?为什么符号查找采用构造函数的master变体,即使符号表也必须包含从库导入的构造函数符号?
-
有没有一种方法,例如,将一些合适的选项传递给dlopen()或dlsym(),以强制使用Slave自己的Dummy构造函数,而不是来自Master的构造函数(即调整符号查找/重新分配行为)?
代码:完整的极简主义源代码示例可以在这里找到:
https://bauklimatik-dresden.de/privat/nicolai/tmp/master-slave-test.tar.bz2
Master中的相关共享lib加载代码:
#include <iostream>
#include <dlfcn.h> // shared library loading on Unix systems
#include "Dummy.h"
int create(void * &data);
typedef int F_create(void * &data);
int destroy(void * data);
typedef int F_destroy(void * data);
int main() {
// use dummy class at least once in program to create constructor
Dummy d;
d.m_c = "Test";
// now load dynamic library
void *soHandle = dlopen( "libSlave.so", RTLD_LAZY );
std::cout << "Library handle 'libSlave.so': " << soHandle << std::endl;
if (soHandle == nullptr)
return 1;
// now load constructor and destructor functions
F_create * createFn = reinterpret_cast<F_create*>(dlsym( soHandle, "create" ) );
F_destroy * destroyFn = reinterpret_cast<F_destroy*>(dlsym( soHandle, "destroy" ) );
void * data;
createFn(data);
destroyFn(data);
return 0;
}
Class Dummy:没有";EXTRA_ STRING";在Master中使用,在Slave 中使用额外字符串
#ifndef DUMMY_H
#define DUMMY_H
#include <string>
#define EXTRA_STRING
class Dummy {
public:
double m_a;
int m_b;
std::string m_c;
#ifdef EXTRA_STRING
std::string m_c2;
#endif // EXTRA_STRING
double m_d;
};
#endif // DUMMY_H
注意:如果我在Master和Slave中都使用了完全相同的类Dummy,代码就会正常工作(正如预期的那样)。
当在从设备中初始化Dummy变量d时调试到汇编程序代码中时,实际上正在调用主设备内存空间中的Dummy构造函数。
这是UNIX上预期的行为。与Windows DLL不同,UNIX共享库被设计为模仿归档库,并且而不是被设计为独立的代码单元。
动态链接器(dlopen()?)认识到,用于编译master的类Dummy应该与编译到Slave的Dummy相同,尽管它是在库中提供的?为什么符号查找采用构造函数的master变体,即使符号表也必须包含从库导入的构造函数符号?
动态加载程序不关心(或知道任何)任何类。它操作符号。
默认情况下,符号解析为动态加载程序可见的任何给定符号(导出符号)的第一个定义。
您可以使用nm -CD Master
和nm -CD libSlave.so
检查从任何给定二进制文件导出的符号集。
有没有办法,例如,将一些合适的选项传递给dlopen()或dlsym(),以强制使用Slave自己的Dummy构造函数,而不是来自Master的Dummy构造函数(即调整符号查找/重新分配行为)?
有几种方法可以修改默认行为。
最好的方法是让libSlave.so
使用自己的命名空间。这将更改所有(损坏的)符号名称,并将完全消除任何冲突。
下一个最好的方法是限制从libSlave.so
导出的符号集,方法是使用-fvisibility=hidden
进行编译,并将显式__attribute__((visibility("default")))
添加到该库中必须可见的(少数)函数中(在您的示例中为create
和destroy
)。
另一种可能的方法是将libSlave.so
与-Wl,-Bsymbolic
标志链接起来,认为符号解析规则非常复杂,非常快,除非你全部理解,否则最好避免这样做。
p.S.人们可能想知道为什么Master
二进制文件会导出任何符号——通常只导出链接期间使用的其他.so
引用的符号。
发生这种情况是因为cmake
在链接主可执行文件时使用-rdynamic
为什么会这样,我不知道。
所以另一个解决方法是:不要使用cmake
(或者至少不要使用它使用的默认标志)。
我遵循了上一个答案中的建议,在加载具有相同符号的两个共享库时是否存在符号冲突:
- 运行'nm-Master'和'nm-libSlave.so'显示了相同的自动生成的构造函数符号:
...
000000000000612a W _ZN5DummyC1EOS_
00000000000056ae W _ZN5DummyC1ERKS_
0000000000004fe8 W _ZN5DummyC1Ev
...
因此,损坏的函数签名在主二进制文件和从二进制文件中都匹配。
加载库时,将使用master的函数,而不是库的版本。为了进一步研究这一点,我创建了一个更为简约的例子,就像上面引用的帖子一样:
master.cpp
#include <iostream>
#include <dlfcn.h> // shared library loading on Unix systems
// prototype for imported slave function
void hello();
typedef void F_hello();
void printHello() {
std::cout << "Hello world from master" << std::endl;
}
int main() {
printHello();
// now load dynamic library
void *soHandle = nullptr;
const char * const sharedLibPath = "libSlave.so";
// I tested different RTLD_xxx options, see text for explanations
soHandle = dlopen( sharedLibPath, RTLD_NOW | RTLD_DEEPBIND);
if (soHandle == nullptr)
return 1;
// now load shared lib function and execute it
F_hello * helloFn = reinterpret_cast<F_hello*>(dlsym( soHandle, "hello" ) );
helloFn();
return 0;
}
从.h
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
void hello();
#ifdef __cplusplus
}
#endif
slave.cpp
#include "slave.h"
#include <iostream>
void printHello() {
std::cout << "Hello world from slave" << std::endl;
}
void hello() {
printHello(); // should call our own hello() function
}
您注意到库和主控台中都存在相同的函数printHello()
。
这次我手动编译了(没有CMake)和以下标志:
# build master
/usr/bin/c++ -fPIC -o tmp/master.o -c master.cpp
/usr/bin/c++ -rdynamic tmp/master.o -o Master -ldl
# build slave
/usr/bin/c++ -fPIC -o tmp/slave.o -c slave.cpp
/usr/bin/c++ -fPIC -shared -Wl,-soname,libSlave.so -o libSlave.so tmp/slave.o
注意在主库和从库中使用-fPIC
。
我现在尝试了RTLD_xx标志和编译标志的几种组合:
1.
dlopen()标志:RTLD_NOW|RTLD_DEEPBIND-两个libs 的fPIC
Hello world from master
Hello world from slave
->结果如预期(这是我想要实现的)
2.
dlopen()标志:RTLD_NOW|RTLD_DEEPBIND-fPIC仅适用于库
Hello world from master
Speicherzugriffsfehler (Speicherabzug geschrieben) ./Master
->这里,在iostream库cout
调用的行中发生segfault;尽管如此,库中的printHello()
函数仍被称为
3.
dlopen()标志:RTLD_NOW-fPIC仅适用于库
Hello world from master
Hello world from master
->这是我最初的行为;所以RTLD_DEEPBIND绝对是我所需要的,它与master二进制文件中的-fPIC结合在一起;
注意:虽然CMake在构建共享库时会自动添加-fPIC,但对于可执行文件,它通常不会这样做;在这里,当使用CMake 构建时,您需要手动添加此标志
注2:使用RTLD_NOW或RTLD_LAZY没有区别。
在上使用-fPIC的组合可执行程序和共享库,以及RTLD_DEEPBIND可以使具有不同Dummy类的原始示例顺利工作。
- clang:错误:链接器命令失败,退出代码为 1(使用 -v 查看调用) - 体系结构的未定义符号 x86_64:
- 使用 trie 数据结构链接不同类型的信息
- 体系结构x86_64的未定义符号:链接器错误
- C++标准是否定义了结构中成员函数的函数内定义是否必须具有静态链接?
- 在结构的嵌套映射上链接运算符 []
- C++ XCODE ld:找不到体系结构x86_64 clang 的符号:错误:链接器命令失败,退出代码为 1(使用 -
- Jetson 工具链文件夹结构和交叉编译时的 libgomp 链接器错误
- 通过自定义结构和链接列表类中的C 中的哈希表
- 如何使用模板在数据结构中链接节点对象
- C++编译错误:ld:找不到体系结构x86_64 clang 的符号:错误:链接器命令失败,退出代码为 1(使用 -v
- C++ 中的链接结构数组
- CLang++ 链接器未定义的符号用于体系结构x86_64
- ld:找不到体系结构x86_64 clang 的符号:错误:链接器命令失败,退出代码为 1(使用 -v 查看调用)
- C、链接数据结构和双指针行为
- 双链接结构列表的内存泄漏
- 具有 C 链接和C++实现的不透明结构
- 链接列表C ,重载[],整体结构
- 使用模板在类(双重链接列表)中定义结构
- 链接的列表,结构和类
- 如何在结构向量内部生成结构链接列表