Case 13: Symbol Versioning Script¶
| Field | Value |
|---|---|
| Verdict | ๐ข COMPATIBLE |
| Category | Quality (Compatible) |
| Platforms | Linux |
| Flags | โ |
Detected ChangeKinds |
โ |
| Source files | browse on GitHub |
Category: ELF/Linker
Verdict: ๐ข COMPATIBLE โ symbol_version_defined_added: LIBFOO_1.0
Direction tested: unversioned (v1) โ versioned (v2)
What changes¶
| Version | Build flags | Symbol in .dynsym |
|---|---|---|
| v1 (old) | gcc -shared -fPIC bad.c (no version script) |
foo, bar |
| v2 (new) | gcc -shared -fPIC good.c -Wl,--version-script=libfoo.map |
foo@@LIBFOO_1.0, bar@@LIBFOO_1.0 |
bad.c and good.c are identical source โ the only difference is the linker script.
libfoo.map:
What abicheck detects¶
Running abicheck dump + abicheck compare on the compiled .so files:
The ELF detector sees LIBFOO_1.0 in v2's .gnu.version_d section but not in v1 โ version definition added. This is a compatible_addition, not a break.
Why it is COMPATIBLE (unversioned โ versioned)¶
When an existing binary was linked against unversioned foo (v1), its ELF DT_NEEDED
has no version requirement (no DT_VERNEED entry for LIBFOO_1.0). Loading such a
binary against versioned v2 works normally: ld.so resolves foo โ foo@@LIBFOO_1.0
without complaint.
Runtime demo:
make clean && make
# app linked against v1 (unversioned)
gcc -g app.c -L. -lv1 -Wl,-rpath,. -o app_v1
./app_v1
# foo() = 0
# bar() = 1
# Swap in v2 (versioned) โ ld.so resolves transparently
cp libv1.so libv1.so.bak
cp libv2.so libv1.so
./app_v1
# foo() = 0 โ still works, no warnings
# bar() = 1
mv libv1.so.bak libv1.so
The opposite direction IS breaking¶
If you go versioned โ unversioned (v1 has @@LIBFOO_1.0, v2 drops the version
script), binaries compiled against the versioned v1 embed a DT_VERNEED entry
LIBFOO_1.0. Running against unversioned v2:
ld.soprints:no version information available (required by ./app)- Basic symbol lookup still works (soft match), but:
dlvsym(handle, "foo", "LIBFOO_1.0")returnsNULLโ hard failure- Any future
LIBFOO_2.0block becomes impossible to add alongside v1 symbols - abicheck reports:
symbol_version_defined_removed: LIBFOO_1.0โ BREAKING
That reverse scenario is a separate test case.
Why two test suites report different verdicts¶
| Test suite | How it builds | v1 | v2 | Verdict |
|---|---|---|---|---|
test_abi_examples.py |
Builds via CMake (applies --version-script) |
bad.c (unversioned) |
good.c + libfoo.map (versioned) |
COMPATIBLE |
test_example_autodiscovery.py |
Compiles source files directly without CMake linker flags | bad.c |
good.c (no --version-script!) |
NO_CHANGE (both lack version sections) |
The autodiscovery test fallback compilation does not re-apply linker flags from
CMakeLists.txt, so both .so files it compiles lack .gnu.version_d โ no version
change detected โ NO_CHANGE.
This is a known gap, listed in KNOWN_GAPS in test_example_autodiscovery.py.
ELF inspection¶
# v1: bare symbols
nm -D libv1.so | grep -E 'foo|bar'
# 0000000000001108 T bar
# 00000000000010f9 T foo
# v2: versioned symbols
nm -D libv2.so | grep -E 'foo|bar|LIBFOO'
# 0000000000000000 A LIBFOO_1.0 โ version-def aux symbol (SHN_ABS, filtered by abicheck)
# 0000000000001108 T bar@@LIBFOO_1.0
# 00000000000010f9 T foo@@LIBFOO_1.0
# v2: version definition section
readelf --version-info libv2.so
# Version definition section '.gnu.version_d':
# 000000: Rev: 1 Flags: BASE Index: 1 Cnt: 1 Name: libv2.so โ skipped (VER_FLG_BASE)
# 0x001c: Rev: 1 Flags: none Index: 2 Cnt: 1 Name: LIBFOO_1.0 โ recorded as versions_defined
abicheck filters out the LIBFOO_1.0 symbol from .dynsym (it's an SHN_ABS
entry with size=0), but correctly captures LIBFOO_1.0 as a version definition from
.gnu.version_d (skipping the VER_FLG_BASE entry for libv2.so itself).
References¶
Source files¶
See also: Examples overview ยท All COMPATIBLE cases ยท Category: Quality (Compatible).