从动态链接库调用函数一次,从可执行文件调用一次
Calling functions once fom dynamic linked library and once from executable
我想测试如果可执行文件和库共享不同版本的库会发生什么,
即具有相同名称的不同 classes。想法:创建一个 test
函数
直接从可执行文件中调用一次,并使用库中的代码调用一次:
MWE:
base.h
定义了一个抽象插件class,它可以生成一个端口对象(类型为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
定义了一个派生插件class,它可以return一个派生端口(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)
的派生端口 class
#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
"like the plugin does",
它调用一个在插件中实现的虚函数。所以我希望 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
(内部和外部)
问题:
- 是否可以调用
mport::test
的两个版本(例如可执行文件和库)?
- 为什么
-rdynamic
改变行为?
- 为什么
-DACCEPT_EXTERN
会影响行为?
这里的问题是你违反了 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
通过在行前添加 __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
另一种选择是通过将 class 封装在匿名命名空间中来简单地使其成为静态的。这将适用于您的简单情况,但如果您的插件由多个翻译单元组成,则效果不佳。
至于你期望在你的两条线上有不同的结果,你得到一个派生的基础引用 class,你为什么期望除了适当的虚拟覆盖之外的任何东西被调用?
我想测试如果可执行文件和库共享不同版本的库会发生什么,
即具有相同名称的不同 classes。想法:创建一个 test
函数
直接从可执行文件中调用一次,并使用库中的代码调用一次:
MWE:
base.h 定义了一个抽象插件class,它可以生成一个端口对象(类型为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 定义了一个派生插件class,它可以return一个派生端口(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)
的派生端口 class#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
"like the plugin does",
它调用一个在插件中实现的虚函数。所以我希望 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
(内部和外部)
问题:
- 是否可以调用
mport::test
的两个版本(例如可执行文件和库)? - 为什么
-rdynamic
改变行为? - 为什么
-DACCEPT_EXTERN
会影响行为?
这里的问题是你违反了 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
通过在行前添加
__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
另一种选择是通过将 class 封装在匿名命名空间中来简单地使其成为静态的。这将适用于您的简单情况,但如果您的插件由多个翻译单元组成,则效果不佳。
至于你期望在你的两条线上有不同的结果,你得到一个派生的基础引用 class,你为什么期望除了适当的虚拟覆盖之外的任何东西被调用?