DLCLOSE并没有真正卸载共享对象,无论它被调用多少次

dlclose doesn't really unload shared object, no matter how many times it is called

本文关键字:多少次 调用 对象 并没有 卸载 共享 DLCLOSE      更新时间:2023-10-16

我的程序使用dlopen加载共享对象,然后使用dlclose卸载它。有时会再次加载这个共享对象。我注意到静态变量没有重新初始化(这对我的程序至关重要),所以我在dlclose之后添加了一个测试(dlopenRTLD_NOLOAD),看看库是否真的卸载了。果然,它还在内存中。

然后我尝试反复调用dlclose,直到库真正卸载,但我得到的是一个无限循环。这是我用来检查库是否被卸载的代码:

dlclose(handles[name]);
do {
  void *handle = dlopen(filenames[name], RTLD_NOW | RTLD_NOLOAD);
  if (!handle)
    break;
  dlclose(handle);
} while (true);

我的问题是,我的共享对象在dlclose之后没有被卸载的可能原因是什么,因为我的dlopen调用是加载它的唯一地方。你能提出一个找出问题根源的行动方案吗?另外,为什么重复调用dlclose没有效果,它们都在减少引用计数,不是吗?

EDIT:刚刚发现只有当我用gcc编译时才会发生这种情况。使用clang,一切都很好。

POSIX标准实际上不要求dlclose从地址空间中卸载库:

虽然不需要dlclose()操作来删除结构来自地址空间,也不禁止实现这样做。

来源:The Open Group Base Specifications Issue 6

这意味着除了使句柄无效之外,dlclose根本不需要做任何事情。

有时卸载也被系统延迟,它只是将库标记为"要删除",并将在稍后的时间实际执行该操作(为了效率或因为现在根本不可能执行该操作)。但是,如果在执行dlopen之前再次调用它,则该标志将被清除,并且仍然加载的库将被重用。

在某些情况下,系统肯定知道库中的一些符号仍在使用,在这种情况下,它不会从地址空间中卸载它,以避免悬空指针。在某些情况下,系统不能确定它们是否在使用,但它也不可能确定它们没有被使用,安全总比后悔好,在这种情况下,系统永远不会真正从内存中删除该库。

还有其他更模糊的情况取决于操作系统类型,通常也取决于版本。例如,一个常见的Linux问题是,如果你创建了一个使用STB_GNU_UNIQUE符号的库,该库被标记为"不可卸载",因此将永远不会被卸载。看看这里,这里(DF_1_NODELETE表示不可卸载)和这里。所以它也可以取决于编译器生成的符号。尝试在库中运行readelf -Ws,并查找标记为UNIQUE的对象。

一般来说,您不能真正依赖dlclose来像您期望的那样工作。在实践中,在过去的十年里,我看到它"失败"的次数比"成功"的次数要多(好吧,它从来没有真正失败过,它只是经常没有从内存中卸载库;然而,它按照标准的要求工作)。

这不是您所有问题的答案,但这是可以帮助您避免dlclose问题的解决方案。这个问题提示了一个关于如何影响重新加载共享库的行为的线索:你可以使用编译器标志-fno-gnu-unique

gcc/g++的man page:

-fno-gnu-unique

在最近使用GNU汇编器和C库的系统上,c++编译器使用"STB_GNU_UNIQUE"绑定来确保模板静态数据成员和内联函数中的静态局部变量的定义是唯一的,即使存在"RTLD_LOCAL";这是必要的,以避免由两个不同的"RTLD_LOCAL"插件使用的库的问题,这取决于其中一个的定义,因此与另一个不同意符号的绑定。但这会导致"dlclose"在受影响的dso中被忽略;如果你的程序依赖于通过"dlclose"answers"dlopen"重新初始化DSO,你可以使用-fno-gnu-unique。

是否默认使用-fno-gnu-unique取决于GCC的配置:--disable-gnu-unique-object默认启用该标志,--enable-gnu-unique-object禁用该标志。

在Windows中使用等效的ifdefWINLINUX:

  • LoadLibrary() = dlopen()
  • FreeLibrary() = dlclose()
  • GetProcAddress() = dlsym()

void *handle;
double (*cosine)(double);
char *error;
handle = dlopen ("/lib/libm.so.6", RTLD_LAZY);
if (!handle) {
  fputs (dlerror(), stderr);
  exit(1);
  }
cosine = dlsym(handle, "cos");
 if ((error = dlerror()) != NULL)  {
   fputs(error, stderr);
   exit(1);
   }
printf ("%fn", (*cosine)(2.0));
dlclose(handle);

动态库加载有很多奇怪的地方。依赖于操作系统初始化静态变量充满了问题。你最好要么完全避免它,要么使用一个插件加载器来为你处理所有的特殊情况。

我建议您查看glib模块。Glib提供了一种与平台无关的加载动态库的方式。你可以使用这些回调:

  • GModuleCheckInit ()
  • GModuleUnload ()

它们可以处理分配和释放任何资源。你可以动态地分配你所需要的,而不是依赖于操作系统以一种可靠的方式为你分配静态数据。

你所需要做的就是在你的动态库中定义这些函数,然后用: 加载和卸载它们。
  • g_module_open ()
  • g_module_close ()

这可以通过用RTLD_LOCAL调用dlopen来修复(也许不是在所有情况下)。

我遇到了同样的问题,其中没有调用析构函数,但是如果我使用RTLD_LOCAL打开共享对象,那么dlclose的行为如预期的那样,并调用析构函数。

在我的WSL上,我有一个dlclose没有调用库析构函数的问题,我的"direct_refcount"无论我执行多少次dlclose相同的句柄,在每次加载命令后,lib的值都继续上升。

但是,当我更改dlsym命令以使用从dlopen返回的句柄而不是使用RTLD_DEFAULT时,它被修复了,因为我认为我可以遍历作用域以找到该符号。

我不知道有什么不同,但这在我的设置中解决了这个问题。

安装WSL, Ubuntu 20, GCC 9.4, GNU ld 2.34, GLIBC 2.31