调用函数一次来自动态链接库,一次来自可执行文件

时间:2018-08-16 19:52:55

标签: c++ dynamic-linking

我想测试如果可执行文件和库共享库的不同版本会发生什么, 即名称相同的不同类别。想法:制作一个test函数 直接从可执行文件中调用一次,并使用库中的代码调用一次:

MWE:

base.h 定义了一个抽象的插件类,可以生成一个端口对象(基本类型)

struct base
{
    virtual void accept(struct visitor& ) {}
    virtual void test() = 0;
    virtual ~base() {}
};

struct visitor
{
    virtual void visit(struct base& ) {}
};

struct plugin
{
    virtual ~plugin() {}
    virtual base& port() = 0;
    virtual void test() = 0;
};

typedef plugin* (*loader_t) (void);

plugin.cpp 定义派生的插件类,该类可以返回派生的端口(mport)

#include <iostream>
#include "base.h"

struct mport : public base
{
    void accept(struct visitor& ) override {}
    void test() override { std::cout << "plugin:test" << std::endl; }
    virtual ~mport() = default;
};

struct port_failure_plugin : public plugin
{
    void test() override final { inp.test(); }
    virtual ~port_failure_plugin() {}
private:
    mport inp;
    base& port() override { return inp; }
};

extern "C" {
const plugin* get_plugin() { return new port_failure_plugin; }
}

host.cpp 定义一个具有相同名称(mport)的派生端口类

#include <cassert>
#include <cstdlib>
#include <iostream>
#include <dlfcn.h>
#include "base.h"

struct mport : public base
{
#ifdef ACCEPT_EXTERN
    void accept(struct visitor& ) override;
#else
    void accept(struct visitor& ) override {}
#endif
    void test() override { std::cout << "host:test" << std::endl; }
};

#ifdef ACCEPT_EXTERN
void mport::accept(struct visitor& ) {}
#endif

int main(int argc, char** argv)
{
    assert(argc > 1);
    const char* library_name = argv[1];

    loader_t loader;
    void* lib = dlopen(library_name, RTLD_LAZY | RTLD_LOCAL);
    assert(lib);
    *(void **) (&loader) = dlsym(lib, "get_plugin");
    assert(loader);

    plugin* plugin = (*loader)();
    base& host_ref = plugin->port();
    host_ref.test(); // expected output: "host:test"
    plugin->test(); // expected output: "plugin:test"

    return EXIT_SUCCESS;
}

编译例如:

g++ -std=c++11 -DACCEPT_EXTERN -shared -fPIC plugin.cpp -o libplugin.so
g++ -std=c++11 -DACCEPT_EXTERN -ldl -rdynamic host.cpp -o host

The complete code is on github(尝试make help

为了让主机像插件一样运行test, 它调用一个虚拟函数,该函数在插件中实现。所以我希望test被称为

  • host可执行文件的目标代码开始(期望:“ host:test”)
  • 一次从plugin库的目标代码开始(期望:“ plugin:test”)

现实看起来有所不同:

  • 在所有(以下情况)中,两个输出都相等(2x“ host:test”或2x“ plugin:test”)
  • 使用-rdynamic编译host.cpp,不使用-DACCEPT_EXTERN编译测试,则调用输出“ plugin:test”
  • 使用-rdynamic-DACCEPT_EXTERN(请参阅Makefile)编译host.cpp,测试调用将调用“ host:test”
  • 不使用-rdynamic编译host.cpp,测试调用输出plugin:test(内部和外部)

问题:

  1. 甚至可以同时调用两个版本的mport::test(例如可执行文件和库文件)吗?
  2. 为什么-rdynamic会改变行为?
  3. -DACCEPT_EXTERN为什么会影响行为?

1 个答案:

答案 0 :(得分:1)

这里的事情是您违反了one definition rule

您的两个mport::test版本具有相同的声明,但它们没有相同的定义。

但是您正在执行动态链接。现在,C ++标准不再涉及动态加载。我们必须转向x86 ELF ABI以获得更多详细信息。

长话短说,ABI支持一种称为symbol interposition的技术,该技术允许动态替换符号并仍然看到一致的行为。这是您在这里所做的,尽管不经意间。

您可以手动检查它:

spectras@etherhop$ objdump -R libplugin.so |grep test
0000000000202cf0 R_X86_64_64       _ZN19port_failure_plugin4testEv@@Base
0000000000202d10 R_X86_64_64       _ZN5mport4testEv@@Base
0000000000203028 R_X86_64_JUMP_SLOT  _ZN5mport4testEv@@Base

在这里您看到,在共享对象中,mport::test的所有使用都有一个重定位条目。所有呼叫都通过PLT

spectras@etherhop$ objdump -t host |grep test
0000000000001328  w    F .text  0000000000000037              _ZN5mport4testEv

在这里您看到host确实导出了该符号(由于-rdynamic)。因此,当动态链接libplugin.so时,动态链接器将使用主程序的mport::test

这是基本机制。这就是为什么没有-rdynamic就看不到的原因:主机不再导出自己的版本,因此插件使用自己的版本。

如何解决?

您可以通过隐藏符号来避免所有这些泄漏(一般来说,这是一种好习惯,它可以加快加载速度并避免名称冲突)。

  • 在编译时添加-fvisibility=hidden
  • 通过在行之前添加get_plugin来手动导出__attribute__ ((visibility("default")))函数。提示:那是特定于编译器的内容,最好在某个地方将其设为宏。

    #define EXPORT __attribute__((visibility("default")))
    // Later:
    EXPORT const plugin* get_plugin() { /* stuff here */ }
    

因此:

spectras@etherhop$ g++ -std=c++11 -fvisibility=hidden -shared -fPIC plugin.cpp -o libplugin.so
spectras@etherhop$ g++ -std=c++11 -fvisibility=hidden -rdynamic host.cpp -ldl -o host
spectras@etherhop$ ./host ./libplugin.so
plugin:test
plugin:test

另一种选择是通过将类封装在匿名名称空间中来使其简单地变为静态。这将在您的简单情况下起作用,但是如果您的插件是由多个翻译单元组成的,那就不好了。

关于您希望在两行中得到不同结果的期望,您正在获得对派生类的基本引用,为什么除了期望的适当的虚拟重写之外,为什么还要期待其他事情?