3

Suppose libA, which I have full control of, depends on libC.so.2. Meanwhile, a third party libB, with which my libA might co-exist within the same process, depends on libC.so.1.

Normal dynamic linking doesn't work because either libA or libB would receive wrong implementation for symbols in libC. How can I make libA works with libB, with minimal modification of the building pipeline of libA?

Huazuo Gao
  • 1,603
  • 14
  • 20

2 Answers2

0

I've searched and tested a bit ...

First, the changing libC:

// libC1.c => libC.so.1
int c(void) { return 21; }
// libC2.c => libC.so.2
int c(void) { return 42; }

Then libA and libB:

// libA.c => libA.so | gcc -fPIC -shared -o libA.so libA.c -l:libC.so.1 -L.
extern int c(void);
int a(void) { return c(); }
// libB.c => libB.so | gcc -fPIC -shared -o libB.so libB.c -l:libC.so.2 -L.
extern int c(void);
int b(void) { return c(); }

Note that above in both cases I specifiy the correct .so file directly (-l:libC.so.1 and -l:libC.so.2). Now both libA and libB refer to the correct libC, but there's a problem: Both libCs export the symbol c!

Thus ...

extern int a(void);
extern int b(void);

#include <stdio.h>

int main() {
  printf("a => %d, b => %d\n", a(), b());
}

... will happily print a => 21, b => 21. The reason is that once the dynamic linker loads one of the libCs, the symbol c (which is undefined in both libA and libB) is resolved (for both libA and libB) to the one loaded.

dlopen seems to be the only way

There are two approaches:

Modify the application using libA and libB

Load both libraries on your own using dlopen, RTLD_LOCAL makes symbols loaded (thus also symbols of dependencies of the loaded library) not visible to the application (or later calls to dlopen).

#include <stdio.h>
#include <assert.h>
#include <dlfcn.h>

int (*a)(void);
int (*b)(void);


int main() {
  void * const a_handle = dlopen("libA.so", RTLD_NOW | RTLD_LOCAL);
  // you could dlopen("libC.so.2", RTLD_NOW | RTLD_GLOBAL) here to "select"
  // the correct symbol `c` for the following, too.
  void * const b_handle = dlopen("libB.so", RTLD_NOW | RTLD_LOCAL);
  assert(a_handle); // real error handling here please!
  assert(b_handle);
  *(void **)(&a) = dlsym(a_handle, "a");
  *(void **)(&b) = dlsym(b_handle, "b");
  assert(a); // real error handling here please!
  assert(b);
  printf("a => %d, b => %d\n", a(), b());
}

Then compiling, linking and running above (main2.c) gives

# gcc main2.c -ldl
# LD_LIBRARY_PATH=. ./a.out
a => 21, b => 42

Modify libA

In the source code of your libA, wherever you call a function funC from libC, you need to replace that call with a call to funC_impl with:

int (*funC_impl)(char *, double); // for a funC(char *, double) which returns an int

// and somewhere during initialization:

void * const c_handle = dlopen("libC.so.2", RTLD_NOW | RTLD_LOCAL | RTLD_DEEPBIND);
// check c_handle != NULL
*(void **)(&funC_impl) = dlsym(c_handle, "funC");
// check for errors! (dlerror)

And all that for every function, of course ... and of course you cannot control libB in that way.

Allegedly -Bsymbolic might help, but I wasn't able to get it to work


If one runs LD_LIBRARY_PATH=. LD_DEBUG=all ./a.out 2>&1 (for the version at the top of the answer) then this is part of the output:

     10545:     Initial object scopes
     10545:     object=./a.out [0]
     10545:      scope 0: ./a.out ./libA.so ./libB.so /nix/store/681354n3k44r8z90m35hm8945vsp95h1-glibc-2.27/lib/libc.so.6 ./libC.so.1 ./libC.so.2 /nix/store/681354n3k44r8z90m35hm8945vsp95h1-glibc-2.27/lib/ld-linux-x86-64.so.2
     10545:     
     10545:     object=linux-vdso.so.1 [0]
     10545:      scope 0: ./a.out ./libA.so ./libB.so /nix/store/681354n3k44r8z90m35hm8945vsp95h1-glibc-2.27/lib/libc.so.6 ./libC.so.1 ./libC.so.2 /nix/store/681354n3k44r8z90m35hm8945vsp95h1-glibc-2.27/lib/ld-linux-x86-64.so.2
     10545:      scope 1: linux-vdso.so.1
     10545:     
     10545:     object=./libA.so [0]
     10545:      scope 0: ./a.out ./libA.so ./libB.so /nix/store/681354n3k44r8z90m35hm8945vsp95h1-glibc-2.27/lib/libc.so.6 ./libC.so.1 ./libC.so.2 /nix/store/681354n3k44r8z90m35hm8945vsp95h1-glibc-2.27/lib/ld-linux-x86-64.so.2
     10545:     
     10545:     object=./libB.so [0]
     10545:      scope 0: ./a.out ./libA.so ./libB.so /nix/store/681354n3k44r8z90m35hm8945vsp95h1-glibc-2.27/lib/libc.so.6 ./libC.so.1 ./libC.so.2 /nix/store/681354n3k44r8z90m35hm8945vsp95h1-glibc-2.27/lib/ld-linux-x86-64.so.2
     10545:     
     10545:     object=/nix/store/681354n3k44r8z90m35hm8945vsp95h1-glibc-2.27/lib/libc.so.6 [0]
     10545:      scope 0: ./a.out ./libA.so ./libB.so /nix/store/681354n3k44r8z90m35hm8945vsp95h1-glibc-2.27/lib/libc.so.6 ./libC.so.1 ./libC.so.2 /nix/store/681354n3k44r8z90m35hm8945vsp95h1-glibc-2.27/lib/ld-linux-x86-64.so.2
     10545:     
     10545:     object=./libC.so.1 [0]
     10545:      scope 0: ./a.out ./libA.so ./libB.so /nix/store/681354n3k44r8z90m35hm8945vsp95h1-glibc-2.27/lib/libc.so.6 ./libC.so.1 ./libC.so.2 /nix/store/681354n3k44r8z90m35hm8945vsp95h1-glibc-2.27/lib/ld-linux-x86-64.so.2
     10545:     
     10545:     object=./libC.so.2 [0]
     10545:      scope 0: ./a.out ./libA.so ./libB.so /nix/store/681354n3k44r8z90m35hm8945vsp95h1-glibc-2.27/lib/libc.so.6 ./libC.so.1 ./libC.so.2 /nix/store/681354n3k44r8z90m35hm8945vsp95h1-glibc-2.27/lib/ld-linux-x86-64.so.2
     10545:     
     10545:     object=/nix/store/681354n3k44r8z90m35hm8945vsp95h1-glibc-2.27/lib/ld-linux-x86-64.so.2 [0]
     10545:      no scope
     10545:     

The issue is that for both libA and libB the initial scope contains the libraries libC.so.1 and libC.so.2 in that order. So when resolving the symbol c in each of libA and libB, it first looks into libC.so.1, finds the symbol and is done with it.

Now "all" that's missing is a way to change this "initial object scope".

Daniel Jour
  • 15,896
  • 2
  • 36
  • 63
0

In case you are ok with changing names of symbols from libC.so.2 you can use Implib.so's renaming functionality. E.g. to change all libC.so.2 symbols to have MYPREFIX_ prefix:

$ cat mycallback.c
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>

#ifdef __cplusplus
extern "C"
#endif
void *mycallback() {
  void *h = dlmopen(LM_ID_NEWLM, "libxyz.so", RTLD_LAZY | RTLD_DEEPBIND);
  if (h)
    return h;
  fprintf(stderr, "dlmopen failed: %s\n", dlerror());
  exit(1);
}
$ implib-gen.py --dlopen-callback=mycallback --symbol_prefix=MYPREFIX_ libC.so.2
$ ... # Link your app with libC.so.2.tramp.S, libC.so.2.init.c and mycallback.c, keep libC.so.1 unchanged

Function names in libC.so.2's header will need to be updated as well (often that's a simple s/// in vim).

Implib.so works by generating a bunch of wrappers for each symbol in problematic library (in this case libC.so.2) and forwarding calls to their actual implementation internally (via dlsym).

yugr
  • 19,769
  • 3
  • 51
  • 96