Search code examples
c++staticlinkershared-librariesdlopen

Why the main executable and a shared library loaded by dlopen share one copy of a namespace static variable?


As far as I understand namespace scope static variables should have one copy in each compilation unit. So if I have a header file like this:

class BadLad {                                                                                      
public:                                                                                             
    BadLad();                                                                                       
    ~BadLad();                                                                                      
};                                                                                                  

static std::unique_ptr<int> sCount;                                                                 
static BadLad sBadLad;

and badlad.cpp

#include "badlad.h"

BadLad::BadLad() {
    if (!sCount) {
        sCount.reset(new int(1));
        std::cout<<"BadLad, reset count, "<<*sCount<<std::endl;
    }
    else {
        ++*sCount;
        std::cout<<"BadLad, "<<*sCount<<std::endl;
    }
}

BadLad::~BadLad() {
    if (sCount && --*sCount == 0) {
        std::cout<<"~BadLad, delete "<<*sCount<<std::endl;
        delete(sCount.release());
    }
    else {
        std::cout<<"~BadLad, "<<*sCount<<std::endl;
    }
}

I expect sCount and sBadLad to be unique in each cpp file that includes badlad.h.

However, I found it's not the case in the following experiment:

  • I compile badlad to a shared library libBadLad.so.
  • I create another shared library libPlugin.so which links libBadLad.so, only plugin.cpp includes badlad.h, so I expect there is one copy of sCount in libPlugin.so.
  • I create a main program that links libBadLad.so, I expect there is one copy of sCount in main.

The main program looks like this:

#include <dlfcn.h>

int main() {
    void* dll1 = dlopen("./libplugin.so", RTLD_LAZY);
    dlclose(dll1);

    void* dll2 = dlopen("./libplugin.so", RTLD_LAZY);
    dlclose(dll2);

    return 0;
}

When executing the main program, I can see the sCount variable is first created and set to 1 before main is called, which is expected. But then after the first dlopen is called, sCount is incremented to 2, and subsequently decreased to 1 when dlclose is called. The same happens to the second dlopen/dlclose.

So my questions is, why there is only one copy of sCount? Why the linker doesn't keep the copies separate (which I think is what most people expect)? It behaves the same if I link libPlugin.so to main directly instead of dlopen.

I'm running this on macOS with clang-4 (clang-900.0.39.2).

EDIT: please see the full source code in this repo.


Solution

  • (Iteration 2)

    What happens in your case is very interesting and very unfortunate. Let's analyze it step by step.

    1. Your program is linked against libBadLad.so. This shared library is thus loaded on program startup. Constructors of static objects are executed before main.
    2. Your program then dlopens libplugin.so. This shared library is then loaded, and constructors of static objects are executed.
    3. What about libBadLad.so that libplugin.so is linked against? Since the process already contains an image of libBadLad.so, this shared library is not loaded the second time. libplugin.so could just as well not link against it at all.
    4. Back to the static objects of libplugin.so. There are two of them, sCount and sBadLad. Both are constructed, in order.
    5. sBadLad has a user-defined non-inline constructor. It is not defined in libplugin.so, so it is resolved against the already-loaded libBadLad.so, which has this symbol defined.
    6. BadLad::BadLad from libBadLad.so is called.
    7. This constructor references a static variable sCount. This resolves to sCount from libBadLad.so, not sCount from libplugin.so, because the function itself is in libBadLad.so. This is already initialised and is pointing to an int that has the value of 1.
    8. The count is incremented.
    9. Meanwhile, sCount from libplugin.so sits quietly, being initialised to nullptr.
    10. The library is unloaded and loaded again, etc.

    And the moral of the story is? Static variables are evil. Avoid.

    Note the C++ standard has nothing to say about any of this, as it does not deal with dynamic loading.

    However a similar effect can be reproduced without any dynaamic loaading.

       // foo.cpp
       #include "badlad.h"
    
       // bar.cpp
       #include "badlad.h"
       int main () {}
    

    Build and test:

       # > g++ -o test foo.cpp bar.cpp badlad.cpp
       ./test
       BadLad, reset count to, 1
       BadLad, 2
       BadLad, 3
       ~BadLad, 2
       Segmentation fault
    

    Why segmentation fault? This is our good old static initialisation order fiasco. The moral of the story? Static variables are evil.