作者PkmX (阿猫)
看板C_and_CPP
标题Re: [问题] dynamic shared library设计问题
时间Sat Oct 7 22:11:27 2017
我觉得这个直接拿个例子来解释如何在执行时载入 C++ shared library,
并建构和操作 DSO 内定义的 class 和 function:
==============================================================================
// foo.hpp
struct foo {
virtual ~foo() = 0;
virtual auto vfn() const -> int = 0;
auto fn() const -> int;
};
inline foo::~foo() = default;
template<typename... Ts>
using make_foo_fn = auto (*)(Ts...) -> foo*;
==============================================================================
// foo.cpp
#include "foo.hpp"
struct foo_impl : foo {
virtual ~foo_impl() = default;
virtual auto vfn() const -> int override { return fn(); }
};
extern "C" {
auto new_foo() -> foo* { return new foo_impl; }
}
==============================================================================
// main.cpp
#include <cassert>
#include <iostream>
#include <memory>
#include <dlfcn.h>
#include "foo.hpp"
// C++17 scopeguard utility
#include <type_traits>
template<typename F>
struct scopeguard : F {
scopeguard(scopeguard&&) = delete;
~scopeguard() { (*this)(); }
};
template<typename F> scopeguard(F) -> scopeguard<std::decay_t<F>>;
#define PP_CAT_IMPL(x, y) x##y
#define PP_CAT(x, y) PP_CAT_IMPL(x, y)
#define at_scope_exit \
[[maybe_unused]] const auto PP_CAT(sg$, __COUNTER__) = scopeguard
auto foo::fn() const -> int { return 42; }
int main() try {
auto libfoo = dlopen("./libfoo.so", RTLD_NOW | RTLD_GLOBAL);
assert(libfoo);
at_scope_exit { [&] { dlclose(libfoo); } };
const auto new_foo =
reinterpret_cast<make_foo_fn<>>(dlsym(libfoo, "new_foo"));
assert(new_foo);
const auto f = std::unique_ptr<foo>{new_foo()};
std::cout << f->vfn() << std::endl;
} catch (...) { throw; }
==============================================================================
$ GXX='g++ -std=c++17 -Wall -Wextra -pedantic -O2'
$ ${GXX} -fPIC -shared foo.cpp -o libfoo.so
$ ${GXX} -rdynamic main.cpp -ldl
$ ./a.out
42
线上版:
http://coliru.stacked-crooked.com/a/0f8900f100523fc7
这里在 foo.hpp 里面宣告 foo 为一个 abstract class 并定义 foo 的 interface,
main() 可以透过这个 interface 操作 foo,
而 foo 的实做 foo_impl 定义在 foo.cpp 里面,
并编译成 libfoo.so 让 main() 透过 dlopen("path/to/libfoo.so", ...) 载入,
但这会有一个问题,main() 根本就不知道 foo_impl 的存在,
所以 foo.cpp 会定义一个 make_foo 的 function 回传一个 foo*,
而 main() 可以用 dlsym(..., "make_foo"); 拿到 foo*,
并透过呼叫 foo 的 virtual function 就可以 dispatch 到 foo_impl 的实做
这里更有趣的就是:
foo_impl::vfn() 会呼叫 foo::fn() 这个不是 virtual 的 function,
而且他的定义在 main.cpp 里面 (当然也有可能是别的 translation unit),
所以 libfoo.so 是看不到 foo:fn() 的定义的,
而 link 的时候要加上 -rdynamic (也就是 ld 的 --export-dynamic),
让 linker 将 foo::fn() 加到 dynamic symbol table 里面,
当 main() 用 dlopen() 呼叫 dynamic loader 将 libfoo.so 载入时,
在处理 run-time relocation 时便可以找到 foo::fn()
实际上去看 gcc 的实做,基本上是遵照 Itanium C++ ABI
foo 的 layout 是这样的:
foo vtable for foo (_ZTV8foo_impl)
0x0 [vptr] - 0x0 [ offset to top ]
| 0x8 [ pointer to typeinfo ]
--------> 0x10 [ ptr to ~foo() D1 ]
0x18 [ ptr to ~foo() D0 ]
0x20 [ ptr to vfn() ]
而 libfoo.so 里面的 foo_impl 的 vtbl:
$ nm libfoo.so | grep _ZTV8foo_impl
0000000000200dc0 V _ZTV8foo_impl
$ readelf -r libfoo.so
Offset Info Type Sym. Value Sym. Name + Addend
000000200dd0 000c00000001 R_X86_64_64 0000000000000a60 _ZN8foo_implD1Ev + 0
000000200dd8 000e00000001 R_X86_64_64 0000000000000a80 _ZN8foo_implD0Ev + 0
000000200de0 001800000001 R_X86_64_64 0000000000000a70 _ZNK8foo_impl3vfnEv + 0
当 dlopen 载入 libfoo.so 时,
会将 foo_impl 的 vtbl + 0x20 写入 foo_impl::vfn() 的位置,
所以当执行到 main() 的 foo* f = make_foo(); 後,f 的 layout 就是:
(假设 f 在 0x100000,_ZTV8foo_impl 被 load 到 0x200dc0)
f @ 0x100000 vtable for foo_impl @ 0x200dc0
0x100000 [ 0x200dd0 ] - 0x200dc0 [ 0 ]
| 0x200dc8 [ _ZTI8foo_impl ]
--------> 0x200dd0 [ foo::~foo() D1 ]
0x200dd8 [ foo::~foo() D0 ]
0x200de0 [ foo_impl::vfn() ]
而 main 中呼叫 f->vfn() 的 code 则是这样 (f = 0x100000 在 rax 里面):
movq %rax, %rbx # rbx = 0x100000
movq (%rax), %rax # rax = *0x100000 = 0x200dd0
movq %rbx, %rdi # rdi = 0x100000 (this)
call *16(%rax) # call *(0x200de0)
这样就会呼叫到 foo_impl::vfn()
而 foo_impl::vfn() 的实做需要呼叫 foo::fn(),
这部份就和一般透过 PLT 间接呼叫动态连结的 function 原理是一样的:
0000000000000a70 <_ZNK8foo_impl3vfnEv>:
a70: e9 ab fe ff ff jmpq 920 <_ZNK3foo2fnEv@plt>
foo::fn() 的 PLT entry:
0000000000000920 <_ZNK3foo2fnEv@plt>:
920: ff 25 f2 06 20 00 jmpq *0x2006f2(%rip) # 201018 <_ZNK3foo2fnEv>
926: 68 00 00 00 00 pushq $0x0
92b: e9 e0 ff ff ff jmpq 910 <.plt>
$ readelf -r libfoo.so | grep _ZNK3foo2fnEv
000000201018 000100000007 R_X86_64_JUMP_SLOT 0000000000000000 _ZNK3foo2fnEv + 0
因为这里有给 RTLD_NOW,(若是 RTLD_LAZY 会是第一次呼叫时才会做 binding)
所以 dlopen libfoo.so 时,便会找到执行时 foo::fn() 的位置,
并将 jump slot 改写成直接 jump 至 foo::fn():
0x600000 <foo::fn()@plt>: 0x200000 <foo::fn()>
jmp foo::fn() ------------------> movl $42, %eax
ret
这也是为什麽 main.cpp 必须将 foo::fn() export 到 dynamic symbol table 中
这里细节真的很多,基本上你把 C++ ABI 和 dynamic linking 的运作模式弄懂,
就知道如何在执行时动态载入 C++ 的 DSO
--
※ 发信站: 批踢踢实业坊(ptt.cc), 来自: 140.113.193.217
※ 文章网址: https://webptt.com/cn.aspx?n=bbs/C_and_CPP/M.1507385491.A.794.html
1F:→ xam: 我觉得看得懂这篇的话, 没道理google找的看不懂.. 10/07 23:00
2F:推 Bencrie: 推详解 XD 10/07 23:10
3F:推 stucode: 推,这要解释清楚真的需要一点篇幅。 10/07 23:17
4F:推 mabinogi805: 也太详细QQ 10/08 00:03
※ 编辑: PkmX (140.113.193.217), 10/08/2017 01:08:17
5F:→ dreamboat66: 谢谢详细的解说,努力理解中 10/08 02:37