SourceXR

C/C++ Cross-Reference Tool

A Binary as a Shared Library and Vice-Versa

A binary is an executable with a default entry point called main(). When the binary is executed (through the execve family of system calls), this function is called (after the initialization functions such as static variables init, etc).

A shared library is an executable whose is position independant (i.e. its address in memory is not predefined, it can be loaded anywhere, and an indirection is set up to call the exported functions), and which exports symbols that can be called from outside the library (variables and functions), either implicitly (at link time), or explicitly through the use of the dlsym() function found in the dlfcn.h header at run time.

But these two modes are not exclusive. You can have a binary which can be loaded as a shared library and vice-versa.

For example, the C libary is executable:

bash $ /lib/x86_64-linux-gnu/libc.so.6

GNU C Library (Debian EGLIBC 2.13-38) stable release version 2.13, by Roland McGrath et al.
Copyright (C) 2011 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 4.4.7.
Compiled on a Linux 3.2.35 system on 2012-12-30.
[...]

To build a binary, you need an entry point, and to build a library you need to export some symbols and be position independent.

Fortunately, the GNU compiler and linker make this operation very easy.

Here are the different steps to build this kind of executable.

Sample code for a minimalist shared library:

#include <iostream>

// exported symbol
extern "C"
int run () {
    std::cout << "so\n";
    return 0;
}

Compile it with the following command line:

g++ -o lib -shared -fPIC lib.cpp

The two flags tell the compiler and linker to generate position independent code (PIC) and to build a shared library.

The symbol is exported as "C" to avoid the name mangling generated in C++.

A binary loading a shared library:

#include <dlfcn.h>
#include <iostream>

int main () {

    // open the shared library
    void *lib = dlopen ("lib", RTLD_LAZY);

    if (!lib) {
        std::cerr << dlerror () << "\n";
        return 1;
    }

    // exported symbol from the library:
    const char *sym = "run";

    // get the symbol address:
    static int (*r) ();
    union { int (*t) (); void *obj; } alias;
    alias.obj = dlsym (lib, sym);
    r = alias.t;
    if (!r) {
        std::cerr << dlerror () << "\n";
        dlclose (lib);
        return 1;
    }

    // call the exported function:
    r ();

    dlclose (lib);
    return 0;
}

Compile it with the following command line:

g++ -o loadpie loadpie.cpp -ldl

If we execute the binary, we see the following output:

$ ./loadpie
so

Now we modify the shared library to have a binary. We add a main() function:

#include <iostream>

extern "C"
int run () {
    std::cout << "so\n";
    return 0;
}

int main () {
    std::cout << "pie\n";
    return 0;
}

And change the compilation command line to have the linker generate a binary:

g++ -o pie -pie -fPIC -Wl,-E pie.cpp

We are still using position independant code (-fPIC) but we tell the linker to generate an executable (-pie), and to export symbols (-Wl,-E) (otherwise the symbols to export will not be visible). With this option you can even load the symbols from the binary itself.

Now, we can call the library in two contexts:

  • as a binary:
$ ./pie
pie
  • and as a shared library:
$ ./loadpie
so

Comments !