Take this toy code for an executable and a shared library:
// main.c
void foo() {}
int main() { return 0; }
// bar.c
void foo();
void bar() { foo(); }
Let's build without optimizations, first with gcc:
$ gcc -fpic -shared -o libbar_gcc.so bar.c
$ gcc -L. -lbar_gcc -o prog_gcc main.c
and observe that foo
doesn't appear in the .dynsyms
section - i.e., isn't exported from the executable:
$ readelf --dyn-syms prog_gcc | grep foo
$
Now try the same with clang, and see that it does export foo
:
$ clang -fpic -shared -o libbar_clang.so bar.c
$ clang -L. -lbar_clang -o prog_clang main.c
$ readelf --dyn-syms prog_clang | grep foo
6: 0000000000001130 6 FUNC GLOBAL DEFAULT 13 foo
If we don't link against libbar_clang
, foo
isn't exported:
$ clang -fpic -shared -o libbar_clang.so bar.c
$ clang -o prog_clang main.c
$ readelf --dyn-syms prog_clang | grep foo
$
Even more surprising, if we restore the link against libbar but change bar
's source to not call foo
:
// bar.c
void bar() { }
We'd see that again foo
isn't exported!
$ clang -fpic -shared -o libbar_clang.so bar.c
$ clang -L. -lbar_clang -o prog_clang main.c
$ readelf --dyn-syms prog_clang | grep foo
$
So modifying the source in a dependent shared library - without changing the source or the linkage of the executable - changes clang's decision on what to export from the executable. What kind of sorcery is this??
I'm on Ubuntu 20, clang 14.0.6 and gcc 9.4.0. Both use the ld
linker.
So the breakthrough came while diffing verbose command lines of clang & gcc: turns out that on my distro, by default gcc uses --as-needed
and clang uses --no-as-needed
. I.e., the gcc driver guides the linker to only link against shared libraries which actually define needed symbols (that are yet undefined), while the clang driver guides the linker to merrily link against any shared libs which appear on the command line.
But how does that cause the observable difference in the question?
My current understanding is that while linking the executable, the default linker logic exports only symbols which the linker can see are actually needed to satisfy linkage elsewhere. If the linker can see that some dependent library relies on the executable to implement some symbol, that symbol would be exported without further user intervention.
Sometimes the linker cannot see that a symbol is needed by a library - say if it is loaded at runtime by dlopen
and isn't visible to the linker in the first place. In such a case the user must intervene, probably with adding -rdynamic
to the link line - thereby forcing all public symbols to be exported from the executable.
Anyway, in the original case in question:
libbar.so
is not used directly from the executable (until we
add a bar()
call from main()
!), and so ---as-needed
, andlibbar.so
needs the symbol foo
and does not export it.I'm dumping this here in case this is of interest to anyone, and also please note this is largely a hypothesis. I'm still curious whether anyone can confirm or refute this.