将Linux上的共享lib与重复但已修改的类/结构链接会导致segfault

Linking shared lib on Linux with duplicate yet modified class/struct causes segfault

本文关键字:结构 链接 segfault 修改 共享 Linux lib      更新时间:2024-09-22

我很难理解在运行时加载动态库时到底会发生什么,以及动态链接器如何识别和处理"相同的符号";。

我读过其他与符号链接有关的问题,并观察了所有典型的建议(使用外部"C",链接库时使用-fPIC等)。据我所知,到目前为止,我的具体问题还没有讨论。论文";如何编写共享库";https://www.akkadia.org/drepper/dsohowto.pdf确实讨论了解决库符号依赖关系的过程,这可能解释了我下面的例子中发生的事情,但遗憾的是,它没有提供解决方法。

我发现一篇帖子,最后一条(不幸的)未回复的评论与我的问题非常相似:

加载具有相同符号的两个共享库时是否存在符号冲突

唯一的区别是:在我的例子中,符号是一个自动生成的构造函数。

以下是设置(Linux):

  • 程序"主";使用一些库类声明";"假人";具有4个成员变量,并通过dlopen()动态加载共享库,并使用dlsym()解析两个简单函数
  • 共享库";从";还使用具有类"的库;Dummy";,但在具有5个成员变量(额外字符串)的较新版本中
  • 当从master调用共享库的函数时,访问Dummy-segfault类中新添加的字符串成员-显然该字符串没有正确初始化

我的假设是:类Dummy的构造函数已经存在于内存中,因为master本身使用此函数,并且在加载共享库时,它不会加载自己版本的构造函数,而是简单地重新使用master的现有版本。通过这样做,额外的字符串变量在构造函数中没有正确初始化,并对其进行访问。

当在从机中初始化Dummy变量d时调试到汇编程序代码中时,实际上正在调用主机内存空间内的Dummy构造函数。

问题:

  1. 动态链接器(dlopen()?)认识到,用于编译master的类Dummy应该与编译到Slave的Dummy相同,尽管它是在库中提供的?为什么符号查找采用构造函数的master变体,即使符号表也必须包含从库导入的构造函数符号?

  2. 有没有一种方法,例如,将一些合适的选项传递给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 Masternm -CD libSlave.so检查从任何给定二进制文件导出的符号集。

有没有办法,例如,将一些合适的选项传递给dlopen()或dlsym(),以强制使用Slave自己的Dummy构造函数,而不是来自Master的Dummy构造函数(即调整符号查找/重新分配行为)?

有几种方法可以修改默认行为。

最好的方法是让libSlave.so使用自己的命名空间。这将更改所有(损坏的)符号名称,并将完全消除任何冲突。

下一个最好的方法是限制从libSlave.so导出的符号集,方法是使用-fvisibility=hidden进行编译,并将显式__attribute__((visibility("default")))添加到该库中必须可见的(少数)函数中(在您的示例中为createdestroy)。

另一种可能的方法是将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类的原始示例顺利工作。