lld as well as other linkers work hard to make weak undefined symbols work beyond ELF binaries when dynamic linking involved, but unless everything is compiled with -fPIC, they don’t actually work as people would expect.
I think most people do not know that fact, and even for those who have knowledge on ELF, the current half-broken behavior is confusing and not useful. So I’d like to propose we simplify it.
Let me explain why it is half-broken. Assume that we have foo.c with the following contents:
attribute((weak)) void weakfn(void) {}
int main() { if (weakfn) weakfn(); }
What it’s intended to do is to call weakfn only when the function is defined. If you link foo.o against a shared library providing a definition of weakfn, the symbol is added to the executable’s dynamic symbol table as a weak undefined symbol.
Create a shared library
$ echo ‘void weakfn() { puts(“hello”); }’ | clang -xc -o bar.so -shared -fPIC -
Link foo.o and bar.so to create an executable
$ clang -c foo.c
$ clang foo.o bar.so
$ LD_LIBRARY_PATH=. ./a.out
hello
Looks good so far. weakfn is in the dynamic symbol table as a weak undefined symbol.
$ objdump --dynamic-syms a.out |grep weak
0000000000400500 w DF UND 0000000000000000 weakfn
But, is it really weak? Not really. If we remove the symbol from bar.so, the main executable starts to crash.
$ clang -xc -o bar.so -shared -fPIC /dev/null
$ LD_LIBRARY_PATH=. ./a.out
Segmentation fault (core dumped)
This is because weakfn is always resolved to its PLT entry’s address in the main executable. Since the PLT slot address is not zero, weakfn in if (weakfn) weakfn()
is always called even if real weakfn is missing. If weakfn is missing, it’s PLT entry jumps to address zero, so calling the function caused a crash.
We cannot avoid it if we are creating a non-PIC binary, because for non-PIC code, function addresses need to be known at link-time. For imported functions, we use their PLT addresses as their symbol values. Dynamic weak undefined symbol is not representable in non-PIC.
If we are linking a position-independent code, weak undefined symbols work fine. In this case, function addresses are read from GOT slots, and their values can be zero or non-zero depending on their load-time symbol resolution results.
I think the current behavior is bad. I’d like to propose the following changes:
-
If a linker is creating a non-PIC ELF binary, and if it finds a DSO symbol foo for an undefined weak symbol foo, then it adds foo as a strong undefined symbol to the dynamic symbol table. This prevents the above crash because the program fails to start if foo is not found at load-time, instead of crashing at run-time.
-
If a linker is creating a non-PIC ELF binary, and if it cannot find a DSO symbol foo for an undefined weak symbol foo, then it does not add foo to the dynamic symbol table, and it sets foo’s value to zero.
In other words, my suggestion is to make the linker to not try too hard for weak undefined symbols in non-PIC. In non-PIC, if weak undefined symbols cannot be resolved at link-time, their values should be set to zero. Otherwise, they should be handled as if they were regular undefined symbol.
I believe it simplifies the semantics and also simplifies the implementation of the linker. What do you think?