... When you want to control the order of initialization/deinitialization of your objects across shared libraries.
Consider the case where you have a beautiful logger class, which is a singleton, and another class which will put some information to log. It is also totally legit that you want to log when the object is being contructed and being destructed.
In the object, codes can be like following:
sth::sth(){
logger::instance().log("sth is being constructed");
}
sth::~sth(){
logger::instance().log("sth is being destructed");
}
// may be in other file, or same file
// This is also possible in case sth is actually a
// factory and the polymorphic class is registered
// in following pattern
static auto whatever = sth::instance().do_something();
In this case, you might think that the order of initialization and deinitialization of the logger and the object is guaranteed. Since C++ standard seems to guarantee for objects static storage duration, they are destructed as if std::atexit
is right after the completion of the constructor of the object. The finish of construction of logger
is sequenced before the finish of construction of sth
. That runtime should be destructing sth
before logger
.
But the point of this repository is to prove things can go wrong when you are doing this in a complexed project without properly specifying the dependency of shared libraries.
Just compile the project
mkdir build && cd build && cmake .. && cmake --build .
-
./wrong
Start of sth::sth(), I will try to put logs logger::logger() you can start putting logs now LOG std is being initialized End of sth::sth(), the runtime will consider I am fully constructed then LOG sth::do_something() is called logger::~logger() you should not being putting logs anymore LOG I am dead, I cannot put logs anymore But I will try to put this log: std is being deinitialized
-
./right{,1,2}
Start of sth::sth(), I will try to put logs logger::logger() you can start putting logs now LOG std is being initialized End of sth::sth(), the runtime will consider I am fully constructed then LOG sth::do_something() is called LOG std is being deinitialized logger::~logger() you should not being putting logs anymore
This seems to be legal in C++ standard that the order don't matter when things goes to beyond the boundary of shared libraries. There is some discussions here you can refer to.
And when it comes to the implementation of glibc, things related to destruction and dynamic libraries are:
- The data structure controlling the destruct of all static objects is
exit_function_list
structure. This is basicially a chain list containing severalexit_function
structs. Whichexit_function
struct record function pointer to call and information indicating when to call. - New entries is always added to the end of the list
- When a global object is done constructed, an entry will be added to the
exit_function_list
structure to register the destructor of the object. - When
ld.so
loads any dynamic library, it will initialize all global objects from it.
Now we are starting the program:
-
ld.so
loads every dynamic libraries, setup relocating things. And then initialize all global static objects from dynamic libraries. The control flow is given back to__libc_start_main_impl
-
In
__libc_start_main_impl
, if the program is dynamically linked,_dl_fini
will be registered to theexit_function_list
structure. This happens after dynamic linked libraries finished loading and beforemain()
is called. -
__libc_start_main_impl
will then initialize all global static objects in the same TU wheremain()
is. -
Then
main()
is called, and the program starts, control flow is now fully in the hands of the programmer. -
When
main()
is about to return,exit_function_list
looks lile:dl_init() __libc_start_main_impl() │ │ │ │ │ │ │ │ │ │ ┌─────┘ │ │ │ │ │ │ │ ┌───────────────▼──────────────────┬───────▼───────┬───────────────▼──────────────────┬────────────────────────────┐ │ Destructors of │ │ Destructors of │ Destructors of │ │ │ │ │ │ │ Global Objects Initialized From │ _dl_fini() │ Global Objects Initialized From │ Objects Initialized From │ │ │ │ │ │ │ Dynamic Libraries │ │ Main.o │ Ordinary Control Flow │ └──────────────────────────────────┴───────────────┴──────────────────────────────────┴────────────────────────────┘
-
When program exits,
__GI_exit
will be called and it will call__run_exit_handlers
, which will iterate through theexit_function_list
structure and call the destructors in the reverse order of initialization. -
For objects initialized after
_dl_fini
is registered, the destructors will be called in the reverse order of initialization, this is the ordinary order as we expected and stated in std::exit() -
_dl_fini
is a bit tricky since it will do a resort of loaded shared libraries, by the order that one will be in front of those it depends on. After the sort,_dl_call_fini
for each of the shared library will be called in the order of the sorted list. -
_dl_call_fini
calls__cxa_finalize
with the address of__dso_handle
of given shared library as argument.__cxa_finalize
also iterates through theexit_function_list
structure and call the destructors in the reverse order of initialization, but it will only select the destructors that are registered with the given__dso_handle
in this case. So_dl_call_fini
will only destory local static objects that initialized fromanything::get_instance()
inside the shared library. -
Both
__cxa_finalize
and__run_exit_handlers
will set a flag in theexit_function_list
structure after calling the destructor, so that the destructor will not be called again when other__cxa_finalize
or__run_exit_handlers
is iterating through the entry twice.
So, if we don't properly specify the dependency of shared libraries, and the initialization of those local static objects are initialized from shared libraries, __run_exit_handlers
will call _dl_fini
before looking at those entries of global objects, and _dl_fini
will lead to the destruction of the global objects in the order given by the dependency of shared libraries, not given by the order of initialization.
And if we did not specify the dependency of shared libraries properly, it becomes the order of linking. This leads to wrong order of destruction of global objects in some cases.
- Avoid using singleton pattern across shared libraries.
- Avoid using other global objects in destructors of global objects if you have to do above.
- If you still have to do above, always to correctly specify the dependency of shared libraries.
- In this case,
sth
useslogger
, sosth
should be linked tologger
- Or order the shared libraries in the linking command, so that the dependency is the correct order that library is in front of those it depends on.
- In this case,
- If you still don't want to do as suggested, don't be surprised when things go wrong.
- And this is only the conclusion I got from the implementation of glibc, I am not sure if this is the case for things like
vc++
or things from MacOS. :(