Skip to content
Advertisement

How to do runtime binding based on CPU capabilities on linux

Is it possible to have a linux library (e.g. “libloader.so”) load another library to resolve any external symbols?

I’ve got a whole bunch of code that gets conditionally compiled for the SIMD level to be supported ( SSE2, AVX, AVX2 ). This works fine if the build platform is the same as the runtime platform. But it hinders reuse across different processor generations.

One thought is to have executable which calls function link to libloader.so that does not directly implement function. Rather, it resolves(binds?) that symbol from another loaded library e.g. libimpl_sse2.so, libimpl_avx2.so or so on depending on cpuflags.

There are hundreds of functions that need to be dynamically bound in this way, so changing the declarations or calling code is not practical. The program linkage is fairly easy to change. The runtime environment variables could also be changed, but I’d prefer not to.

I’ve gotten as far as making an executable that builds and starts with unresolved external symbols (UES) via the ld flag --unresolved-symbols=ignore-all. But subsequent loading of the impl lib does not change the value of the UES function from NULL.

Advertisement

Answer

Edit: I found out later on that the technique described below will only work under limited circumstances. Specifically, your shared libraries must contain functions only, without any global variables. If there are globals inside the libraries that you want to dispatch to, then you will end up with a runtime dynamic linker error. This occurs because global variables are relocated before shared library constructors are invoked. Thus, the linker needs to resolve those references early, before the dispatching scheme described here has a chance to run.


One way of accomplishing what you want is to (ab)use the DT_SONAME field in your shared library’s ELF header. This can be used to alter the name of the file that the dynamic loader (ld-linux-so*) loads at runtime in order to resolve the shared library dependency. This is best explained with an example. Say I compile a shared library libtest.so with the following command line:

g++ test.cc -shared -o libtest.so -Wl,-soname,libtest_dispatch.so

This will create a shared library whose filename is libtest.so, but its DT_SONAME field is set to libtest_dispatch.so. Let’s see what happens when we link a program against it:

g++ testprog.cc -o test -ltest

Let’s examine the runtime library dependencies for the resulting application binary test:

> ldd test
linux-vdso.so.1 =>  (0x00007fffcc5fe000)
libtest_dispatch.so => not found
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fd1e4a55000)
/lib64/ld-linux-x86-64.so.2 (0x00007fd1e4e4f000)

Note that instead of looking for libtest.so, the dynamic loader instead wants to load libtest_dispatch.so instead. You can exploit this to implement the dispatching functionality that you want. Here’s how I would do it:

  • Create the various versions of your shared library. I assume that there is some “generic” version that can always be used, with other optimized versions utilized at runtime as appropriate. I would name the generic version with the “plain” library name libtest.so, and name the others however you choose (e.g. libtest_sse2.so, libtest_avx.so, etc.).

  • When linking the generic version of the library, override its DT_SONAME to something else, like libtest_dispatch.so.

  • Create a dispatcher library called libtest_dispatch.so. When the dispatcher is loaded at application startup, it is responsible for loading the appropriate implementation of the library. Here’s pseudocode for what the implementation of libtest_dispatch.so might look like:

    #include <dlfcn.h>
    #include <stdlib.h>
    
    // the __attribute__ ensures that this function is called when the library is loaded
    __attribute__((constructor)) void init()
    {
        // manually load the appropriate shared library based upon what the CPU supports
        // at runtime
        if (avx_is_available) dlopen("libtest_avx.so", RTLD_NOW | RTLD_GLOBAL);
        else if (sse2_is_available) dlopen("libtest_sse2.so", RTLD_NOW | RTLD_GLOBAL);
        else dlopen("libtest.so", RTLD_NOW | RTLD_GLOBAL);
        // NOTE: this is just an example; you should check the return values from 
        // dlopen() above and handle errors accordingly
    }
    
  • When linking an application against your library, link it against the “vanilla” libtest.so, the one that has its DT_SONAME overridden to point to the dispatcher library. This makes the dispatching essentially transparent to any application authors that use your library.

This should work as described above on Linux. On Mac OS, shared libraries have an “install name” that is analogous to the DT_SONAME used in ELF shared libraries, so a process very similar to the above could be used instead. I’m not sure about whether something similar could be used on Windows.

Note: There is one important assumption made in the above: ABI compatibility between the various implementations of the library. That is, your library should be designed such that it is safe to link against the most generic version at link time while using an optimized version (e.g. libtest_avx.so) at runtime.

User contributions licensed under: CC BY-SA
8 People found this is helpful
Advertisement