I believe that I have come across a workable solution. It is based upon techniques for creating -nostdlib
executables (such as OS kernels). However, our shared library still links the standard libraries in this case. I found this RaspberryPi forum thread especially useful.
The solution is to manually execute the function pointers stored in the shared library's embedded init_array
. The trick is to use a linker script to define pointers for accessing this array. We then extern
these pointers in the program code. We can repeat the process for executing destructors as well.
In test.cpp
, we have the following:
#include <cstdio>
#include <unistd.h>
class Test
{
public:
Test() { printf("Hello world!\n"); }
~Test() { printf("Goodbye world!\n"); }
};
Test myGlobal; // a global class instance
extern "C"
{
// ensures linker generates executable .so (assuming x86_64)
extern const char test_interp[] __attribute__((section(".interp"))) =
"/lib64/ld-linux-x86-64.so.2";
// function walks the init_array and executes constructors
void manual_init(void)
{
typedef void (*constructor_t)(void);
// _manual_init_array_start and _manual_init_array_end
// are created by linker script.
extern constructor_t _manual_init_array_start[];
extern constructor_t _manual_init_array_end[];
constructor_t *fn = _manual_init_array_start;
while(fn < _manual_init_array_end)
{
(*fn++)();
}
}
// function walks the fini_array and executes destructors
void manual_fini(void)
{
typedef void (*destructor_t)(void);
// _manual_init_array_start and _manual_init_array_end
// are created by linker script.
extern destructor_t _manual_fini_array_start[];
extern destructor_t _manual_fini_array_end[];
destructor_t *fn = _manual_fini_array_start;
while(fn < _manual_fini_array_end)
{
(*fn++)();
}
}
// entry point for libtest.so when it is executed
void lib_entry(void)
{
manual_init(); // call ctors
printf("Entry point of the library!\n");
manual_fini(); // call dtors
_exit(0);
}
We need to manually define the _manual*
pointers through a linker script. We must use the INSERT
keyword so that don't entirely replace ld's default linker script. In a file test.ld
, we have:
SECTIONS
{
.init_array : ALIGN(4)
{
_manual_init_array_start = .;
*(.init_array)
*(SORT_BY_INIT_PRIORITY(.init_array.*))
_manual_init_array_end = .;
}
}
INSERT AFTER .init; /* use INSERT so we don't override default linker script */
SECTIONS
{
.fini_array : ALIGN(4)
{
_manual_fini_array_start = .;
*(.fini_array)
*(SORT_BY_INIT_PRIORITY(.fini_array.*))
_manual_fini_array_end = .;
}
}
INSERT AFTER .fini; /* use INSERT so we don't override default linker script */
We must provide two parameters to the linker: (1) our linker script and (2) the entry point to our library. To compile, we do the following:
g++ test.cpp -fPIC -Wl,-T,test.ld -Wl,-e,lib_entry -shared -o libtest.so
We get the following output when libtest.so
is executed at the commandline:
$ ./libtest.so
Hello world!
Entry point of the library!
Goodbye world!
I've also tried defining globals in .cpp files other than test.cpp
that are also compiled into the shared library. The ctor calls for these globals are included in init_array
, so they are called by manual_init()
. The library works "like normal" when it is loaded as a regular shared library.