Symbol Versioning in Shared Libraries The Linux Programming Interface

 

Symbol Versioning in Shared Libraries
Chapter 42.3 โ€” The Linux Programming Interface | EmbeddedPathashala
๐Ÿ“Œ Topic
Symbol Versioning
๐Ÿงช Examples
3 Code Demos
๐ŸŽฏ Level
Intermediate
๐Ÿ“– Source
TLPI Ch 42

Key Concepts
.symver directive Version Script (.map) VER_1 / VER_2 tags @@ default version objdump -t readelf -s –version-script linker flag ABI compatibility

What is Symbol Versioning?

When you ship a shared library (like libmylib.so), multiple programs may link against it over time. Imagine program p1 was compiled when xyz() worked one way. Later you changed xyz() โ€” now p2 uses the new behavior. But p1 is already deployed and must continue to work as before.

Symbol versioning solves this: the same library can export two versions of the same function at the same time. Each program binds to exactly the version it was compiled against โ€” no recompilation needed, no breakage.

Program p1
compiled vs VER_1
โ†’ libsv.so
Contains both xyz@VER_1
and xyz@@VER_2
โ†’ xyz@VER_1
v1 xyz
Program p2
compiled vs VER_2
โ†’ โ†’ xyz@@VER_2
v2 xyz

Step 1 โ€” Write a Version Script (.map file)

A version script is a plain text file you pass to the linker. It defines which symbols belong to which version tag, and which symbols are public vs hidden.

The version script for the first release of a library looks like this:

/* sv_v1.map - version script for libsv version 1 */

VER_1 {
    global: xyz;   /* xyz() is visible outside the library */
    local:  *;     /* everything else is hidden */
};

global: โ€” symbols listed here are exported (visible to programs linking the library).

local: *; โ€” the wildcard hides all other symbols. This is good practice โ€” you only expose what you intend to.

Note: The version tag name (VER_1) has no inherent meaning. It is just a label. Good practice is to use names like MYLIB_1.0, GLIBC_2.1, etc. โ€” something that includes the library name and version number.

Step 2 โ€” Build the Original Library (v1)
/* sv_lib_v1.c โ€” original library with one version of xyz() */
#include <stdio.h>

void xyz(void) { printf("v1 xyz\n"); }
void pqr(void) { printf("v1 pqr\n"); }
# Compile to position-independent object
gcc -g -c -fPIC -Wall sv_lib_v1.c

# Link as shared library, applying the version script
gcc -g -shared -o libsv.so sv_lib_v1.o -Wl,--version-script,sv_v1.map
/* sv_prog.c โ€” program that calls xyz() */
#include <stdio.h>
void xyz(void);

int main(void) {
    xyz();
    return 0;
}
# Build program p1 against the library
gcc -g -o p1 sv_prog.c libsv.so

# Run it
LD_LIBRARY_PATH=. ./p1
# Output: v1 xyz

At this point p1 is linked against xyz@VER_1. This binding is permanently recorded in the executable’s symbol table.

Step 3 โ€” Add a New Version (Keep Both!)

Now we want to change xyz() but keep backward compatibility for p1. We write a new source file that contains both implementations and uses the .symver assembler directive to tag each one.

/* sv_lib_v2.c โ€” library v2 with two versions of xyz() */
#include <stdio.h>

/*
 * .symver syntax:
 *   __asm__(".symver real_fn_name, exported_name@VERSION_TAG");
 *
 * Single @  = non-default binding (used by old programs)
 * Double @@ = DEFAULT binding (used by new programs linking fresh)
 */
__asm__(".symver xyz_old, xyz@VER_1");   /* old programs use this */
__asm__(".symver xyz_new, xyz@@VER_2");  /* new programs use this */

void xyz_old(void) { printf("v1 xyz\n"); }   /* old behavior */
void xyz_new(void) { printf("v2 xyz\n"); }   /* new behavior */

void pqr(void) { printf("v2 pqr\n"); }

The updated version script adds VER_2 and declares that VER_2 depends on VER_1:

/* sv_v2.map โ€” version script for libsv v2 */

VER_1 {
    global: xyz;
    local:  *;     /* hides xyz_old, xyz_new, etc. */
};

VER_2 {
    global: pqr;   /* new function exported in v2 */
} VER_1;           /* VER_2 inherits rules from VER_1 */
Important: The local: *; in VER_1 is critical. Because VER_2 inherits from VER_1, the wildcard also hides xyz_old and xyz_new from being exported directly. Only the versioned aliases xyz@VER_1 and xyz@@VER_2 are exported.
# Rebuild the shared library with the new version script
gcc -g -c -fPIC -Wall sv_lib_v2.c
gcc -g -shared -o libsv.so sv_lib_v2.o -Wl,--version-script,sv_v2.map

# Build program p2 (will bind to VER_2 โ€” the @@ default)
gcc -g -o p2 sv_prog.c libsv.so

# Verify both programs work correctly
LD_LIBRARY_PATH=. ./p1       # Output: v1 xyz  (uses xyz@VER_1)
LD_LIBRARY_PATH=. ./p2       # Output: v2 xyz  (uses xyz@@VER_2)

Step 4 โ€” Verify with objdump and readelf

We can confirm which version each program is bound to by inspecting its symbol table:

# Show version tag recorded in p1
objdump -t p1 | grep xyz
# 08048380  F *UND* 0000002e  xyz@@VER_1

# Show version tag recorded in p2
objdump -t p2 | grep xyz
# 080483a0  F *UND* 0000002e  xyz@@VER_2

# Alternative: use readelf
readelf -s p1 | grep xyz
readelf -s p2 | grep xyz

Syntax Meaning Use case
xyz@VER_1 Non-default binding Old programs compiled against VER_1
xyz@@VER_2 Default binding (@@) New programs linking fresh to the library
Rule: Exactly one .symver directive for a given symbol must use @@. This is the version new programs will bind to by default. All other older versions use single @.

Version Tag Dependencies โ€” Chaining Versions

Version dependencies can be chained across multiple releases:

/* sv_v3.map โ€” three-generation version script */

VER_1 {
    global: xyz;
    local:  *;
};

VER_2 {
    global: pqr;
} VER_1;             /* VER_2 depends on VER_1 */

VER_3 {
    global: newapi;
} VER_2;             /* VER_3 depends on VER_2 (which depends on VER_1) */
/* Library source with three generations of xyz */
#include <stdio.h>

__asm__(".symver xyz_v1, xyz@VER_1");
__asm__(".symver xyz_v2, xyz@VER_2");
__asm__(".symver xyz_v3, xyz@@VER_3");  /* VER_3 is the current default */

void xyz_v1(void) { printf("xyz: original behavior\n"); }
void xyz_v2(void) { printf("xyz: version 2 behavior\n"); }
void xyz_v3(void) { printf("xyz: version 3 behavior\n"); }

void pqr(void)    { printf("pqr: introduced in VER_2\n"); }
void newapi(void) { printf("newapi: introduced in VER_3\n"); }

This means programs compiled at any of these three generations will continue to get the right behavior โ€” all served from one .so file.

Best Practice (glibc style): Use version tags like MYLIB_1.0, MYLIB_2.0, MYLIB_2.1 โ€” include the package name so version tags remain globally unique when inspecting a binary.

๐ŸŽฏ Interview Questions & Answers
Q1. What problem does symbol versioning solve in shared libraries?
It allows a shared library to export multiple versions of the same function simultaneously. This ensures that old programs compiled against an earlier API continue to work correctly even after the library changes its function behavior โ€” without recompiling the old programs.
Q2. What is the difference between @ and @@ in a .symver directive?
Single @ marks a non-default version (used by programs that were already compiled against that version). Double @@ marks the default version โ€” this is what new programs will bind to when they link against the library. Exactly one .symver directive per symbol must use @@.
Q3. What is a version script and how do you apply it during linking?
A version script is a plain text file (commonly named *.map) that defines which symbols belong to which version tags, and visibility rules (global/local). It is passed to the linker using: gcc -shared -o libfoo.so foo.o -Wl,--version-script,foo.map
Q4. What does local: *; do in a version script?
It hides all symbols that are not explicitly listed under global:. This means internal implementation details (like xyz_old, xyz_new) are not visible outside the library โ€” only the versioned aliases are exported.
Q5. How can you verify which version of a symbol an executable is bound to?
Use objdump -t executable | grep symbol_name or readelf -s executable | grep symbol_name. The output will show the version tag (e.g., xyz@@VER_1 or xyz@@VER_2) recorded at link time.
Q6. What does VER_2 depend on VER_1 mean in a version script? (} VER_1;)
It means VER_2 inherits the global: and local: specifications from VER_1. On Linux, this is mostly semantic โ€” it documents the evolution chain. The practical effect is that a local: *; in VER_1 will also hide symbols not explicitly declared in VER_2.

Leave a Reply

Your email address will not be published. Required fields are marked *