Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What does "dynamic" in "dynamic atexit destructor" mean?

I ported my application from VC++7 to VC++9 recently. Now it sometimes crashes at exit - the runtime starts calling global objects destructors and an access violation occurs in one of them.

Whenever I observer the call stack the top functions are:

CMyClass::~CMyClass() <- crashes here
dynamic atexit destructor for 'ObjectName'
_CRT_INIT()
some more runtime-related functions follow

The question is what is the meaning of the word "dynamic" in "dynamic atexit destructor"? Can it provide any additional information to me?

like image 481
sharptooth Avatar asked Dec 23 '09 12:12

sharptooth


1 Answers

its difficult to pinpoint the exact problem without the actual code, but maybe you can find it yourself after reading this:

from http://www.gershnik.com/tips/cpp.asp

atexit() and dynamic/shared libraries

C and C++ standard libraries include a sometimes useful function: atexit(). It allows the caller to register a callback that is going to be called when the application exits (normally). In C++ it is also integrated with the mechanism that calls destructors of global objects so things that were created before a given call to atexit() will be destroyed before the callback and vice versa. All this should be well known and it works perfectly fine until DLLs or shared libraries enter the picture.

The problem is, of course, that dynamic libraries have their own lifetime that, in general, could end before the main application's one. If a code in a DLL registers one of its own functions as an atexit() callback this callback should better be called before the DLL is unloaded. Otherwise, a crash or something worse will happen during the main application exit. (To make things nasty crashes during exit are notoriously hard to debug since many debuggers have problem dealing with dying processes).

This problem is much better known in the context of the destructors of C++ global objects (which, as mentioned above, are atexit()'s brothers). Obviously any C++ implementation on a platform that supports dynamic libraries had to deal with this issue and the unanimous solution was to call the global destructors either when the shared library is unloaded or on application exit, whichever comes first.

So far so good, except that some implementations "forgot" to extend the same mechanism to the plain old atexit(). Since C++ standard doesn't say anything about dynamic libraries such implementations are technically "correct", but this doesn't help the poor programmer who for one reason or another needs to call atexit() passing a callback that resides in a DLL.

On the platforms I know about the situation is as follows. MSVC on Windows, GCC on Linux and Solaris and SunPro on Solaris all have a "right" atexit() that works the same way as global destructors. However, GCC on FreeBSD at the time of this writing has a "broken" one which always registers callbacks to be executed on the application rather than shared library exit. However, as promised, the global destructors work fine even on FreeBSD.

What should you do in portable code? One solution is, of course, to avoid atexit() completely. If you need its functionality it is easy to replace it with C++ destructors in the following way

//Code with atexit()

void callback()
{
    //do something
}

...
atexit(callback);
...

//Equivalent code without atexit()

class callback
{
public: 
    ~callback()
    {
        //do something
    }
    
    static void register();
private:
    callback()
    {}
    
    //not implemented
    callback(const callback &);
    void operator=(const callback &); 
};

void callback::register()
{
    static callback the_instance;
}

...
callback::register();
...

This works at the expense of much typing and non-intuitive interface. It is important to note that there is no loss of functionality compared to atexit() version. The callback destructor cannot throw exceptions but so do functions invoked by atexit. The callback::register() function may be not thread safe on a given platform but so is atexit() (C++ standard is currently silent on threads so whether to implement atexit() in a thread-safe manner is up to implementation)

What if you want to avoid all the typing above? There usually is a way and it relies on a simple trick. Instead of calling broken atexit() we need to do whatever the C++ compiler does to register global destructors. With GCC and other compilers that implements so-called Itanium ABI (widely used for non Itanium platforms) the magic incantation is called __cxa_atexit. Here is how to use it. First put the code below in some utility header

#if defined(_WIN32) || defined(LINUX) || defined(SOLARIS)

    #include <stdlib.h>

    #define SAFE_ATEXIT_ARG 
    
    inline void safe_atexit(void (*p)(SAFE_ATEXIT_ARG)) 
    {
        atexit(p);
    }

#elif defined(FREEBSD)

    extern "C" int __cxa_atexit(void (*func) (void *), void * arg, void * dso_handle);
    extern "C" void * __dso_handle;     


    #define SAFE_ATEXIT_ARG void *
    
    inline void safe_atexit(void (*p)(SAFE_ATEXIT_ARG))
    {
        __cxa_atexit(p, 0, __dso_handle);
    }

#endif
And then use it as follows


void callback(SAFE_ATEXIT_ARG)
{
    //do something
}

...
safe_atexit(callback);
...

The way __cxa_atexit works is as follows. It registers the callback in a single global list the same way non-DLL aware atexit() does. However it also associates the other two parameters with it. The second parameter is just a nice to have thing. It allows the callback to be passed some context (like some object's this) and so a single callback can be reused for multiple cleanups. The third parameter is the one we really need. It is simply a "cookie" that identifies the shared library that should be associated with the callback. When any shared library is unloaded its cleanup code traverses the atexit callback list and calls (and removes) any callbacks that have a cookie that matches the one associated with the library being unloaded. What should be the value of the cookie? It is not the DLL start address and not its dlopen() handle as one might assume. Instead the handle is stored in a special global variable __dso_handle maintained by C++ runtime.

The safe_atexit function must be inline. This way it picks whatever __dso_handle is used by the calling module which is exactly what we need.

Should you use this approach instead of the verbose and more portable one above? Probably not, though who knows what requirements you might have. Still, even if you don't ever use it, it helps to be aware of how things work so this is why it is included here.

like image 107
Alon Avatar answered Nov 07 '22 02:11

Alon