我也觉得很奇怪,我一个做物理的,写点 C++小工具,怎么就遇到了这么 nasty 的问题呢。
简单的说,有一个已有的库,提供物理过程的模拟,它带一个借助 local global object 来控制生命周期的 singleton 作为 Messenger ,用来打日志。还有一个调用这个库的软件,它带一个 Factory Class ,用来构造一系列多态类。这些候选的类在自己的翻译单元内通过初始化全局变量的形式向这个 Factory Class 注册自己。然后这个 Factory Class 在构造和析构的时候都会打点日志。
Messenger 必然是先于 Factory Class 完成初始化的,因为 Factory Class 的构造函数已经打了日志了,那么 Factory Class 的构造函数返回之前 Messenger 已经完成构造并可用。
至少看到代码的时候直觉暗示我:这时候 Factory Class 一定会先于 Messenger 析构。毕竟这套系统的原理就是每个 static 对象构造函数完成的时候会通过atexit
向__exit_funcs
注册自己的析构函数。程序退出的时候运行时应该反向的逐个调用析构函数。这就保证了析构按照构造完成的反序进行。
好了,但是观察到的现象是程序结束的时候 Messenger 先析构,然后才是 Factory Class 。然后造成混乱,结果就是程序退出的时候必然吐一个核。虽然多数程序应该完成的功能都完成了,但是会造成很多混乱,比如在脚本里面就难以判断退出状态之类的。虽然完整的两个项目太复杂,并且其中一个还没开源,但是这里有最小重现可以一看。
我也觉得很蒙 B ,问了一圈大佬也没获得正面的解答(为什么&怎么办)。虽然改用 Nifty Counter Idiom 大概能解决问题,但是这可能涉及大改造。对屎山动手术是我想要避免的。
然后就是快乐的打断点、看代码时间。完整的结论我写在了博客里面。快速的结论是(仅适用于比较新的 glibc ,但是考虑到很多反直觉的东西出发点都是 ELF 规范的要求所以大概对于 MacOS 也可能行为差不多)
__libc_start_main_impl
会向 __exit_funcs
注册 _dl_fini
这个函数,_dl_fini
会按照动态库的依赖关系调用 __exit_funcs
里面注册的析构函数。__libc_start_main_impl
调用之前(比如上面提到的多态类通过全局变量初始化向 Factory 注册自己这种情况),那么他们的析构函数在 __exit_funcs
的位置会比 _dl_fini
靠前。_dl_fini
按照动态库的依赖关系调用 _dl_call_fini
,对于每个动态库它也会按照构造反向调用析构函数,但是它只会调用来自自己的 static object 的析构函数。所以,问题的关键就是跨越动态库&lazy init 对象初始化在动态库加载触发的时候(这里触发初始化的动态库不一定是这些对象所在的动态库,也可以是另外的动态库,这些库都会被 ld.so
加载并初始化全局对象),析构的时候动态库依赖关系优先级比构造顺序优先级高。
然后这个项目整个项目恰好没正确指定这个顺序,甚至用了-undefined dynamic_lookup
来保证只要最终的可执行文件符号都解决了就万事大吉。
对于这个特定的软件,解决方案也很简单,用 as-needed
把调用库的软件的所有动态库动态链接到提供了 Messenger 的那个库上面。as-needed
只是为了避免链接搞得太多。
虽然看起来动态库依赖就能解决问题,但是技术上讲依然可以搞得更乱,比如我有 libab.so
有 a ,b 两个类。然后 libcd.so
有 c ,d 两个类,这四个类都会在进入 __libc_start_main_impl
之前完成构造。我希望 a 先于 c 析构、但是 d 先于 b 析构。简单的动态库依赖关系又不好使了。还是得上 Nifty Counter idiom 。
...或者,实在不行就不析构这些对象了(部分情况可行,但是有时候要求这些东西做一些清理工作就不行)。
...或者,不要在析构函数调用别的静态对象了,搞得心惊胆战的,好处也不多。日志不打了也不会少块肉。
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.