0
votes

I'm trying to learn and understand how to develop shared libraries for Linux. However, I encountered a segfault I can't explain. I wonder if this is the right way to implement constructors in a shared library. The source code of my mock up library is the following.

helper.h:

#ifndef HELPER_HELPER_H
#define HELPER_HELPER_H

namespace helper {

int GetHelp(int i=0);

}   // namespace helper

#endif  // HELPER_HELPER_H

helper.cxx:

#include "helper.h"

#include <iostream>
#include <string>


namespace helper {

void __attribute__((constructor)) InitHelp(void)
{
    std::cout << "Setting up Helper" << std::endl;
}

void __attribute__((destructor)) FinishHelp(void)
{
    std::cout << "Leaving Helper" << std::endl;
}

int GetHelp(int i)
{
    return 2*i+1;
}

}   // namespace helper

main.cxx:

#include <iostream>
#include <cstdlib>

#include "helper.h"


int main(int argc, char** argv)
{
    std::cout << "Hello World" << std::endl;

    const int i = 3;
    std::cout << "GetHelp returned " << helper::GetHelp(i) << std::endl;

    std::cout << "Good bye World" << std::endl;

    return EXIT_SUCCESS;
}

I have put them into separate subdirectories and added the simplest possible cmake files.

Root directory:

cmake_minimum_required(VERSION 3.19)

project(MyProject VERSION 1.2.3
                  DESCRIPTION "Very nice project"
                  LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

add_subdirectory(libhelper)

add_subdirectory(pdemo)

libhelper directory:

add_library(helper)
target_sources(helper PUBLIC helper.h
                      PRIVATE helper.cxx)
target_include_directories(helper PUBLIC .)

install(TARGETS helper)

pdemo directory:

add_executable(pdemo)
target_sources(pdemo PRIVATE main.cxx)
target_link_libraries(pdemo PRIVATE helper)

install(TARGETS pdemo)

After creating a build directory, setting up with

cmake -DCMAKE_BUILD_TYPE='Debug' -DBUILD_SHARED_LIBS=ON ..

and compiling, the executable produces a segfault. Tracing back with gdb from the core file it turns out that the problem occurs in the InitHelp function, somewhere deep inside libstdc++. By removing the line std::cout << "Setting up ... the problem disappears and the program produces the expected output. I have tested it with both GCC 5.4.0 and GCC 10.2.0 and both suffers the same problem. Oddly, if LTO is switched on the problem evaporates.

I expect that at the point the constructor is called during the initialisation of the executable all supporting libraries have been set up already, including the C++ standard library. Is this the case? If yes, what causes the segfault? Do I misunderstand the way constructors are intended to be used?

(Note: I understand that I could produce the same result by moving the library constructor to the constructor of a C++ class and instantiating a global object from this class. I guess the compiler would use the same mechanism with some additional steps maybe I'm not aware of. However, I want to understand what's going on and what are the real rules of implementing a shared library.)

Update:

After reading KamilCuk's answers, I started thinking and concluded that things are much worse than I thought before. Consider the following program:

#include <iostream>
#include <cstdlib>

class Foo
{
public:
    Foo() {
        std::cout << "This is Foo speaking" << std::endl; }
};

Foo global_foo;

int main(int argc, char** argv)
{
    std::cout << "Hello World" << std::endl;

    return EXIT_SUCCESS;
}

Based on the things we discussed before this seemingly simple creature is an invalid C++ code. It is clear that if it's statically linked to libstdc++, then it is a static initialization order failure, since there is no guarantee that std::cout, etc will be constructed before global_foo. If it is dynamically linked, then there is no guarantee for proper order of initialization since the loading procedure of shared objects and executables do not define any predetermined order in which library initialization is done and library constructors are run in such a stage of the loading process at which no other libraries are guaranteed to be initialized. Effectively it means that static initialization order fiasco is overextending the boundaries of the linking units (.so and executable files).

Looking at this from a practical viewpoint it means that instantiating static objects in a C++ program must be done with extreme care. The developer must be sure that nothing directly or indirectly happening in the constructor will ever touch anything outside the control of this class, there will be no facilities in other libraries touched, no global variables involved, etc. Being conservative, the best approach is to completely avoid objects as global variables. (I know, it's always important to avoid global variables, but sometimes some exceptions are necessary. Like std::cout :) )

Moreover, designing a shared library for either C or C++, the library constructor can not rely on any other libraries the library depends on.

Do I understand well? By the way, do MS Windows dll's have the same property?

1
Is this the case? no. what's going on std::cout is initialized after your library, that's all. what are the real rules Just don't use global variables in constructors or make sure that these objects are constructed first. Research "static initialization order fiasco". - KamilCuk
Dear KamilChuk, Thank you. I'm a bit surprised, since SIOF is seemingly a C++ problem, that is, the arbitrary order of initialization of static variables between different translation units. The case above is the initialization order between different libraries .so files, so this is a dynamic linking problem. Assuming SIOF applies to this as well, libstdc++ isn't initialized when my library is being set up. If I can't rely on any of the libraries my lib is dependent on while seting up, how could I set up my own library, if likely calls to those libraries are needed? - jozsef

1 Answers

0
votes

Is this the case?

No.

what's going on

std::cout is initialized after your library, that's all.

what are the real rules

Just don't use global variables in constructors or make sure that these objects are constructed first. Research "static initialization order fiasco".

If I can't rely on any of the libraries my lib is dependent on while seting up, how could I set up my own library, if likely calls to those libraries are needed?

Make sure all objects that you depend on are initialized.

In case of global std::cout standard streams there is a std::ios_base::Init.

Also do not use nonportable extensions when you do not need to. __attribute__((__constructor__)) and __attribute__((__destructor__)) are meant for use in C code to hook to startup/shutdown. Just use actual C++ syntax that does exactly the same.

struct InitHelper {
   InitHelper() {
       std::ios_base::Init some_name; // make sure std::cout is initialized
       std::cout << "Setting up Helper" << std::endl;
   }
   ~InitHelper() {
       std::cout << "Leaving Helper" << std::endl;
   }
};

static InitHelper inithelper; // will call constructor and destructor