In ELF linkers, undefined symbols from DSO participate in symbol resolution, just like undef from .o. The differences with undef from .o are:
- undef purely from DSO do not need .symtab entries
- --no-allow-shlib-undefined can error for such undef (-rpath-link ("load dependent libraries") behavior can suppress some errors.
ELF linkers extract an archive member to satisfy an undefined symbol from a shared object. Here is an example:
1 | echo '.globl _start; _start: call bb' > a.s |
ld extracts c.a(c.o)
and exports cc
into the dynamic symbol table. ld.bfd, gold, and ld.lld have the same behavior.
This archive extraction behavior intrigued me because link.exe (PE/COFF) and ld64 (Mach-O) don't inspect undefined symbols in DLL/dylib for symbol resolution.
1 |
|
a.out
doesn't define or reference cc
.
Thoughts
On ELF, an undefined symbol from a shared object can interact with a .o definition. The .o definition needs to be exported to .dynsym
in case the shared object relies on symbols in the executable.
The rationale is that a shared object may have incomplete DT_NEEDED
entries. An undefined symbol may need to be resolved to the executable or another shared object. we can imagine that not extracting archive members to satisfy an undefined symbol from a shared object can cause ld.so "unresolved symbol" errors. (--no-allow-shlib-undefined
may catch such fragile links.)
However, if every shared object is linked with -z defs
(aka --no-undefined
), we can make sure a shared object won't have unresolved symbols (provided that the runtime shared objects have the same symbol set, which is a reasonable request.) Not extracting archive members will be very robust.
Due to circular dependency or other reasons, it can be difficult to make every shared object -z defs
happy. Unfortunately --no-allow-shlib-undefined
doesn't provide a way to allow some undefined symbols.
The above reasoning might make not extracting an archive member plausible. However, a more important factor is at play here: indirect dependency should not affect symbol resolution. A shared object built with -z defs
is not a good justification because it involves indirect dependency. The linker can only reasonably check direct dependencies for symbol resolution.
One definition rule violation
One definition rule is C++'s, but we can reuse the concept: multiple definitions have more or less unreliability.
In the ld -rpath=. a.o b.so c.a
ELF example, if we add the DF_SYMBOLIC
flag to c.so
a.out
will have definitions trying to interpose c.so
definitions in vain. Linkers don't seem to have any ability to detect such fragile links.
Did "dynamic linking should be similar to static linking" affect the design?
The ELF pioneers had "dynamic linking should be similar to static linking" in mind when designing dynamic linking. I think it means two things: (a) no source-level annotation, (b) emulating archive member extraction.
For ld a.o -lb c.a
, -lb
may refer to either libb.a
or libb.so
. One might argue that the member in c.a
should be extracted to satisfy b
, in case b
refers to libb.a
.
However, an archive and a shared object have a major difference: a shared object tracks dependencies while an archive does not. If we let b.a
track dependencies, the executable linker command line may probably be ld a.o -lb -lb0 c.a
where libb0.a
is another archive satisfying undefined symbols in libb.a
. In this case, not extracting c.a
members is still fine.
Related discussions:
- https://bugs.llvm.org/show_bug.cgi?id=43554
- https://reviews.llvm.org/D108006 (propose
--no-search-static-libs-for-shlib-undefined
to ld.lld) - https://groups.google.com/g/generic-abi/c/NyH6f470Cuc