Upgrade to Pro — share decks privately, control downloads, hide ads and more …

动态库,是得多动态?

 动态库,是得多动态?

运行时加载动态库的应用与挑战。

视频: https://www.bilibili.com/video/BV1LU4y1K7om

Zhihao Yuan

December 11, 2021
Tweet

More Decks by Zhihao Yuan

Other Decks in Programming

Transcript

  1. 4 2021 Pure C++ | PURECPP 使用静态库 app editor.o file.o

    line.o search.o source.o libed.a editor.o file.o line.o libre.a search.o
  2. 5 2021 Pure C++ | PURECPP 动态链接 app libed.so =>

    libre.so => source.o libed.so editor.o file.o line.o libre.so search.o
  3. 7 2021 Pure C++ | PURECPP #include "repromath_export.h" namespace repromath

    { REPROMATH_EXPORT auto ddot(int n, double const *x, double const *y) -> double; REPROMATH_EXPORT auto dsum(int n, double const *x) -> double; } 动态链接库的例子 CMake 自动生成的头文件 add_library(repromath SHARED) generate_export_header(repromath)
  4. 8 2021 Pure C++ | PURECPP #include <repromath.h> #include <stdio.h>

    int main() { double x[] = {1., 2., 3.}; double y[] = {4., -5., 6.}; printf("result = %g\n", repromath::ddot(3, x, y)); } 用户侧的代码和静态链接相同
  5. 10 2021 Pure C++ | PURECPP • Level 0: 依赖库

    • Level 1: 延迟加载 「动态」的不同等级
  6. 12 2021 Pure C++ | PURECPP • Windows: 辅助库 delayimp.lib

    + linker 选项 /DELAYLOAD:mylib.dll • Solaris: linker 选项 -z lazyload -lmylib • Linux: DIY: Implib.so (类似 DLL 导入库的方案) 延迟加载跨平台吗?
  7. 13 2021 Pure C++ | PURECPP printf("address prior to use:

    %p\n", repromath::ddot); printf("result = %g\n", repromath::ddot(3, x, y)); printf("address after using: %p\n", repromath::ddot); MSVC 的 /DELAYLOAD 选项
  8. 14 2021 Pure C++ | PURECPP • 函数的地址在程序运行过程中可以改变 – 因为槽的地址

    ≠ 实际函数的地址 • C++ 的对象在内存中不能变更位置 (relocate),但现在函数可以 延迟加载的迷惑行为
  9. 16 2021 Pure C++ | PURECPP #include "repromath_export.h" namespace repromath

    { REPROMATH_EXPORT auto ddot(int n, double const *x, double const *y) -> double; REPROMATH_EXPORT auto dsum(int n, double const *x) -> double; } 动态加载库的例子 CMake 自动生成的头文件 add_library(repromath MODULE) generate_export_header(repromath)
  10. 17 2021 Pure C++ | PURECPP • LoadLibraryEx("mylib.dll", nullptr, flags)

    • LoadLibrary("mylib.dll") • GetProcAddress(handle, "function_or_variable_name") • FreeLibrary(handle) • GetLastError() APIs: Win32
  11. 19 2021 Pure C++ | PURECPP double x[] = {1.,

    2., 3.}; double y[] = {4., -5., 6.}; auto lib = ::LoadLibraryW(L"repromath.dll"); /* ...处理错误 */ typedef auto ddot_t(int, double const *, double const *) -> double; auto ddot = (ddot_t *)::GetProcAddress(lib, "?ddot@repromath@@YANHPEBN0@Z"); printf("result = %g\n", ddot(3, x, y)); ::FreeLibrary(lib); Win32 下的例子
  12. 21 2021 Pure C++ | PURECPP • dlopen("mylib.so") • dlsym(handle,

    "symbol_name") • dlclose(handle) • dlerror() APIs: POSIX
  13. 23 2021 Pure C++ | PURECPP double x[] = {1.,

    2., 3.}; double y[] = {4., -5., 6.}; auto lib = ::dlopen("./librepromath.so", RTLD_LOCAL | RTLD_NOW); /* ...处理错误 */ typedef auto ddot_t(int, double const *, double const *) -> double; auto ddot = (ddot_t *)::dlsym(lib, "_ZN9repromath4ddotEiPKdS1_"); printf("result = %g\n", ddot(3, x, y)); ::dlclose(lib); POSIX 下的例子
  14. 25 2021 Pure C++ | PURECPP • dlopen, LoadLibrary, 和

    LoadLibraryEx 增加计数 • dlclose 和 FreeLibrary 减少计数 • GetModuleHandleEx 能增加已经加载了的库的引用计数 平台对载入的库使用了引用计数
  15. 26 2021 Pure C++ | PURECPP • Level 0: 依赖库

    • Level 1: 延迟加载 • Level 2: 动态实体 「动态」的不同等级
  16. 27 2021 Pure C++ | PURECPP void *dlsym(void *handle, char

    const *symbol); 近距离观察动态载入 API
  17. 28 2021 Pure C++ | PURECPP (ddot_t *)::dlsym(lib, "_ZN9repromath4ddotEiPKdS1_"); 近距离观察动态载入

    API 类型为 void * 的指针可以被强制 转换为函数指针吗?
  18. 30 2021 Pure C++ | PURECPP FARPROC GetProcAddress(HMODULE hModule, LPCSTR

    lpProcName); Win32 下动态载入 API 的情况 typedef INT_PTR (FAR WINAPI *FARPROC)();
  19. 32 2021 Pure C++ | PURECPP 在程序运行过程中,决议符号的结果会指向 • 程序之外 (foreign)

    的函数,或者 • 对象模型之外 (foreign) 的对象 动态实体的奇怪之处
  20. 33 2021 Pure C++ | PURECPP • 在另一个编程语言中使用可加载模块 • 使用

    Foreign Function Interface (FFI) 来访问 外语的「外」
  21. 34 2021 Pure C++ | PURECPP from ctypes import CDLL,

    c_double x = (c_double * 3)(1., 2., 3.) y = (c_double * 3)(4., -5., 6.) repromath = CDLL('repromath.dll') ddot = repromath['?ddot@repromath@@YANHPEBN0@Z'] ddot.restype = c_double print("result = {}".format(ddot(3, x, y))); ctypes 的例子 遗漏这行,你会得到一堆垃圾
  22. 35 2021 Pure C++ | PURECPP • pybind11: 你写的 C++

    代码会在 Python 运行时中组建模块 • JNA (Java Native Access): 你可以用 Java 语法声明 C 函数 • pydffi (DragonFFI): 你可以用 C++ 声明 C 或 C++ 函数… 强类型 FFI 是有必要的 import pydffi pydffi.dlopen("/path/to/libarchive.so") CU = pydffi.FFI().cdef("#include <archive.h>") a = funcs.archive_read_new()
  23. 36 2021 Pure C++ | PURECPP #include <repromath.h> int main()

    { double x[] = {1., 2., 3.}; double y[] = {4., -5., 6.}; auto lib = dlopen("repromath"); auto ddot = __magic<repromath::ddot>(lib); printf("result = %g\n", ddot(3, x, y)); } 假想的 C++ FFI
  24. 37 2021 Pure C++ | PURECPP • 声明决定了一个实体的符号 – 名字

    – 类型 – extern "C" – ABI tag 等编译器扩展 用函数、对象声明决议动态实体
  25. 38 2021 Pure C++ | PURECPP • Level 0: 依赖库

    • Level 1: 延迟加载 • Level 2: 动态实体 • Level 3: 插件系统 「动态」的不同等级
  26. 39 2021 Pure C++ | PURECPP • 如果把内存中的整个程序看作是一个「C++ 程序」,那么这是 ODR

    violation – 症状各种各样 • 主流平台都有缓和这一风险的机制 – 不过总有「绕过」办法的问题… 载入两个定义了同一个实体的库会发生什么?
  27. 40 2021 Pure C++ | PURECPP • OpenSSL + LibreSSL

    在同一个进程中能干什么好事? • 「ABI 兼容性」这个术语已经暗示了我们希望两个兼容的东西功能是可替换的 对于动态实体来说,符号冲突是意外 但如果是我们在利用 ABI 相同这一 点,用编程方式从多个载入库中获 得额外功能呢?
  28. 41 2021 Pure C++ | PURECPP • GIMP 的图形处理框架 •

    拖拽文件就能添加、删除功能 案例:GEGL
  29. 42 2021 Pure C++ | PURECPP PLUGINAPP_API LPPLUGINSTRUCT plugin_app_create_plugin(void); PLUGINAPP_API

    void plugin_app_destroy_plugin(LPPLUGINSTRUCT); PLUGINAPP_API const gchar* plugin_app_get_plugin_name(void); PLUGINAPP_API const gchar* plugin_app_get_plugin_provider(void); PLUGINAPP_API const gchar* plugin_app_get_menu_name(void); PLUGINAPP_API const gchar* plugin_app_get_menu_category(void); PLUGINAPP_API void plugin_app_run_proc(void); 一个典型的基于 C 的插件系统 该例子修改自 https://www.codeproject.com/Articles/38966 7/Simple-Plug-in-Architecture-in-Plain-C
  30. 43 2021 Pure C++ | PURECPP LPPLUGINSTRUCT plugin_app_create_plugin() { g_debug("PluginDialog1::plugin_app_create_plugin");

    /* ... */ return PLS; } const gchar* plugin_app_get_plugin_name() { g_debug("PluginDialog1::plugin_app_get_plugin_name"); return "Dialog1 Plugin"; } 插件1 实现的东西
  31. 44 2021 Pure C++ | PURECPP LPPLUGINSTRUCT plugin_app_create_plugin() { g_debug("...");

    /* ... */ return PLS; } const gchar* plugin_app_get_plugin_name() { g_debug("..."); return "Dialog2 Plugin"; } 插件2 实现的东西
  32. 45 2021 Pure C++ | PURECPP LPPLUGINSTRUCT plugin_app_create_plugin() { g_debug("Some

    thing"); /* ... */ return PLS; } const gchar* plugin_app_get_plugin_name() { g_debug("..."); return "Dialog1 Plugin"; } 同一实体多个定义 LPPLUGINSTRUCT plugin_app_create_plugin() { g_debug("Some other thing"); /* ... */ return PLS; } const gchar* plugin_app_get_plugin_name() { g_debug("..."); return "Dialog2 Plugin"; }
  33. 46 2021 Pure C++ | PURECPP typedef LPPLUGINSTRUCT (*CREATEPROC) (void);

    typedef void (*DESTROYPROC) (LPPLUGINSTRUCT); typedef const gchar* (*NAMEPROC) (void); typedef const gchar* (*PROVIDERPROC)(void); typedef const gchar* (*MENUPROC) (void); typedef const gchar* (*MENUCATPROC) (void); typedef void (*RUNPROC) (void); 为了之后使用 GetProcAddress 准备的指针类型
  34. 47 2021 Pure C++ | PURECPP void load_all_plugins(GtkWidget *widget, gpointer

    user_data) { LPPLUGINSTRUCT pls = NULL; CREATEPROC create = NULL; MENUPROC menuproc = NULL; MENUCATPROC menucatproc = NULL; /* ... */ while (plugin_helper_get_plugin_list()) { create = (CREATEPROC) GetProcAddress(h, "plugin_app_create_plugin"); 用法 struct _PluginStruct { NAMEPROC nameProc; PROVIDERPROC providerProc; MENUPROC menuProc; MENUCATPROC menuCatProc; RUNPROC runProc; DESTROYPROC destProc; };
  35. 49 2021 Pure C++ | PURECPP 接口 whereispython 基于 C++

    的插件系统的例子 可执行文件 plugindemo 模块 fullinstaller microsoftstore
  36. 50 2021 Pure C++ | PURECPP class installation { public:

    virtual auto executable() -> std::filesystem::path = 0; virtual auto windowed_executable() -> std::filesystem::path = 0; virtual ~installation() = default; }; class factory { public: virtual auto lookup(char const *ver) -> std::unique_ptr<installation> = 0; }; namespace whereispython 和同名头文件
  37. 51 2021 Pure C++ | PURECPP class fullinstaller : public

    installation { std::unique_ptr<HKEY, hkey_deleter> hkey_; public: explicit fullinstaller(char const *version); auto executable() -> std::filesystem::path override { return string_value(L"ExecutablePath"); } auto windowed_executable() -> std::filesystem::path override { return string_value(L"WindowedExecutablePath"); } }; namespace fullinstaller 和同名 dll
  38. 52 2021 Pure C++ | PURECPP class fullinstaller_factory : public

    factory { public: virtual auto lookup(char const *version) -> std::unique_ptr<installation> { try { return std::make_unique<fullinstaller>(version); } catch (std::exception &) { return nullptr; } } }; namespace fullinstaller 和同名 dll
  39. 54 2021 Pure C++ | PURECPP class microsoftstore : public

    installation { std::filesystem::path install_location_; public: explicit microsoftstore(char const *version) { auto shell = PowerShell::Create() ->AddCommand("Get-AppxPackage") ... class microsoftstore_factory : public factory { ... namespace microsoftstore 和同名 dll
  40. 56 2021 Pure C++ | PURECPP template <class Factory> class

    plugin { std::unique_ptr<HMODULE, library_deleter> lib_; Factory *obj_; public: explicit plugin(std::filesystem::path const &dll) : lib_([&] { /* ... */ }()), obj_([this] { if (auto pinst = (Factory *)::GetProcAddress(lib_.get(), "instance")) return pinst; /* ... */ }()) plugin.h 头文件里的 namespace plugindemo
  41. 57 2021 Pure C++ | PURECPP plugin<whereispython::factory> (Factory *)::GetProcAddress(lib_.get(), "instance")

    whereispython::factory * whereispython::fullinstaller_factory * whereispython::fullinstaller_factory instance;
  42. 58 2021 Pure C++ | PURECPP • 载入一个目录下的所有插件 auto openplugins(std::filesystem::path

    dir) -> std::vector<plugin<whereispython::factory>>; openplugin.h 头文件里的 namespace plugindemo
  43. 60 2021 Pure C++ | PURECPP for (auto &plugin :

    plugindemo::openplugins(fs::current_path())) { if (auto python = plugin->lookup(argv[1])) { if (nonempty) std::cout << std::endl; std::cout << python->executable() << '\n'; std::cout << python->windowed_executable() << '\n'; nonempty = true; } } plugindemo 可执行文件的 main 函数
  44. 61 2021 Pure C++ | PURECPP • 删掉 fullinstaller.dll 会丢失第一组答案

    • 删掉 microsoftstore.dll 会丢失第二组答案 • 拖入文件,插入 (plug-in) 功能 演示 查找版本 "3.7"
  45. 62 2021 Pure C++ | PURECPP • 原理上想违反,但不一定得是函数的 ODR •

    允许控制以载入为目的导出的别名,这样的功能很有用 插件想要违反 One Definition Rule GCC & Clang int foo asm("myfoo") = 2;
  46. 63 2021 Pure C++ | PURECPP • Level 0: 依赖库

    • Level 1: 延迟加载 • Level 2: 动态实体 • Level 3: 插件系统 • Level 4: Live update 「动态」的不同等级
  47. 65 2021 Pure C++ | PURECPP • 调用已经卸载了的库中的函数会导致 access violation

    • …函数现在有生命期了 但可能想要卸载旧代码
  48. 66 2021 Pure C++ | PURECPP • Apache 和 Nginx

    的模块无法卸载 • Python 的模块系统无法卸载 C 和 C++ 扩展 – importlib.reload 也不会重新载入这些扩展 • musl libc 的 dlclose 是 no-op 应用与实现常常避免卸载动态库 重启进程解决一切问题 如果扩展创建的活着的对象不能保持扩展自身不 被回收,不卸载扩展避免了违反 Python 的语义 如果库可以被卸载,线程本地 存储 (TLS) 的实现会很复杂
  49. 67 2021 Pure C++ | PURECPP 库创建的对象能比库自身活得更长吗? 嵌套生命期 • 没有问题

    ✔ 对象可以逃逸 • 给每个对象加一个库的工厂对象的强引用? • 让库跟踪所有的对象? – 可以考虑用 GetModuleHandleEx 让库完全掌控 自身的卸载时机 • 如果库会创建生命期不受库管制的线程, 参考 FreeLibraryAndExitThread • 很难调试
  50. 71 2021 Pure C++ | PURECPP class logger { public:

    virtual ~logger() = default; }; class singleton { public: virtual auto get() -> logger & = 0; }; 测试一下
  51. 72 2021 Pure C++ | PURECPP class memory_logger : public

    logger { std::unique_ptr<char[]> buf_ = std::make_unique<char[]>(1024); public: memory_logger() { /* 记录 thread id */ } ~memory_logger() { /* 记录 thread id */ } }; 该 logger 的对象实例将是线程特定的
  52. 74 2021 Pure C++ | PURECPP class memory_logger_singleton : public

    singleton { public: virtual auto get() -> logger & override { thread_local memory_logger inst; return inst; } }; tslogger::memory_logger_singleton instance; 我们要返回这样一个实例
  53. 75 2021 Pure C++ | PURECPP auto load = []

    { return plugin<singleton>(fs::current_path() / libname); }; auto inst = load(); std::thread th[] = { /* 下一张幻灯片 */ }; for (auto &thr : th) thr.join(); 可执行文件的 main 函数
  54. 76 2021 Pure C++ | PURECPP 使用该库的线程 th[0] std::thread([&] {

    inst->get(); /* th[1] 卸载了库之后 */ }) th[1] std::thread([&] { /* th[0] 初始化了 TLS 之后 */ inst->get(); inst.unload(); })
  55. 77 2021 Pure C++ | PURECPP class memory_logger : public

    logger { ... memory_logger() { std::cout << " + thread (" << std::this_thread::get_id() << ") attached\n"; } ~memory_logger() { std::cout << " - thread (" << std::this_thread::get_id() << ") detached\n"; } 我们的线程特定 logger 会记录这些东西
  56. 79 2021 Pure C++ | PURECPP GCC + glibc on

    Linux 表现好到不真实?
  57. 80 2021 Pure C++ | PURECPP class memory_logger_singleton : public

    singleton { ... memory_logger_singleton() { std::cout << " + process attached\n"; } ~memory_logger_singleton() { std::cout << " + process detached\n"; } }; 把静态对象的活动也记录下来
  58. 81 2021 Pure C++ | PURECPP auto load = []

    { return plugin<singleton>(fs::current_path() / libname); }; auto inst = load(); std::thread th[] = { /* ... */ }; for (auto &thr : th) thr.join(); load(); 然后在所有线程退出之后再载入一次这个库
  59. 84 2021 Pure C++ | PURECPP • RTLD_NODELETE – 调用

    dlclose() 时不卸载共享库 – 后果是,即便之后用 dlopen() 重新载入该库,其中的静态变量和全局变量也不会重新初始化 • DF_1_NODELETE (elf.h) – 自动给包含 thread_local 对象的共享库打上该标记,直到所有这些对象被销毁 – 此标记被清除之后,再次调用 dlclose() 才能卸载该库 glibc 在销毁 thread_local 对象的过程中调用 dlclose() 是无用操作
  60. 88 2021 Pure C++ | PURECPP 越动态,越多挑战 延迟加载 • 函数地址在程序运行过

    程中可能会变 动态实体 (显式加载) • 存在程序和对象模型之 外的函数和对象 插件 (加载多个定义) • 违反 ODR Live update (卸载) • 函数可能有生命期 • 析构函数也有生命期给 实现 TLS 带来了挑战