# Shared Library Versioning ## Overview Shared library versioning is a mechanism used in operating systems to manage different versions of dynamic libraries (.so files on Unix-like systems). It ensures that applications link to the correct version of a library while maintaining compatibility. ## Key Concepts **SONAME** : - The SONAME (Shared Object Name) is an identifier embedded in a shared library that represents its ABI (Application Binary Interface) version. - Applications link against the SONAME, not the actual filename, allowing upgrades without breaking compatibility. **Semantic Versioning** : - Libraries often follow semantic versioning: `MAJOR.MINOR.PATCH`. - **MAJOR**: Changes that break backward compatibility. - **MINOR**: Backward-compatible feature additions. - **PATCH**: Backward-compatible bug fixes. ## ABI vs API - **API (Application Programming Interface)**: The set of functions and symbols exposed by the library. - **ABI (Application Binary Interface)**: The compiled interface, including calling conventions, data types, and memory layout. - ABI stability is crucial for shared libraries because applications depend on binary compatibility. ## Backward Compatibility - Libraries should maintain backward compatibility within the same major version. - Breaking ABI changes require incrementing the major version and updating the SONAME. ## Best Practices - Use SONAME to manage ABI versions. - Avoid breaking ABI unless necessary. - Provide symbolic links: : - `libexample.so` → `libexample.so.1` → `libexample.so.1.2.3` - Document changes clearly. ## Nuances - Different distributions may package libraries differently. - Some systems use `ldconfig` to manage symbolic links. - Applications may fail if the expected SONAME is missing, even if the actual library file exists. ### Examples ## SONAME Usage Example When building a shared library: ```bash clang -shared -Wl,-soname,libexample.so.1 -o libexample.so.1.2.3 example.o ``` Resulting files: ```text libexample.so.1.2.3 # Actual library file libexample.so.1 # SONAME symlink (ABI version) libexample.so # Generic symlink for development ``` Applications link against `libexample.so.1`, not the full filename. ## Semantic Versioning Example - libmath.so.1.0.0 → Initial release - libmath.so.1.1.0 → Adds new functions (backward-compatible) - libmath.so.2.0.0 → Changes function signatures (breaks ABI) When moving from 1.x.x to 2.x.x, update SONAME to `libmath.so.2`. ## ABI vs API Example - **API Change (safe)**: Adding a new function `add_matrix()` does not break existing binaries. - **ABI Break (unsafe)**: Changing `struct Matrix { int rows; int cols; }` to include a new field changes memory layout, breaking existing binaries. ## Symbolic Link Structure After installation: ```text /usr/lib/ libexample.so -> libexample.so.1 libexample.so.1 -> libexample.so.1.2.3 libexample.so.1.2.3 ``` ASCII Diagram: ```text libexample.so → libexample.so.1 → libexample.so.1.2.3 ``` ## Nuances - Different Linux distros may package libraries differently. - If SONAME is missing, apps fail with: ```text error while loading shared libraries: libexample.so.1: cannot open shared object file ``` # GNU ELF Symbol Versioning :::{note} Support for symbol versioning is planned for a future release. Stay tuned for updates ::: This document continues to show a minimal end-to-end demonstration of GNU ELF symbol versioning, then documents: 1. What the `.symver` / `__attribute__((symver))` mechanisms do. 2. What a GNU ld *version script* does. 3. Which ELF sections the linker/loader create/use for symbol versioning. The example exports two versions of the same symbol and preserves backward compatibility across library releases. ```{contents} :depth: 2 :local: true ``` ## A. Minimal Working Example (C + GNU ld) ## Goal Export two versions of an API symbol (`foo`), keeping ABI compatibility for old applications while making a new implementation the default for new applications. ## Example files `demo_v1.c` — first release of the library: ``` // v1 of the library: exports foo() #include void foo(void) { puts("foo v1"); } ``` `demo_v1.map` — version script for first release: ``` DEMO_1 { global: foo; local: *; }; ``` This assigns `foo` to version node `DEMO_1` and hides everything else. `demo_v2.c` — second release (keep old `foo` and add a new default `foo`): ``` #include /* Approach A: classic assembler directive (works with GCC & Clang) */ __asm__(".symver foo_v1,foo@DEMO_1"); // non-default (old) foo __asm__(".symver foo_v2,foo@@DEMO_2"); // default (new) foo void foo_v1(void) { puts("foo v1"); } void foo_v2(void) { puts("foo v2 (default)"); } /* New API symbol in v2 */ void bar(void) { puts("bar v2"); } ``` demo_v2.map — version script for second release: ``` DEMO_1 { global: foo; local: *; }; DEMO_2 { global: foo; bar; } DEMO_1; /* DEMO_2 inherits from DEMO_1 */ ``` `main_old.c` — an “old” app built against v1: ``` void foo(void); int main(void) { foo(); } ``` `main_new.c` — a “new” app built against v2: ``` void foo(void); void bar(void); int main(void) { foo(); bar(); } ``` `Makefile`: ``` CC?=clang CFLAGS=-fPIC -O2 -Wall -Wextra all: stage1 stage2 inspect stage1: libdemo.so.1.0 p_old libdemo.so.1.0: demo_v1.c demo_v1.map $(CC) $(CFLAGS) -shared -Wl,-soname,libdemo.so.1 \ -Wl,--version-script=demo_v1.map -o $@ demo_v1.c ln -sf $@ libdemo.so p_old: main_old.c libdemo.so $(CC) -Wl,-rpath,'$$ORIGIN' -L. -o $@ main_old.c -ldemo stage2: libdemo.so.2.0 p_new libdemo.so.2.0: demo_v2.c demo_v2.map $(CC) $(CFLAGS) -shared -Wl,-soname,libdemo.so.1 \ -Wl,--version-script=demo_v2.map -o $@ demo_v2.c ln -sf $@ libdemo.so p_new: main_new.c libdemo.so $(CC) -Wl,-rpath,'$$ORIGIN' -L. -o $@ main_new.c -ldemo inspect: @echo "==> Exported (with versions) in libdemo.so" @nm -D --with-symbol-versions libdemo.so | egrep ' foo| bar' || true @echo; echo "==> Version sections in libdemo.so" @readelf -W --version-info libdemo.so | sed -n '1,120p' clean: rm -f p_old p_new libdemo.so libdemo.so.* *.o ``` ## Build & Run ```bash # Stage 1: build first library + "old" app make stage1 LD_LIBRARY_PATH=. ./p_old # -> prints: foo v1 ``` ```bash # Stage 2: replace lib with v2 (keeps SONAME), rebuild "new" app make stage2 LD_LIBRARY_PATH=. ./p_old # -> still prints: foo v1 (old app bound to DEMO_1) LD_LIBRARY_PATH=. ./p_new # -> prints: "foo v2 (default)" and "bar v2" ``` ## Inspecting the Result See exported symbols and their versions: ```bash nm -D --with-symbol-versions libdemo.so | egrep ' foo| bar' # ... foo@DEMO_1 # ... foo@@DEMO_2 # ... bar@@DEMO_2 ``` `@@` marks the *default* version; `@` marks a non-default (older) version. See version metadata sections: ```bash readelf -W --version-info libdemo.so ``` You should see: - Version definitions (from `.gnu.version_d`). - Per-symbol version indices in `.gnu.version`. - (In executables) Version needs in `.gnu.version_r`. Optional runtime trace from the dynamic linker: ```bash LD_DEBUG=versions LD_LIBRARY_PATH=. ./p_new 2>&1 | grep -E 'checking for version|needed' ``` ### B. What `.symver` / `__attribute__((symver))` Do - `.symver` (assembler directive) \- Binds a specific implementation symbol to a public name *and* a version node. \- Syntax: `.symver , @` or `.symver , @@`. \- `@@` marks the default implementation selected by new linkers. \- The version node must exist in the version script when you build the DSO. - `__attribute__((symver("...")))` \- Front-end attribute that makes compiler emit the corresponding `.symver`. \- Handy with LTO because it attaches at the language level. Exactly one default definition should exist for a symbol (the one with `@@`). ### C. What a Version Script Does A GNU ld *version script* (use with `-Wl,--version-script=...`): 1. Declares **version nodes** and binds **symbols** to those nodes. 2. Controls visibility (`global:` exported; `local:` hidden). 3. Supports **inheritance** between nodes to evolve the ABI without bumping the SONAME. 4. For C++, you can match demangled names with an `extern "C++" { ... }` block. If you want to tag all currently **exported** symbols with one version without changing visibility, this works: ```none V1 { *; }; ``` This does not force hidden symbols to become exported; it only tags whatever is already exported. ### D. ELF Sections Used by Symbol Versioning When you link with versioned symbols, the following GNU extension sections (referenced by dynamic tags) are produced/used: - `.gnu.version` (*SHT_GNU_versym*, referenced by `DT_VERSYM`) \- A table parallel to `.dynsym` that stores a 16-bit version index per symbol. - `.gnu.version_d` (*SHT_GNU_verdef*, referenced by `DT_VERDEF`/`DT_VERDEFNUM`) \- Version **definitions** provided by this DSO (node names, indices, hashes). - `.gnu.version_r` (*SHT_GNU_verneed*, referenced by `DT_VERNEED`/`DT_VERNEEDNUM`) \- Version **requirements** (what this module needs from its dependencies). `readelf -W --version-info` summarizes these sections so you can verify bindings and defaults. ### E. Practical Notes & Common Pitfalls - C++ name mangling: \- If you version C++ functions with `.symver`, use the **mangled** name. \- In version scripts you can use demangled names inside `extern "C++" { ... }`. - Default vs. non-default: \- Default shows as `@@`; older variants show as `@`. \- Newly linked apps bind to the default; previously linked apps keep using their recorded version. - Diagnostics: \- `nm -D --with-symbol-versions`: quick view of `foo@@V2` / `foo@V1`. \- `readelf -W --version-info`: detailed view of `.gnu.version*` sections. \- `LD_DEBUG=versions`: watch the dynamic linker’s version checks at runtime. ### F. One-Page Cheat Sheet - `.symver` / `__attribute__((symver))`: \- Attach a version to a symbol definition; exactly one default (`@@`) per symbol. \- Ensure the version node exists in the link-time version script. - Version script: \- Define version nodes, bind symbols, control visibility, and express inheritance. \- Can version all currently exported symbols via `V1 { *; };`. - ELF sections: \- `.gnu.version` — per-dynamic-symbol version indices (`DT_VERSYM`). \- `.gnu.version_d` — version definitions (`DT_VERDEF*`). \- `.gnu.version_r` — version requirements (`DT_VERNEED*`). ### Appendix: Quick Commands ```bash # Build first version and old app make stage1 LD_LIBRARY_PATH=. ./p_old # Upgrade library, build new app make stage2 LD_LIBRARY_PATH=. ./p_old LD_LIBRARY_PATH=. ./p_new # Inspect exports and versions nm -D --with-symbol-versions libdemo.so | egrep ' foo| bar' readelf -W --version-info libdemo.so # Trace loader checks ```