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 libC
s 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 libC
s, 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.
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".