GNU Symbol Version Scripts: Add a New Library Interface Without Rebinding Old Clients
Preserve an old client’s symbol binding by keeping its required GNU symbol version available, while assigning a new default version for newly linked clients. A version-node name alone does not implement compatibility: the library must retain the old behavior and the loader must resolve the requested version.
This guide pins GNU Binutils 2.44 and reports an executed GCC 14.2.0, x86-64 GNU ELF fixture. It tests ordinary link-time references to one C function. Non-GNU loaders, other architectures, explicit dlsym/dlvsym lookup and changed function signatures were not tested.
Separate the library identity from the symbol contract
A SONAME identifies a shared-library dependency. A symbol version identifies an interface supplied within that library. The fixture kept the SONAME libcontract.so.1 constant while retaining api at an old version and introducing a new default.
The GNU ld 2.44 VERSION documentation defines version nodes and their dependency relationships. Their names are labels selected by the library maintainer, not numbers interpreted as semantic versions by the loader.
Preserve old nodes and their interfaces once clients depend on them. Adding an inheritance relationship does not automatically synthesize old behavior or adapt arguments. It describes the version graph; implementation compatibility remains the maintainer’s responsibility.

Create a deliberately visible behavior difference
The original fixture library supplied api as the default LIB_1.0 version, returning eleven. A C consumer declared int api(void), linked against that original library and printed the result. Its first execution returned eleven.
The replacement library retained the old implementation and added a new one returning twenty-two:
/* GNU ELF fixture, compiled with GCC 14.2.0 */
int old_impl(void) { return 11; }
int new_impl(void) { return 22; }
__asm__(".symver old_impl,api@LIB_1.0");
__asm__(".symver new_impl,api@@LIB_2.0");
The single-at spelling selects a named nondefault version. The double-at spelling marks the default version used for an ordinary new link. Only one default version of a given symbol is permitted. These are GNU symbol-versioning extensions, not portable C declarations.
The two return values make binding visible without changing the function’s type or risking an incompatible call. A real ABI evolution needs more extensive tests of the old behavior, including error handling and data ownership.
Define nodes and hide implementation aliases
The replacement library used this version script:
LIB_1.0 {
global: api;
local: *;
};
LIB_2.0 {
global: api;
} LIB_1.0;
The build linked the shared object with –version-script and the unchanged SONAME. Dynamic-symbol inspection showed api@LIB_1.0 and api@@LIB_2.0, while old_impl and new_impl were not separately exported.
Hiding those convenience implementation names keeps consumers from bypassing the intended versioned entry points. Check the actual dynamic symbol table after linking; a script’s presence in the source tree does not prove it was passed to the final link.
Avoid broad global wildcards on old nodes. The linker documentation warns that they can accidentally add new symbols to an old exported interface set. Use deliberate assignments and review the exported version map as an interface change.
Keep the old consumer unchanged
The key compatibility test reused the already-linked old executable. Only the library in the owned runtime directory changed from the original to the dual-version replacement. The old executable still returned eleven, and its file hash remained unchanged.
A newly linked executable, built from the same unversioned C declaration but against the replacement library, returned twenty-two. Rebuilding the old client first would have missed the very distinction being tested: a new link selects the current default.
| Executed combination | Result | Binding evidence |
|---|---|---|
| Old client + original library | 11 | Original positive control. |
| Unchanged old client + dual library | 11 | Client requires LIB_1.0. |
| New client + dual library | 22 | Client requires LIB_2.0. |
| Old client + new-only library | Loader rejection. | Required LIB_1.0 is unavailable. |
The final negative control used a library that supplied only the new node. The old process failed with a missing-LIB_1.0 version diagnostic instead of silently rebinding to the new default. The fixture restored the dual-version library afterward.
Inspect definitions and requirements together
Use the versioned readelf reference to inspect dynamic symbols and version information. On the library, identify version definitions and the symbol-to-node association. On each client, inspect version needs rather than assuming its requirement from the current source header.
readelf --dyn-syms --wide ./libcontract.so.1
readelf --version-info ./consumer_old
readelf --version-info ./consumer_new
Record which actual library file the controlled execution uses. A stale search path can make a version test appear to pass against the wrong artifact. Keep the test’s runtime directory private to the fixture instead of changing system loader configuration.
Know when two symbol versions are insufficient
Versioning a function does not automatically preserve a shared global object’s layout, cross-library allocation rules or every callback interaction. An old function that forwards into new state may still violate its former contract even while its symbol version resolves correctly.
Header evolution also needs review: a source rebuild can select a new interface while an old binary retains the old one. Qualify both histories. If the required compatibility cannot be maintained safely, a deliberate library-level ABI break may be more appropriate than keeping a misleading old version node.
Release the version-node map, exported-symbol inventory, unchanged old-client hash and both execution results together. Here the four host combinations passed their intended assertions. They establish the scoped GNU binding mechanism, while target-loader compatibility and production API semantics remain separate acceptance work.