C++程序连接两个第三方共享库时崩溃。

11

我有两个外包的共享库,用于Linux平台(无源代码,无文档)。当这些库单独链接到程序时它们可以正常工作(g++ xx.cpp lib1.so 或者 g++ xx.cpp lib2.so)。

但是,当任何C++程序同时链接到这两个共享库时,程序不可避免地会崩溃并出现“double free”错误(g++ xx.cpp lib1.so lib2.so)。

即使C++程序是一个空的“hello world”程序,与这些库没有关系,它也会崩溃。

#include <iostream>
using namespace std;
int main(){
     cout<<"haha, I crash again. Catch me if you can"<<endl;
     return 0;
}

Makefile:

g++ helloword.cpp lib1.so lib2.so

我得到了一些线索,这些lib1.so和lib2.so库可能共享某些公共全局变量,并且它们会两次销毁某些变量。我尝试使用gdb和valgrind,但无法从回溯中提取有用的信息。

是否有任何方法可以隔离这两个共享库并使它们以sandbox方式工作?

编辑(添加核心转储和gdb回溯):

我刚刚将上述玩具空白helloword程序与这两个库链接起来(平台:centos 7.0 64位,使用gcc4.8.2):

g++ helloworld.cpp  lib1.so lib2.so -o check

Valgrind:

==29953== Invalid free() / delete / delete[] / realloc()
==29953==    at 0x4C29991: operator delete(void*) (in /usr/lib64/valgrind/vgpreload_memcheck-amd64-linux.so)
==29953==    by 0x613E589: __cxa_finalize (in /usr/lib64/libc-2.17.so)
==29953==    by 0x549B725: ??? (in /home/fanbin/InventoryManagment/lib1.so)
==29953==    by 0x5551720: ??? (in /home/fanbin/InventoryManagment/lib1.so)
==29953==    by 0x613E218: __run_exit_handlers (in /usr/lib64/libc-2.17.so)
==29953==    by 0x613E264: exit (in /usr/lib64/libc-2.17.so)
==29953==    by 0x6126AFB: (below main) (in /usr/lib64/libc-2.17.so)
==29953==  Address 0x6afb780 is 0 bytes inside a block of size 624 free'd
==29953==    at 0x4C29991: operator delete(void*) (in /usr/lib64/valgrind/vgpreload_memcheck-amd64-linux.so)
==29953==    by 0x613E589: __cxa_finalize (in /usr/lib64/libc-2.17.so)
==29953==    by 0x4F07AC5: ??? (in /home/fanbin/InventoryManagment/lib2.so)
==29953==    by 0x5039900: ??? (in /home/fanbin/InventoryManagment/lib2.so)
==29953==    by 0x613E218: __run_exit_handlers (in /usr/lib64/libc-2.17.so)
==29953==    by 0x613E264: exit (in /usr/lib64/libc-2.17.so)
==29953==    by 0x6126AFB: (below main) (in /usr/lib64/libc-2.17.so)

gdb回溯信息:

(gdb) bt
#0  0x00007ffff677d989 in raise () from /lib64/libc.so.6
#1  0x00007ffff677f098 in abort () from /lib64/libc.so.6
#2  0x00007ffff67be197 in __libc_message () from /lib64/libc.so.6
#3  0x00007ffff67c556d in _int_free () from /lib64/libc.so.6
#4  0x00007ffff7414aa2 in __tcf_0 () from ./lib1.so
#5  0x00007ffff678158a in __cxa_finalize () from /lib64/libc.so.6
#6  0x00007ffff739f726 in __do_global_dtors_aux () from ./lib1.so
#7  0x0000000000600dc8 in __init_array_start ()
#8  0x00007fffffffe2c0 in ?? ()
#9  0x00007ffff7455721 in _fini () from ./lib1.so
#10 0x00007fffffffe2c0 in ?? ()
#11 0x00007ffff7debb98 in _dl_fini () from /lib64/ld-linux-x86-64.so.2
Backtrace stopped: previous frame inner to this frame (corrupt stack?)

更新

感谢@RaduChivu的帮助,我找到了一个非常类似的情况: 程序退出时__tcf_0导致分段错误 ,看起来确实存在两个库之间的全局变量冲突。考虑到我没有这两个外部共享库的源文件,除了使用两个独立的进程外,是否还有其他解决此冲突的方法?


听起来好像这两个库是使用不同版本的gcc(或者在任何情况下使用不同的编译器)进行编译的,你在这种情况下无法做任何事情,除了与开发人员交谈以使用相同的编译器或创建通过IPC通信的单独进程。 - Radu Chivu
@NirMH "#" 只是共享库的名称。我已经进行了编辑以使其更易读。 - fanbin
1
只有当它是一个全局变量,链接器在编译时找不到它,所以系统从另一个.so文件中加载它(extern void* globalVar),你才能使用gdb获取堆栈跟踪并将其发布在你的问题中。也许我们可以从那里获得更多的上下文。 - Radu Chivu
2
是的,问题就像你所说的那样,是两个全局变量之间的名称冲突,更多信息请参考以下链接:http://stackoverflow.com/questions/6828984/crash-in-tcf-0 - Radu Chivu
当使用RTLD_GLOBAL两次打开同一个库(相同的库,两个不同的文件名)时,我发现了相同的问题。第二个dlclose导致段错误,追踪到_dl_close_worker -> __do_global_dtors_aux -> __cxa_finalize -> ... -> free。 - Dan Stahlke
显示剩余4条评论
2个回答

4

我在搜索了一天之后解决了这个问题,并在这里留下了一个笔记,以防将来有人遇到同样的问题。

解释

这证明了@RaduChivn和我猜测的是正确的:两个共享库可能共享一个全局变量。即使一个空程序同时链接到这两个共享库,当它退出时,共同的全局变量也会尝试被释放两次,从而导致双重释放损坏。

提示来自gdb回溯中的此消息:

#4  0x00007ffff7414aa2 in __tcf_0 () from ./lib1.so

如本主题所述:

使用gprof和g++时看到的__tcf_0函数是什么?,

tcf_0是由g++生成的函数,用于在触发exit()时销毁静态对象。该消息提示当一个共享库试图退出另一个共享库后,双重释放发生了。

由于这两个库是设计为一起工作的,因此损坏是不可接受的工程灾难。这样一个低质量但显而易见的错误如何能够在五个版本发布中存活下来呢?这可能是因为大多数库用户在Windows平台上工作(其软件包运行良好)。然而,这种假设提供了另一个关于错误来源的线索:共享库在Windows上运行良好,在Linux上崩溃;那么必须是某些与操作系统相关的行为差异导致了错误。这个主题提供了一些见解:

在编译exec和shared library时,全局变量在Windows上有多个副本,在Linux上只有一个副本

简而言之,来自共享库的"extern globals"在linux上只有一个副本,而在windows上有多个副本。

解决方案

(1) 自然而然地,我们可以通过创建两个进程,每个进程分别链接到一个库来解决问题。

(2) @DavidSchwartz 提供了另一个解决方法,即在程序末尾使用 _exit(0) 而不是常见的 "return 0" 或 "exit(0)"。根据 What is the difference between using _exit() & exit() in a conventional Linux fork-exec? ,需要手动刷新文件并检查 atexit 任务;对于内存部分,由于程序正在退出,操作系统会回收所有进程内存,不用担心。

(3) 另一种方法是使用 dlopen(xx.so, RTLD_LOCAL),先将所有符号盲目隐藏,然后手动 dlsym 所需的函数符号。 (@JonathanWakely 在这里指出 RTLD_LOCAL 有副作用,请参阅评论)。

在这种情况下,库编码器甚至没有在其共享库中使用 "extern C",使得名称重整变得相当难以阅读。如果其他人也喜欢这样做,以下主题可能会有所帮助:

在动态加载共享库时遇到未定义符号错误

如果您的共享库没有得到很好的支持,就像我的情况一样,仍然有解决方案。我手动整理了所有需要的函数,并使用nm在.so文件中找到每个相应的符号,逐个链接它们,这样就可以解决问题。


2
RTLD_LOCAL可以解决当前的问题,但请注意这意味着您将无法在共享库接口上使用C++异常或RTTI(在您的情况下可能不是问题)。此外,您似乎暗示不使用extern "C"是一件坏事,但除非您想从非C++程序调用库,否则没有理由使用它,即使您确实想从非C++程序调用它,您只需要在公共API中使用extern "C"。名称修饰并不是为了使名称可读,这就是为什么它被称为“修饰”的原因。 - Jonathan Wakely
@JonathanWakely 我会注意你的RTLD_LOCAL和extern "C"建议。已更新答案以反映这一点。谢谢。 - fanbin

2

一个可能的解决方案是从不调用exit。要终止程序,只需调用_exit。如果有任何需要通常由exit完成的特定任务,请在调用_exit之前自行完成。


已经检查并运行正常。假设使用了智能指针并手动完成了清理工作,这个看起来不错。根据您的建议更新了答案。谢谢。 - fanbin

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