Quiztime II

So, following up from the last quiztime which was about the importance of explicit linking, another case from the wonderful world of shared libraries.

This time we study the implications of dlopen, its parameters and C++. Consider the program and module below. If you run that, it will crash somewhat obscurely in libstdc++. Why?

#0  0x00007f687b2eb126 in std::ostream::sentry::sentry(std::ostream&) () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#1  0x00007f687b2eb889 in std::basic_ostream >& std::__ostream_insert >(std::basic_ostream >&, char const*, long) () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#2  0x00007f687b2ebd57 in std::basic_ostream >& std::operator< <  >(std::basic_ostream >&, char const*) () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#3  0x00007f687a6f01a0 in Impl::print (this=0x558a9bc1f2e0, str="Hello") at module.cc:24
#4  0x0000558a99de3d1b in main (argc=1, argv=0x7ffca28cf538) at main.cc:31
#ifndef INTERFACE_H
#define INTERFACE_H
 
#include <string>
 
class IInterface {
    public:
        virtual ~IInterface() {};
        virtual void print(const std::string& str) = 0;
};
 
#endif // INTERFACE_H
</string>
#include "interface.h"
 
#include <iostream>
 
class Impl : public IInterface {
    public:
        Impl();
        virtual ~Impl();
        void print(const std::string& str);
};
 
extern "C" {
void *entry_point(void)
{
    return new Impl;
}
};
 
Impl::Impl() {};
Impl::~Impl() {};
 
void Impl::print(const std::string& str)
{
    std::cerr < <"Some text to print\n";
    std::cerr << "Got passed this: " << str << "\n";
    std::cerr << "=====\n";
}
#include <dlfcn .h>
#include <iostream>
 
#include "interface.h"
 
extern "C" {
typedef void *(*EntryFunction)(void);
};
 
int main(int argc, char *argv[])
{
    IInterface *iface;
    EntryFunction f;
 
    void *lib = dlopen("./module.so", RTLD_NOW | RTLD_DEEPBIND);
    if (lib == nullptr) {
        std::cerr < < dlerror () << "\n";
        return 1;
    }
 
    f = (EntryFunction) dlsym (lib, "entry_point");
 
    if (f == nullptr) {
        std::cerr << dlerror () << "\n";
        return 1;
    }
 
    iface = reinterpret_cast<IInterface *>(f());
 
    while (true) {
        iface->print ("Hello");
    }
}
</iostream></dlfcn>
.PHONY: all clean
all: main module.so
 
clean:
	rm -f main
	rm -f module.so
 
main: main.cc
	g++ -g -o $@ $< -ldl
 
module.so: module.cc
	g++ -g -o $@ -shared $< -fPIC

3 thoughts on “Quiztime II

  1. Two nits – you can’t use Impl as IInterface, because you use private inheritance (but I guess you meant using public inheritance anyway). And second – you wanted to do reinterpret_cast to the pointer to IIinterface.

    With those two away, I can only guess as I didn’t run it, but I guess this has something to do with using std::cerr global variable, which probably is not initialized in the module.so, eh?

    1. That’s basically a mixture from the pre tag eating things in <> and me being stupid fixing this. Thanks for pointing out. As for cerr, it’s actually more nasty than that.

  2. Despite using reinterpret_cast being atrocious[1], the real root problem is (what I would claim is a) ODR violation. Because how C++ is implemented in g++, both main and module.so will have duplicate symbols (for example from template instantiation), but you are inhibiting ld.so from collapsing symbols to resolve the ODRV because of RTLD_DEEPBIND.

    #include
    #include
    int main(void) { printf(“main.cc> %p\n”, &std::cerr); auto f = reinterpret_cast(dlsym(dlopen(“./module.so”, RTLD_NOW|RTLD_DEEPBIND), “ep”)); f(); return 0; }

    #include
    extern “C” void ep(void) { printf(“module.cc> %p\n”, &std::cerr); }

    In this sense, DEEPBIND is like hiding symbols, cf. https://gcc.gnu.org/bugzilla/show_bug.cgi?id=78747

    [1] better do: entry_point { return static_cast(new Impl); }, main { static_cast(f()); }. It would also solve the case if an Impl were to use virtual inheritance. And it looks nicer. Given typecasts from and to void* can be done with static_cast[2], every (remaining) reinterpret_cast is one too much.

    [2] You still have to make sure that dereferencing a T* [char* exempted] pointer only happens when the pointee is actually a T. Which, in the case of IInterface/Impl, is not necessarily given, even if one disregards virtual inheritance.

Leave a Reply

Your email address will not be published.