Skip to content

Case 13: Symbol Versioning Script

Field Value
Verdict ๐ŸŸข COMPATIBLE
Category Quality (Compatible)
Platforms Linux
Flags โ€”
Detected ChangeKinds symbol_version_defined_added
Source files examples/case13_symbol_versioning/

Category: ELF/Linker | Verdict: โœ… COMPATIBLE

Verdict and consumer impact

Nothing breaks: a binary already linked against unversioned foo/bar (old) keeps running unmodified when the library adds GNU symbol versioning (new). ld.so resolves the old, unversioned symbol references against the new foo@@LIBFOO_1.0/bar@@LIBFOO_1.0 definitions transparently โ€” no DT_VERNEED entry means the loader doesn't require a specific version, so it accepts the newest one. This is the safe direction to add symbol versioning to a previously-unversioned library.

Old/new diff

old/lib.c new/lib.c
int foo(void) { return 0; } int foo(void) { return 0; }
int bar(void) { return 1; } int bar(void) { return 1; }

Identical source. The only difference is new's link step adds a version script, libfoo.map:

LIBFOO_1.0 {
  global: foo; bar;
  local: *;
};

abicheck command

gcc -shared -fPIC -g old/lib.c -o libfoo_v1.so
gcc -shared -fPIC -g new/lib.c -o libfoo_v2.so -Wl,--version-script=libfoo.map
abicheck compare libfoo_v1.so libfoo_v2.so

Expected abicheck finding

Verdict: COMPATIBLE (exit 0)

Quality Issues:
- symbol_version_defined_added: Symbol version definition added: LIBFOO_1.0

Minimum evidence

min_evidence: L0 โ€” version definitions live in the ELF .gnu.version_d section, readable straight from the binary alongside the dynamic symbol table. No debug info or headers needed.

Why abicheck catches it

abicheck reads each library's .gnu.version_d section as part of its L0 ELF metadata pass (skipping the VER_FLG_BASE self-entry) and diffs the sets of version names between old and new. LIBFOO_1.0 present only on the new side is classified symbol_version_defined_added and placed in the compatible/quality bucket, not the breaking one โ€” going the other direction (a version definition removed, which breaks any consumer whose DT_VERNEED requires it) is a separate, breaking ChangeKind.

Runtime failure demonstration

Severity: INFORMATIONAL โ€” no observable effect on existing binaries.

Scenario: compile app against old (unversioned), swap in new (versioned) .so without recompile.

# Build old library + app
gcc -shared -fPIC -g old/lib.c -o libfoo.so
gcc -g app.c -L. -lfoo -Wl,-rpath,. -o app
./app
# โ†’ foo() = 0
# โ†’ bar() = 1

# Swap in new library (no recompile)
gcc -shared -fPIC -g new/lib.c -o libfoo.so -Wl,--version-script=libfoo.map
./app
# โ†’ foo() = 0   (same โ€” no breakage)
# โ†’ bar() = 1

nm -D libfoo.so | grep -E 'foo|bar'
# โ†’ 0000000000001108 T bar@@LIBFOO_1.0
# โ†’ 00000000000010f9 T foo@@LIBFOO_1.0

Why INFORMATIONAL: app was linked against unversioned symbols, so its ELF has no DT_VERNEED entry requiring LIBFOO_1.0; the dynamic linker resolves foo/bar against the new library's default (@@) versioned definitions without complaint. The opposite direction โ€” a library going from versioned to unversioned, dropping a version consumers already require โ€” is breaking (symbol_version_defined_removed), because dlvsym() lookups pinned to that version would then fail.

Safe redesign

No fix needed โ€” adding a version script to a previously-unversioned library is the correct way to start managing symbol versions going forward. Keep existing symbols in the first version node (as done here) so already-built consumers keep resolving against them; only later API changes need a new LIBFOO_2.0 node.

Real-world example: glibc and most core system libraries ship symbol versioning so multiple ABI generations of the same function (e.g. realpath@GLIBC_2.2.5 vs. realpath@GLIBC_2.3) can coexist in one .so.

Cross-tool comparison

readelf --version-info is the lower-level way to inspect the same fact:

readelf --version-info libfoo_v1.so
# โ†’ No version information found in this file.

readelf --version-info libfoo_v2.so
# โ†’ Version definition section '.gnu.version_d' contains 2 entries:
#     Index: 1  Cnt: 1  Name: libfoo_v2.so     โ† skipped (VER_FLG_BASE)
#     Index: 2  Cnt: 1  Name: LIBFOO_1.0       โ† recorded as versions_defined

References


Source files

  • CMakeLists.txt
  • app.c
  • libfoo.map

See also: Examples overview ยท All COMPATIBLE cases ยท Category: Quality (Compatible).