linkerbinutils

Undefined symbols in executable, override with LD_PRELOAD


Is it possible to produce an executable that has an undefined symbol at static linking time but which runs correctly when using LD_PRELOAD with a library that implements it?

For example:

/* binary.c */
extern void f(void);
int main(void) { f(); }

/* liblib.c */
void f() { puts("OK"); }
$ ./binary
error while loading shared libraries
$ LD_PRELOAD=liblib.so ./binary
OK

I'd like to produce an underlinked ELF which (by design) ought to use LD_PRELOAD for overriding f(). My best attempts with binutils have produced a relocation of type R_X86_64_NONE, which might be correct, but I wonder if I can force ld to produce a proper PLT relocation while leaving the symbol truly undefined.


Solution

  • Is it possible to produce an executable that has an undefined symbol at static linking time but which runs correctly when using LD_PRELOAD with a library that implements it?

    Yes, you can do that fairly easily.

    $ cat liblib.c
    /* liblib.c */
    #include <stdio.h>
    void f() { puts("OK"); }
    

    We'll suppose liblib.so resides in some chosen directory.

    $ mkdir libs
    

    Build it and put it there:

    $ gcc -shared -o ./libs/liblib.so liblib.c
    

    Build your executable like this:

    $ cat binary.c
    /* binary.c */
    extern void f(void);
    int main(void) { f(); }
    
    $ gcc -o binary binary.c -Wl,-z,undefs,--export-dynamic-symbol=f
    

    The linker option -z undefs directs the linker to permit linking the program with unresolved references. That allows f() to be unresolved. The linker option --export-dynamic-symbol=f directs the linker to insert f in the dynamic symbol table of the executable.

    The program won't run without help:

    $ ./binary
    ./binary: symbol lookup error: ./binary: undefined symbol: f
    

    But it will run with liblib.so pre-loaded:

    $ LD_PRELOAD=./libs/liblib.so ./binary
    OK
    

    Or alternatively:

    $ LD_LIBRARY_PATH=./libs LD_PRELOAD=liblib.so ./binary
    OK
    

    But could it be easier?

    If your goal just is to leave f undefined by the static linker for dynamic resolution at runtime, you don't need LD_PRELOAD to achieve that. If there are pecularities of your real-world case that make LD_PRELOAD indispensable then you can skip the rest of this.

    Look at the shared library dependencies of binary:

    $ readelf --dynamic binary | grep -E 'Dynamic|NEEDED'
    Dynamic section at offset 0x2dc8 contains 27 entries:
     0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
     
    

    libc is the only one. gcc links it by default.

    See f in the dynamic symbol table of the executable:

    $ readelf --dyn-syms binary | grep -E 'Symbol|Num|f'
    Symbol table '.dynsym' contains 7 entries:
       Num:    Value          Size Type    Bind   Vis      Ndx Name
         3: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND f
    

    It's present as undefined (UND).

    Here's the GCC primer style in which you'd try to compile and link binary:

    $ gcc -o binary binary.c -Llibs -llib
    

    The program still won't run without help:

     ./binary 
    ./binary: error while loading shared libraries: liblib.so: cannot open shared object file: No such file or directory
    

    Because liblib.so isn't in any of the dynamic linker's default search directories.

    Look again at the shared library dependencies:

    $ readelf --dynamic binary | grep -E 'Dynamic|NEEDED'
    Dynamic section at offset 0x2db8 contains 28 entries:
     0x0000000000000001 (NEEDED)             Shared library: [liblib.so]
     0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
     
    

    Now there's a new dependency on liblib.so.

    Check for f again in the dynamic symbol table:

    $ readelf --dyn-syms binary | grep -E 'Symbol|Num|f'
    Symbol table '.dynsym' contains 7 entries:
       Num:    Value          Size Type    Bind   Vis      Ndx Name
         3: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND f
    

    No change. f is still undefined.

    When the static linker resolves a symbol to a definition that it finds in a shared library answering to -lfoo, by default it just adds libfoo.so to the shared library dependencies of the executable and adds the symbol as undefined to the dynamic symbol table of the executable. It provides the dynamic linker with the information: You need to find libfoo.so to resolve the undefined dynamic symbols in this executable.

    So to provide a runtime definition of void f() you just need to make sure that the dynamic linker can find some liblib.so that defines void f(), like:

    $ LD_LIBRARY_PATH=./libs ./binary 
    OK