将 Linux 上的共享库链接到重复但已修改的 class/struct 会导致段错误

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

我无法理解到底发生了什么,在运行时加载动态库时以及动态链接器如何识别和处理“相同的符号”。

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

我发现 post 最后一条(不幸的是)未回答的评论与我的问题非常相似:

Is there symbol conflict when loading two shared libraries with a same symbol

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

这是设置 (Linux):

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

在从机中初始化 Dummy 变量 d 调试汇编代码时,确实调用了主机内存中的 Dummy 构造函数 space。

问题:

  1. 动态链接器 (dlopen()?) 如何识别,尽管提供了 class 用于编译主机的 Dummy 应该与编译到 Slave 中的 Dummy 相同在图书馆本身?为什么符号查找采用构造函数的主变体,即使符号 table 也必须包含从库中导入的构造函数符号?

  2. 有没有办法,例如通过将一些 suitable 选项传递给 dlopen() 或 dlsym() 来强制使用 Slave 自己的 Dummy 构造函数而不是来自 Master 的 Dummy 构造函数(即调整符号 lookup/reallocation 行为)?

代码:可在此处找到完整的简约源代码示例:

https://bauklimatik-dresden.de/privat/nicolai/tmp/master-slave-test.tar.bz2

Master中相关共享库加载代码:

#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 中使用完全相同的 class Dummy,则代码可以正常工作(如预期)。

When debugging into the assembler code when initializing the Dummy variable d in the slave, indeed Dummy's constructor inside the master's memory space is being called.

这是 UNIX 上的预期行为。与 Windows DLL 不同,UNIX 共享库旨在模仿存档库,而非 旨在成为 self-contained 独立的代码单元。

How does the dynamic linker (dlopen()?) recognize, that the class Dummy used to compile the master should be the same as Dummy compiled into Slave, despite it being provided in the library itself? Why does the symbol lookup take the master's variant of the constructor, even though the symbol table must also contain the constructor symbol imported from the library?

动态加载程序不关心(或不知道)任何 类。它运行 个符号 .

通过默认 符号被解析为动态加载程序可见的任何给定符号的第一个定义(导出的符号) .

您可以使用 nm -CD Masternm -CD libSlave.so 检查从任何给定二进制文件导出的符号集。

Is there a way, for example by passing some suitable options to dlopen() or dlsym() to enforce usage of the Slave's own Dummy constructor instead of the one from Master (i.e. tweak the symbol lookup/reallocation behavior)?

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

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

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

另一种可能的方法是 link libSlave.so 带有 -Wl,-Bsymbolic 标志,认为符号解析规则很快就会变得非常复杂,除非你全部理解它们,否则最好避免这样做。


P.S。人们可能想知道为什么 Master 二进制文件会导出任何符号——通常只导出在 link 期间使用的其他 .so 引用的符号。

发生这种情况是因为 cmake 在 link 运行主可执行文件时使用 -rdynamic为什么它会那样做,我不知道。

所以另一个解决方法是:不要使用 cmake(或者至少不要使用它使用的默认标志)。

我遵循了上一个答案中的建议 Is there symbol conflict when loading two shared libraries with a same symbol :

  • 运行 'nm Master' 和 'nm libSlave.so' 显示相同的自动生成的构造函数符号:
...
000000000000612a W _ZN5DummyC1EOS_
00000000000056ae W _ZN5DummyC1ERKS_
0000000000004fe8 W _ZN5DummyC1Ev
...

因此,被破坏的函数签名在主二进制文件和从属二进制文件中都匹配。

加载库时,使用master的函数而不是库的版本。为了进一步研究这一点,我创建了一个更简约的示例,如上面引用的 post:

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;
}

slave.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

注意 master 和 slave-library 中 -fPIC 的使用。

我现在尝试了几种 RTLD_xx 标志和编译标志的组合:

1.

dlopen() 标志:RTLD_NOW | RTLD_DEEPBIND -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 的行中发生段错误;尽管如此,库中的 printHello()s 函数仍被调用

3.

dlopen() 标志:RTLD_NOW -fPIC 图书馆

Hello world from master
Hello world from master

-> 这是我原来的行为;所以 RTLD_DEEPBIND 绝对是我需要的,连同主二进制文件中的 -fPIC;

注意:虽然 CMake 在构建共享库时会自动添加 -fPIC,但通常不会为可执行文件添加 -fPIC;在这里你需要在使用 CMake

构建时手动添加此标志

注 2:使用 RTLD_NOW 或 RTLD_LAZY 没有区别。

可执行文件和共享库 上使用 -fPIC 与 RTLD_DEEPBIND 使具有不同 Dummy 类 的原始示例可以正常工作。