I've distilled my problem down to a (hopefully) very simple example. At a high level, I have a shared library which provides a class implementation, and a main executable which uses the library. In my example, the library is then extended with CPPFLAG=-DMORE
so that the class initializer list now has one additional member. Since the ABI signature of the library does not changed, there should be no need to recompile the executable. Yet, in my case, I get a coredump. I do not understand why this is an issue. Can someone please point out where I am going wrong?
Linux, amd64, gcc version 9.4.0 (Ubuntu 9.4.0-1ubuntu1~20.04.1)
Using the code provided below, do the following:
make clean
make main
(which also builds base-orig
version of the library)./main
which runs just finemake base_more
./main
which crashes with
Base hello
Base class constructor has non-null MORE
Base goodbye
Base class destructor has non-null MORE
*** stack smashing detected ***: terminated
Aborted (core dumped)
#ifdef MORE
#include <functional>
#endif
class base
{
public:
base();
~base();
private:
#ifdef MORE
std::function<void()> more_;
#endif
};
#include "base.h"
#include <iostream>
#ifdef MORE
void hi()
{
std::cout << "Hello from MORE" << std::endl;
}
#endif
base::base()
#ifdef MORE
: more_(std::bind(&hi))
#endif
{
std::cout << "Base hello " << std::endl;
#ifdef MORE
if (nullptr != more_)
{
std::cout << "Base class constructor has non-null MORE" << std::endl;
}
#endif
}
base::~base()
{
std::cout << "Base goodbye " << std::endl;
#ifdef MORE
if (nullptr != more_)
{
std::cout << "Base class destructor has non-null MORE" << std::endl;
}
#endif
}
#include "base.h"
int main()
{
base x;
}
base_orig:
g++ -O0 -g -fPIC -shared -olibbase.so base.cpp
objdump -C -S -d libbase.so > orig.objdump
base_more:
g++ -O0 -g -DMORE -fPIC -shared -olibbase.so base.cpp
objdump -C -S -d libbase.so > more.objdump
main: base_orig
g++ -O0 -g -Wextra -Werror main.cpp -o main -L. -Wl,-rpath=. -lbase
objdump -C -S -d main > main.objdump
clean:
rm -f main libbase.so
I tried to go through the objdump
output to figure out why the stack is getting corrupted, but alas, my knowledge of amd64 assembly is rather weak.
You're trying to fit a probably 24 or 32 byte std::function
member into a 1-byte empty class. There simply isn't enough space to hold it.
When you say base x;
in main
, main
does two things:
base
objectbase
's constructorSince MORE
wasn't defined when you compiled main
, as far as it is concerned, base
has no data members. Therefore it will only reserve 1 byte of memory (since every object needs a unique address, even if it's empty). It then passes a pointer to that 1 byte of memory to base
's constructor, which is located in your dynamically-loaded library. Since MORE
was defined when that library was compiled, it thinks a base
object has one std::function
member and will try to initialize that member in the memory that main
passed it a pointer to. There isn't enough space there, and so it ends up initializing more_
in memory that was in used by something else.
Remember, a pointer contains no information about how much memory is available where it points, so base
's constructor must assume that it was passed a pointer to enough memory to hold a base
object. That means that main
and base
's constructor need to agree on how big a base
object is.
The way to avoid this issue is to avoid passing actual objects across library boundaries and only ever pass pointers.
That is, you can make base
's constructor private
and add a static function std::unique_ptr<base> make_base()
. That way it becomes the sole responsibility of the library to allocate memory for base
objects, and you can never encounter this situation where the main program and the library disagree on how much memory is needed to hold a base
. This does, of course, come with some overhead, since it requires that all base
objects be dynamically-allocated. It's also important to make sure the main program and library are compiled using the same compiler and C++ standard library so that you can make they agree on how big any standard library types that you do pass across the library boundary are (such as std::unique_ptr
or std::string
).