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?
Environment
Linux, amd64, gcc version 9.4.0 (Ubuntu 9.4.0-1ubuntu1~20.04.1)
Setup
Using the code provided below, do the following:
make cleanmake main(which also buildsbase-origversion of the library)./mainwhich runs just finemake base_more./mainwhich crashes withBase hello Base class constructor has non-null MORE Base goodbye Base class destructor has non-null MORE *** stack smashing detected ***: terminated Aborted (core dumped)
Code
library header (base.h)
#ifdef MORE
#include <functional>
#endif
class base
{
public:
base();
~base();
private:
#ifdef MORE
std::function<void()> more_;
#endif
};
library source (base.cpp)
#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
}
Executable (main.cpp)
#include "base.h"
int main()
{
base x;
}
Makefile
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::functionmember into a 1-byte empty class. There simply isn't enough space to hold it.When you say
base x;inmain,maindoes two things:baseobjectbase's constructorSince
MOREwasn't defined when you compiledmain, as far as it is concerned,basehas 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 tobase's constructor, which is located in your dynamically-loaded library. SinceMOREwas defined when that library was compiled, it thinks abaseobject has onestd::functionmember and will try to initialize that member in the memory thatmainpassed it a pointer to. There isn't enough space there, and so it ends up initializingmore_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 abaseobject. That means thatmainandbase's constructor need to agree on how big abaseobject 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 constructorprivateand add a static functionstd::unique_ptr<base> make_base(). That way it becomes the sole responsibility of the library to allocate memory forbaseobjects, and you can never encounter this situation where the main program and the library disagree on how much memory is needed to hold abase. This does, of course, come with some overhead, since it requires that allbaseobjects 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 asstd::unique_ptrorstd::string).