无论调用多少次,dlclose都不能真正卸载共享对象。

26

我的程序使用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);

我的问题是,如果我的dlopen调用是唯一加载共享对象的地方,为什么在dlclose之后我的共享对象没有被卸载?你能建议一种跟踪问题源头的方法吗?此外,为什么多次调用dlclose没有效果,它们不是每次都会减少引用计数吗?

编辑:刚刚发现只有在使用gcc编译时才会出现这个问题。使用clang时一切正常。


1
他们每个都在减少引用计数,不是吗?不,后续的调用不在您当前的进程中。检查返回值,在第一次调用后,您的句柄无效。 - πάντα ῥεῖ
1
@JoachimPileborg:没错,不过如果你仍然可以从库中访问符号,那么它确实会让你想知道dlclose是否在实践中起到了任何作用。 - Karoly Horvath
1
就此而言,我进行了一些简单的实验,一切都按预期工作——当您匹配dlopen和dlclose计数时,库中全局变量的析构函数被调用,并且块静态变量在后续的加载/卸载运行中重新初始化。 - Kerrek SB
1
@Kerrek 关于将动态加载功能集成到标准C++中,过去曾有一些零星的尝试-例如http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2407.html。但我认为这些尝试已经逐渐淡出。 - Alan Stokes
4
根据 man 手册中的描述:"如果引用计数降至零,且没有其他已加载的库使用其中的符号,则动态库将被卸载。"你可能会遇到这个问题。在我看来,这意味着您不能依赖于 dlclose 实际上将库卸载。 - n. m.
显示剩余19条评论
6个回答

30

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

尽管dlclose()操作“不需要从地址空间中删除结构”,但实现也不禁止这样做。

来源:The Open Group Base Specifications Issue 6

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

有时系统也会延迟卸载操作,它只将库标记为“待删除”,并在以后的某个时间实际执行该操作(出于效率或因为现在执行该操作根本不可能)。但是,如果您在此之前再次调用dlopen,则标志将被清除,并且仍加载的库将被重用。

在某些情况下,系统确信库的某些符号仍在使用中,因此它不会从地址空间卸载它以避免悬空指针。在某些情况下,系统无法确定它们是否正在使用,但也无法确定它们不是,为了安全起见,在这种情况下它将永远不会真正从内存中删除该库。

根据操作系统类型和版本,还有更加晦涩的情况。例如,常见的Linux问题是,如果您创建了使用STB_GNU_UNIQUE符号的库,则该库将被标记为“不可卸载”,因此永远不会被卸载。请参见这里, 这里DF_1_NODELETE表示不可卸载),以及这里。因此,这也可能取决于编译器生成的符号或符号类型。在库上运行readelf -Ws命令,并查找标记为UNIQUE的对象。

总的来说,你不能完全依赖dlclose按照你的期望工作。在实践中,过去十年中我看到它“失败”的次数比“成功”多(好吧,它从来没有真正失败过,只是经常无法将库从内存中卸载;但它按照标准要求工作)。


3
非常有帮助的答案!对于因“STB_GNU_UNIQUE”而遇到此问题的人,有个提示:如果你使用的是GCC编译器,可以设置一个编译选项“--no-gnu-unique”,这将避免此问题。 - oLen

7
这不是解决你所有问题的答案,但这是可以帮助你避免与 dlclose 相关问题的解决方案。 这个问题给出了一个线索,关于如何影响重新加载共享库的行为:你可以使用编译器标志 -fno-gnu-unique
来自gcc / g++的手册页面:
“-fno-gnu-unique” 在具备最新GNU汇编器和C库的系统上,C++编译器使用“STB_GNU_UNIQUE”绑定,以确保内联函数中的模板静态数据成员和静态局部变量的定义即使存在“RTLD_LOCAL”,也是唯一的;这是必要的,以避免由两个不同的“RTLD_LOCAL”插件使用的库依赖于其中一个插件中的定义,并因此与另一个插件关于符号绑定的区别而产生问题。但这会导致 "dlclose" 不考虑受影响的 DSO;如果您的程序依赖于通过 "dlclose" 和 "dlopen" 重新初始化 DSO,则可以使用 -fno-gnu-unique。
是否默认使用 -fno-gnu-unique 取决于GCC的配置方式:--disable-gnu-unique-object默认启用此标志,--enable-gnu-unique-object禁用此标志。

2

在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 ("%f\n", (*cosine)(2.0));
dlclose(handle);

1

动态库加载有很多怪癖。依赖操作系统初始化静态变量存在很多问题。最好的方法是完全避免或使用插件加载器处理所有特殊情况。

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

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

您只需要在动态库中定义这些函数,然后使用以下命令加载和卸载它们:


2
GLib模块在除Windows以外的所有平台上也使用dlopen/dlclose,如果dlclose无法卸载库,则在不同系统上可能会出现许多原因(有些实际上根本不会卸载库,dlclose只是一个空方法),那么在该平台上使用GLib模块将毫无意义。 - Mecki

1

这个问题可以通过使用dlopenRTLD_LOCAL来解决(也许不是所有情况都适用)。

我曾经遇到过同样的问题,析构函数没有被调用,但如果我使用RTLD_LOCAL打开共享对象,那么dlclose就会按预期工作并调用析构函数。


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

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

我不知道区别在哪里,但这特别解决了我的设置中的问题。

设置WSL,Ubuntu 20,GCC 9.4,GNU ld 2.34,GLIBC 2.31。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接