8. Symbol versioning
All symbol versioning related functionality is guarded by the ELD_ENABLE_SYMBOL_VERSIONING
build macro.
8.1. --default-symver
--default-symver assigns the output’s default version to any exported dynamic
symbol that does not already have an explicit version. The default version name
is the output DT_SONAME for shared libraries when -soname is used;
otherwise it is the basename of the output file.
When --default-symver is active for a supported link mode, eld always creates
the synthetic default version node. This is true even when no regular dynamic
symbol is assigned to that version, such as a hidden-only shared library, a
dynamic executable without -E, or a static PIE without -E.
The ordinary base version definition is emitted by the existing version definition machinery whenever named version definitions are present. This option additionally adds the synthetic default version node.
eld models this option as an internal version script node named after the
default version name with global: *;. The node is registered before user
version scripts so explicit user version-script rules take precedence.
eld creates the default symbol version node for link modes that can produce dynamic symbol/version information. These include:
shared libraries;
dynamic executables, including links forced dynamic with
--force-dynamic;code-independent executables, including PIE and static PIE.
Regular static non-PIE executables do not create the node.
If ELD_ENABLE_SYMBOL_VERSIONING is disabled, eld warns that --default-symver
is unsupported and does not synthesize the node.
8.2. Symbol resolution of versioned symbols
A versioned symbol is of two types:
default versioned symbol (
bar@@V1): a default versioned symbol satisfies undefined references of both the versioned and unversioned references of the symbol.bar@@V1definition can satisfy undefined references of bothbar@V1and plainbar.non-default versioned symbol (
bar@V1): a non-default versioned symbol only satisfies undefined references of the versioned references of the symbol.bar@V1definition can only satisfy undefined references ofbar@V1.
eld supports symbol resolution of versioned symbols by inserting two symbols for default-versioned symbols into the already-existing symbol resolution machinery and adding a symbol normalization phase that runs after symbol resolution to combine the two symbol nodes for the default-versioned symbols into one, if required.
Whenever eld sees a non-default-versioned symbol defintion bar@V1, then eld
inserts bar@V1 to the symbol resolution machinery, and it automatically
gets used to resolve bar@V1 undefined references.
Here is the interesting part, whenever eld sees a default-versioned symbol
definition bar@@V1, then eld inserts two symbols to the symbol resolution
machinery: a canonical symbol bar@V1 and a non-canonical symbol bar.
With this, bar@@V1 definition resolves undefined references to both
bar and bar@V1, as it should.
One fundamental rule of versioned-symbol resolution is that, for a default
versioned symbol, both the canonical symbol (foo@V1) and the non-canonical
(unversioned symbol foo) must resolve to the same definition. Allowing them
to resolve to different symbols is incorrect because, the unversioned
symbol is simply an alias for the versioned symbol (foo@V1), and both are
originally the same symbol (foo@@V1).
8.2.1. Why is symbol normalization required?
As we saw above, for a default-versioned symbol definition bar@@V1,
eld creates two symbols bar@V1 and bar. Some undefined symbol references
may have resolved to bar@V1 definition and some may have resolved to bar
definition. This is a problem because in reality there is only one symbol
bar@@V1. If we keep two symbol nodes per default-versioned symbol definition,
then we will need to also emit both these symbols into the symbol table and
create duplicate GOT/PLT slots for these symbols.
Symbol normalization resolves this duplicate symbol issue by rewriting
all the references to non-canonical symbols (bar) into the canonical symbols
(bar@V1). A key thing to note here is that we only require the non-canonical
aliases during symbol resolution phase.
LLD does not always normalize the symbols and thus creates duplicate GOT/PLT slots for the same symbol when a default versioned symbol is accessed as both plain unversioned reference and versioned reference.
Reproducer:
#!/usr/bin/env bash
cat >1.c <<\EOF
__asm__(".symver foo1,foo@@V1");
int foo1() {
return 1;
}
EOF
cat >vs.t <<\EOF
V1 {
global:
foo;
};
EOF
cat >main.c <<\EOF
#include <stdio.h>
int foo();
__asm__(".symver foov1, foo@V1");
int foov1();
int main() {
int u = foo() + foov1();
printf("u: %d\n",u);
return 0;
}
EOF
clang-20 -o 1.o 1.c -c -fPIC
clang-20 -o main.o main.c -c -fPIC
LDs=(ld.eld ld.lld ld.bfd)
SFs=(eld lld bfd)
for i in "${!SFs[@]}"; do
${LDs[$i]} -o lib1.${SFs[$i]}.so 1.o -shared --version-script vs.t
clang-20 -o main.${SFs[$i]}.out main.o lib1.${SFs[$i]}.so --ld-path=$(which ${LDs[$i]})
echo "${SFs[$i]}:"
llvm-readelf -r main.${SFs[$i]}.out | grep foo
echo ""
done
Output:
eld:
0000000000002230 0000000700000007 R_X86_64_JUMP_SLOT 0000000000000000 foo@V1 + 0
lld:
0000000000003a58 0000000600000007 R_X86_64_JUMP_SLOT 0000000000000000 foo@V1 + 0
0000000000003a68 0000000800000007 R_X86_64_JUMP_SLOT 0000000000000000 foo@V1 + 0
bfd:
0000000000004008 0000000500000007 R_X86_64_JUMP_SLOT 0000000000000000 foo@V1 + 0
8.2.2. Versioned symbols symbol-resolution examples
Symbol resolution of versioned-symbols is largely the same as non-versioned symbols. However, there are (corner) cases that may seem surprising. Here we will see some examples of symbol resolution of versioned symbols. We will also note differences from GNU ld (2.46.50) and lld (22.0) wherever relevant.
In these examples (A) and (B) are simply used to differentiate symbols and to avoid any confusion when referring to them. Please note that (A) and (B) does not mean that the symbols come from different input files.
The order matters in these examples, (A) bar@@V1, (B) bar@V2, means
that the linker first sees bar@@V1 and then sees bar@V2.
The symbols originates from regular object files unstated otherwise.
(A)
bar@@V1, (B)bar@V1
multiple definition error.
(A)
bar@@V1, (B) weakbar@V1
(A) bar@@V1 is used to resolve undefined references to bar@V1 and bar.
(A) weak
bar@@V1, (B)bar@V1
(B) bar@V1 is used to resolve undefined references to bar@V1 and bar.
(B) bar@V1 becomes a default-versioned symbol, and appears as bar@@V1 in
the dynamic symbol table.
(A) weak
bar@@V1, (B)bar
(B) bar is used to resolve undefined references to bar@V1 and bar.
(B) bar becomes a default-versioned symbol, and appears as bar@@V1 in the
dynamic symbol table.
(A) weak
bar@@V1, (B)bar@@V1
(B) bar@@V1 is used to resolve undefined references to bar@V1 and bar.
(A)
bar@@V1, (B)bar@@V2
eld and bfd errors out with multiple definition error.
lld incorrectly links correctly. Reproducer:
#!/usr/bin/env bash
cat >1.c <<\EOF
__asm__(".symver foo1,foo@@V1");
int foo1() {
return 1;
}
__asm__(".symver foo2,foo@@V2");
int foo2() {
return 3;
}
__asm__(".symver bar,bar@V2");
int bar() {
return 5;
}
EOF
cat >main.c <<\EOF
#include <stdio.h>
int foo();
int bar();
int main() {
int u = foo() + bar();
printf("u: %d\n",u);
return 0;
}
EOF
cat >vs.t <<\EOF
V1 {
global:
foo;
};
V2 {
global:
foo;
};
EOF
clang-20 -o 1.o 1.c -c -fPIC
clang-20 -o main.o main.c -c
LDs=(ld.eld ld.lld ld.bfd)
SFs=(eld lld bfd)
for i in "${!SFs[@]}"; do
clang-20 -o lib.${SFs[$i]}.so 1.o --ld-path=$(which ${LDs[$i]}) -shared -fPIC -Wl,--version-script,vs.t
done
(A) weak
bar@@V1, (B)bar, (C)bar@V1
multiple definition error.
(A) weak
bar@@V1, (B) weakbar, (C)bar@V1
(C) bar@V1 is used to resolve undefined references to both bar and bar@V1.
(A) weak
bar@@V1, (B)bar, and version script that assignsV2tobar.
(B) bar becomes a default versioned symbol and appears as bar@@V1
in the dynamic symbol table. There will be no bar@@V2 in the symbol table!
reproducer:
#!/usr/bin/env bash
cat >1.c <<\EOF
__asm__(".symver bar1, bar@@V1");
__attribute__((weak))
int bar1() {
return 1;
}
EOF
cat >2.c <<\EOF
int bar() {
return 3;
}
EOF
cat >3.c <<\EOF
int bar();
__asm__(".symver bar3v1, bar@V1");
int bar3v1();
int b() {
return bar() + bar3v1();
}
EOF
cat >vs.t <<\EOF
V2 {
global:
bar;
};
V1 {
global:
bar;
};
EOF
cat >main.c <<\EOF
#include <stdio.h>
#define show(x) printf("%s: %d\n", #x, x);
int b();
int bar();
__asm__(".symver bar1, bar@V1");
int bar1();
int b_main() {
return bar() + bar1();
}
int main() {
show(b());
show(b_main());
return 0;
}
EOF
TARGET="x86_64-linux-gnu"
clang -target ${TARGET} -o 1.o 1.c -c -fPIC
clang -target ${TARGET} -o 2.o 2.c -c -fPIC
clang -target ${TARGET} -o 3.o 3.c -c -fPIC
clang -target ${TARGET} -o main.o main.c -c
LDs=(ld.eld ld.lld ld.bfd)
SFs=(eld lld bfd)
for i in "${!SFs[@]}"; do
${LDs[$i]} -o lib12.${SFs[$i]}.so 1.o 2.o 3.o --version-script vs.t -shared
clang -target ${TARGET} --ld-path=$(which ${LDs[$i]}) -o main.${SFs[$i]}.out main.o lib12.${SFs[$i]}.so
done
(A)
bar, (B) weakbar@@V1, and version script that assignsV2tobar.
With GNU ld, the dynamic symbol table contains both bar@@V1 and bar@@V2, and
with lld and eld, the dynamic symbol table only contains bar@@V1.
Additionally, GNU seems to incorrectly resolves bar@V1 undefined references to
weak bar@@V1, instead of the plain bar. LLD and eld correctly behaves here.
Reproducer:
#!/usr/bin/env bash
cat >1.c <<\EOF
__asm__(".symver bar1, bar@@V1");
__attribute__((weak))
int bar1() {
return 1;
}
int bar() {
return 5;
}
EOF
cat >2.c <<\EOF
int baz() {
return 3;
}
EOF
cat >3.c <<\EOF
int bar();
__asm__(".symver bar3v1, bar@V1");
int bar3v1();
int b() {
return bar() + bar3v1();
}
EOF
cat >vs.t <<\EOF
V2 {
global:
bar;
};
V1 {
global:
bar;
};
EOF
cat >main.c <<\EOF
#include <stdio.h>
#define show(x) printf("%s: %d\n", #x, x);
int b();
int bar();
__asm__(".symver bar1, bar@V1");
int bar1();
int b_main() {
return bar() + bar1();
}
int main() {
show(b());
show(b_main());
return 0;
}
EOF
TARGET="x86_64-linux-gnu"
clang -target ${TARGET} -o 1.o 1.c -c -fPIC
clang -target ${TARGET} -o 2.o 2.c -c -fPIC
clang -target ${TARGET} -o 3.o 3.c -c -fPIC
clang -target ${TARGET} -o main.o main.c -c
LDs=(ld.eld ld.lld ld.bfd)
SFs=(eld lld bfd)
for i in "${!SFs[@]}"; do
${LDs[$i]} -o lib12.${SFs[$i]}.so 1.o 2.o 3.o --version-script vs.t -shared
clang -target ${TARGET} --ld-path=$(which ${LDs[$i]}) -o main.${SFs[$i]}.out main.o lib12.${SFs[$i]}.so
done
(A) bar, (B)
bar@@V1, and version script that assignsbarto the version nodeV2.
With GNU ld, the dynamic symbol table contains both bar@@V1 and bar@@V2, and
with lld, the dynamic symbol table only contains bar@@V1.
[!IMPORTANT] eld currently reports multiple definition error.
(A)
bar@@V1, (B)bar, and version script that assignsbarto the version nodeV2.
Multiple definition error.