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 clean
make main
(which also buildsbase-orig
version of the library)./main
which runs just finemake base_more
./main
which 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.
2
Answers
The
base.h
header included from main.cpp looks different (has different size) comparing to the same header included by base.cpp which at that point has also themore_
member defined.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;
inmain
,main
does two things:base
objectbase
‘s constructorSince
MORE
wasn’t defined when you compiledmain
, 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 tobase
‘s constructor, which is located in your dynamically-loaded library. SinceMORE
was defined when that library was compiled, it thinks abase
object has onestd::function
member and will try to initialize that member in the memory thatmain
passed 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 abase
object. That means thatmain
andbase
‘s constructor need to agree on how big abase
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 constructorprivate
and add a static functionstd::unique_ptr<base> make_base()
. That way it becomes the sole responsibility of the library to allocate memory forbase
objects, 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 allbase
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 asstd::unique_ptr
orstd::string
).