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

Calling functions once fom dynamic linked library and once from executable

本文关键字:调用 一次 函数 可执行文件 用于 动态链接库      更新时间:2023-10-16

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

女工程师:

base.h定义了一个抽象插件类,它可以生成一个端口对象(类型为 base(

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

完整的代码在 github 上(试试make help(

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

  • 一次来自host可执行文件的目标代码(期望:"host:test"(
  • 一次来自plugin库的目标代码(期望:"插件:测试"(

现实看起来有所不同:

  • 在以下所有情况下,两个输出都是相等的(2x"host:test"或2x"plugin:test"(
  • 编译主机.cpp带有-rdynamic,并且没有-DACCEPT_EXTERN测试调用输出"plugin:test">
  • 编译主机.cpp-rdynamic-DACCEPT_EXTERN(参见 Makefile(,测试调用调用"host:test">
  • 编译主机.cpp不带-rdynamic,测试调用输出plugin:test(实习生和外部(

问题:

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

这里的问题是你违反了一个定义规则。

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

但是你正在做一个动态链接时间。现在,C++标准不关心动态载荷。我们必须转向x86 ELF ABI以获取更多详细信息。

长话短说,ABI 支持一种称为符号插入的技术,该技术允许动态替换符号并仍然看到一致的行为。这就是你在这里所做的,尽管是无意的。

您可以手动检查:

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
  • 通过在行前置__attribute__ ((visibility("default")))来手动导出get_plugin函数。提示:这是特定于编译器的东西,最好在某个地方使其成为宏。

    #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

另一种选择是通过将类包含在匿名命名空间中来简单地使类成为静态的。这将适用于您的简单情况,但如果您的插件由多个翻译单元组成,那就不好了。

至于你期望在两行上得到不同的结果,你得到了一个派生类的基本引用,你为什么要期望调用除适当的虚拟覆盖之外的任何内容?