Search code examples
clinuxdynamic-library

Optional dynamic library


Background

Trying to profile an executable, I experimented the profiler Intel VTune and I learn that there is an API library (ITT) that provide utility to start/stop profiling. Its basic functions __itt_resume() and __itt_pause(). What triggers me is that the library is optional, i.e. if the runtime library of ITT is not loaded, these functions are basically noops.

Optional library?

I want to know (first of all on Linux)

  1. Does a process checks that the dynamic library he is linking to is loaded when he starts or when each symbol, or the first symbol of the library is called at runtime (i.e. lazy initialization)? I think on Windows it's at startup because of can't find XXX.dll messages, but I am not sure on Linux. Also, with the example, I don't get any compilation & execution issues even if the symbol is not defined in some_process.c.

  2. How to implement this on Linux? Looking at the Github repo of ITT, among many macro trickery, I feel like the key is here:

#define ITTNOTIFY_VOID(n) (!ITTNOTIFY_NAME(n)) ? (void)0 : ITTNOTIFY_NAME(n)

Basically it wraps every function call with a function pointer call if its not NULL.

  1. How to implement this in a cross-platform way (Windows, Mac, Linux) ?

I end up with a minimal example that looks like the code linked here, but it does not work as it should. In the linked version, my_api_hello_impl() is not called as it should. Also, there is no crash checking the value of the extern symbol api_hello_ptr() when the library is not linked.

my_api.c

#include "my_api.h"
#include <stdio.h>

void(*api_hello_ptr)();

void api_hello_impl()
{
    printf("Hello\n");
}

__attribute__((constructor))
static void init()
{
    printf("linked\n");
    api_hello_ptr = api_hello_impl;
}

my_api.h

#pragma once

extern void(*api_hello_ptr)();

inline void api_hello() { if(api_hello_ptr) api_hello_ptr(); }

some_process.c

#include "my_api.h"

int main()
{
    // NOOPS of not linked at runtime
    api_hello();
}

Makefile

# my_api is not linked to some_process
some_process: some_process.c my_api.h
    $(CC) -o $@ $<

my_api.so: my_api.c my_api.h
    $(CC) -shared -fPIC -o $@ $<

test_linked: some_process my_api.so
    LD_PRELOAD="$(shell pwd)/my_api.so" ./some_process

test_unlinked: some_process my_api.so
    ./some_process

.PHONY: test_linked test_unlinked

Output:

$ make test_linked
LD_PRELOAD="/tmp/tmp.EkrQbILrNg/my_api.so" ./some_process
linked
$ make test_unlinked                                                                                              
./some_process

Solution

  • Does a process checks that the dynamic library he is linking to is loaded when he starts

    Yes, it does. If a dynamic library is linked, then it is a runtime requirement and the system loader will not start execution of a program without finding and loading the library first. There are mechanisms for delayed-loading, but it is not the norm on Linux, they are done manually or using custom libraries. By default, all dynamically linked objects need to be loaded before execution starts.

    Note: I'm assuming we are talking about ELF executables here since we are on Linux.

    How to implement this on Linux?

    You can do it using macros or wrapper functions, plus libdl (link with -ldl), with dlopen() + dlsym(). Basically, in each one of those wrappers, the first thing you do is check if the library was already loaded, and if not, load it. Then, find and call the needed symbol.

    Something like this:

    #include <dlfcn.h>
    #include <stdio.h>
    #include <stdlib.h>
    
    static void *libfoo_handle = NULL;
    static int (*libfoo_func_a)(int, int);
    
    static void load_libfoo_if_needed(void) {
        if (!libfoo_handle) {
            // Without "/" in the path, this will look in all standard system
            // dynamic library directories.
            libfoo_handle = dlopen("libfoo.so", RTLD_LAZY | RTLD_GLOBAL);
    
            if (!libfoo_handle) {
                perror("failed to load libfoo.so");
                _exit(1);
            }
    
            // Optionally use dlsym() here to initialize a set of global
            // function pointers, so that you don't have to do it later.
    
            void *tmp = dlsym(libfoo_handle, "func_a");
            if (!tmp) {
                perror("no symbol func_a in libfoo.so");
                _exit(1);
            }
    
            *((void**)&libfoo_func_a) = tmp;
        }
    }
    
    
    int wrapper_libfoo_func_a(int a, int b) {
        load_libfoo_if_needed();
        return libfoo_func_a(a, b);
    }
    
    // And so on for every function you need. You could use macros as well.
    

    How to implement this in a cross-platform way (Windows, Mac, Linux)?

    For macOS, you should have dlopen() and dlsym() just like in Linux.

    Not sure how to exactly do this on Windows, but I know there is LoadLibrary() available in different flavors (e.g. one, two, etc.), which should be more or less the equivalent of dlopen() and GetProcAddress(), which should be the equivalent of dlsym().

    See also: Loading a library dynamically in Linux or OSX?