Destructor of a global static variable in a shared library is not called on dlclose
Asked Answered
B

1

12

In a main program, I dlopen and dlclose (LoadLibrary and FreeLibrary respectively) a shared library. The shared library contains a static variable that is instantiated upon dlopen, and destroyed upon dlclose. This behavior is consistent on MSVC 2008 and 2013, GCC 3.4.6, and Sunstudio 12.1. With GCC 4.9.1 and GCC 5.2.1 however, the destructor was no longer called on dlclose. Instead, it was called before program exit.

The particularity of the static variable's class, is that in the constructor, there is a call to a templated function get (of global scope) that returns a local static variable.

I was able to reproduce this behavior with the following one cpp file linked into a shared library:

#include <iostream>

template <typename T> // In my actual code, i is of type T, however, this has no effect
int get()
{
   static int i = 0;
   return i;
}

class Dictionary {
public:
   Dictionary()
   {
      std::cout << "Calling Constructor" << std::endl;
      get<int>();
   }
   ~Dictionary(){
      std::cout << "Calling Destructor" << std::endl;
   }

private:
   Dictionary(const Dictionary&);
   Dictionary& operator=(const Dictionary&);
};
static Dictionary d;

I investigated the tweaks that can be made in order to have the destructor called on dlclose, and concluded the following:

  • If the function get was not templated
  • else if the variable i in the function get was not static
  • else if the function get is made static

The main program's code is the following:

#include <dlfcn.h>
#include <cassert>
#include <string>
#include <iostream>

void* LoadLib(std::string name)
{
      void* libInstance;
      name = "lib" + name + ".so";
      libInstance = dlopen(name.c_str(), RTLD_NOW);
      if ( ! libInstance ) std::cout << "Loading of dictionary library failed. Reason: " << dlerror() << std::endl;
      return libInstance;
}

bool UnloadLib(void* libInstance)
{
     int ret = dlclose(libInstance);
     if (ret == -1)
     {
        std::cout << "Unloading of dictionary library failed. Reason: " << dlerror() << std::endl;
        return false;
     }
     return true;
}

int main()
{
   void* instance = LoadLib("dll");
   assert(instance != 0);

   assert(UnloadLib(instance));
   std::cout << "DLL unloaded" << std::endl;
}

I built the binaries with the following commands:

g++ -m64 -g -std=c++11 -shared -fPIC dll.cpp -o libdll.so
g++ -m64 -g -std=c++11 -ldl main.cpp -o main.out

The output I get when the destructor is called before program exit is the following:

Calling Constructor
DLL unloaded
Calling Destructor

The output I get when the destructor is called on dlclose is the following:

Calling Constructor
Calling Destructor
DLL unloaded

Questions:

  • If the change of behavior between the versions of GCC is not a bug, can you please explain why is the destructor not called on dlclose?
  • Can you please explain for each tweak: Why is the destructor called on dlclose in this case?
Burly answered 21/7, 2016 at 17:23 Comment(2)
Sounds like a bug report you should make against GCCAmand
static variables are now thread-safe (they have to be due to C++ standard changes introduced in C++11). Possibly this makes a difference. Visual Studio 2013 did not have this implemented, while 2015 does. Maybe you should test with 2015. Only gcc 4.x and above implemented this, so that's why you should see if the static variable threading requirement is playing a role.Lacedaemonian
B
7

There is no guarantee that unloading (destructors are invoked) happens on dlclose. On musl (as opposed to glibc), constructors only run the first time a library is run, and destructors only run on exit. For portable code, dlclose cannot be assumed to unload the symbols immediately.

The unload behavior depends on glibc's symbol binding when doing dynamic linking, and is independent of GCC.

The static variable get::i has a STB_GNU_UNIQUE binding. For static variables in inline functions, the uniqueness of the object is assured by the ELF linker. However, for dynamic loading, the dynamic linker assures uniqueness by marking the symbol STB_GNU_UNIQUE. Hence, another attempt to dlopen the same shared library by some other code will lookup the symbol and find that it is unique and return the existent one from the unique symbols table. A symbol with unique binding cannot be unloaded.

Unique binding can be disabled with -fno-gnu-unique if not needed.

References

Bug that I raised to GCC

STB_GNU_UNIQUE

Burly answered 25/7, 2016 at 17:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.