In C++11 code I have a static variable with a nontrivial constructor and destructor (which allocates memory):
// in logger.h
class logger {
public:
class log_data
{
// log_data allocates and frees memory during construction/destruction
}
static log_data l;
};
// in logger.cpp
logger::log_data logger::l;
logger.cpp is then compiled into both a shared library and an executable which links to the shared library itself. I expect the linker to recognize that logger::l is compiled twice and “merge” the two copies.
The problem is that, at runtime, this is what I think is happening:
- The executable initializes logger::l on a given memory address, before main() is called, by calling the constructor
- The library does the same, on the same memory address, overwriting it
- main() is normally executed
- The library deletes the object
- The executable deletes the object at the same address
I am pretty sure that’s the case because:
- on x86, valgrind detects that a block of memory in log_data constructor has been allocated and it is definitely lost at program exit (which makes sense since the first initialization is overwritten by the second). For some reason, it does not complain about double destruction at program exit
- on armv7, the program segfaults after main() is completed after printing “double free or corruption (fasttop)”. I do not have valgrind available on that platform to double check the allocation, but gdb reports a corrupt stack after main exits.
My question is: is there a way to tell the compiler that a static variable has to be constructed and destructed only once, no matter in how many libraries or executable is compiled? Are there any other solutions?
I tried removing logger.cpp from the executable compilation, and linking it with only 1 shared library which includes logger.cpp. It properly works, but the problem remains because logger.cpp can be included in multiple shared libraries, all used by the same executable
Thank you
7
Both the executable and shared libraries will get their own logger::log_data logger::l;
static variable. You can confirm this with a tool like nm
.
I’ve set up a basic local reproduction with the following:
// foo.hpp
void foo();
// foo.cpp
#include "foo.hpp"
void foo() {}
// log.hpp
#include <iostream>
class logger {
public:
logger() {
std::cout<<"logger()"<<std::endl;
}
~logger() {
std::cout<<"~logger()"<<std::endl;
}
void log() {}
};
extern logger logger_global;
// log.cpp
#include "log.hpp"
logger logger_global;
// log_internal.cpp
#include "log.hpp"
void logger::log() {}
// main.cpp
#include <iostream>
#include "log.hpp"
#include "foo.hpp"
int main() {
std::cout<<"main "<<std::endl;
logger_global.log();
foo();
}
// CMakeLists.txt
cmake_minimum_required(VERSION 3.14)
project(test LANGUAGES C CXX)
add_library(foo SHARED log.cpp foo.cpp)
add_executable(exec main.cpp log.cpp)
target_link_libraries(exec PUBLIC foo)
Running the executable:
$ ./exec
logger()
logger()
main
~logger()
~logger()
Looking at nm:
$ nm exec | grep logger_global
0000000000001244 t _GLOBAL__sub_I_logger_global
0000000000004151 B logger_global
$ nm libfoo.so | grep logger_global
0000000000004031 B logger_global
Both the executable and the library get their own logger_global
and each does its own static construction and destruction. Unlike a static library, the linker can’t grab only what is needed from the shared library. The entire shared library must be loaded into memory and as part of that its static data must be initialized. Running in a debugger, you can verify that logger::logger
is called twice with the same this
pointer.
You may wonder what _GLOBAL__sub_I_logger_global
is and why only the executable has it. This function is automatically generated by the compiler and performs static initialization for the global logger. The shared library actually has one too, but, it has a different name: _GLOBAL__sub_I_log.cpp
.
What to do?
You should pull the common logger code out into its own library and have both your library and executable link the logger library.
3
For the double destruction, Valgrind memcheck doesn’t care about destructors. It redirects all allocation and deallocation functions (malloc family and operators new and delete), instruments all memory reads and writes and also wraps all system calls. Of course if multiple calls to a destructor lead to multiple deletes of the same memory then errors will be reported.