A version script is a plain text file (usually with a .map extension) that you pass to the GNU linker (ld) when building a shared library. It gives you two powerful capabilities:
- Symbol visibility control — Precisely declare which symbols are public (global) and which are hidden (local), using wildcards if needed.
- Symbol versioning — Allow a single shared library to simultaneously provide multiple versions of the same function, so old and new programs can both run against the same library file without conflicts.
You activate a version script with the linker option:
gcc -Wl,--version-script,yourscript.map ...
Even when you carefully write your library, the C compiler makes all non-static functions globally visible by default. When you build a shared library from multiple .c files, all their functions end up exported — even the ones you only intended as internal helpers that are called between your own source files.
A version script lets you say: “only these specific functions are public, hide everything else” — in one place, without modifying every source file.
global:
vis_f1; /* exported — part of public ABI */
vis_f2; /* exported */
local:
*; /* hide EVERYTHING else */
};
Breaking down each part:
| Element | What It Means |
|---|---|
VER_1 { ... }; |
A version node. Contains declarations for one version. The name (VER_1) is the version tag. In modern ld it can be omitted (anonymous version tag). |
global: |
Symbols listed here are exported — visible to any program linking against this library. |
local: |
Symbols listed here are hidden — invisible to the outside world. |
*; |
A wildcard — matches all symbols not explicitly listed. Using local: *; means “hide everything that wasn’t explicitly declared global.” |
local: *; line, all symbols not mentioned in global: remain visible by default — you must explicitly hide the rest.We have three source files: vis_comm.c (internal helper), vis_f1.c and vis_f2.c (public API). Both f1 and f2 call the internal comm function.
/* vis_comm.c — internal helper, NOT for external use */
#include <stdio.h>
void vis_comm(void) {
printf("vis_comm: shared internal logic\n");
}
/* vis_f1.c — public API function 1 */
#include <stdio.h>
void vis_comm(void); /* use internal helper */
void vis_f1(void) {
printf("vis_f1: called\n");
vis_comm();
}
/* vis_f2.c — public API function 2 */
#include <stdio.h>
void vis_comm(void);
void vis_f2(void) {
printf("vis_f2: called\n");
vis_comm();
}
Now the version script vis.map:
# vis.map — export only vis_f1 and vis_f2, hide vis_comm
VER_1 {
global:
vis_f1;
vis_f2;
local:
*; # hide everything else (including vis_comm)
};
Build without the version script first, then with it:
# Build WITHOUT version script — vis_comm is accidentally exported
gcc -g -fPIC -c -Wall vis_comm.c vis_f1.c vis_f2.c
gcc -g -shared -o vis_bad.so vis_comm.o vis_f1.o vis_f2.o
readelf --dyn-syms vis_bad.so | grep vis_
# Output shows: vis_f1, vis_f2, AND vis_comm — all exported!
# Build WITH version script — vis_comm is hidden
gcc -g -shared -o vis_good.so vis_comm.o vis_f1.o vis_f2.o \
-Wl,--version-script,vis.map
readelf --dyn-syms vis_good.so | grep vis_
# Output shows: ONLY vis_f1 and vis_f2 — vis_comm is hidden!
GLOBAL DEFAULT are exported. Symbols not listed (or showing LOCAL) are hidden. The version script causes vis_comm to disappear from the dynamic symbol table entirely.Normally, if you change the behavior or signature of a function in your shared library, you must bump the major version number (e.g., libfoo.so.1 → libfoo.so.2). All existing programs linked against the old version must be recompiled or they break.
Symbol versioning solves this by allowing a single shared library to provide multiple implementations of the same function — one for old programs, one for new programs. Each program is “locked in” to the version that existed when it was compiled.
libc.so.6 is a single file that contains multiple versioned implementations of functions like memcpy(). Programs compiled 10 years ago use the old version, while new programs use the current version — from the same .so file.xyz@@VER_1 — original implementation. Programs compiled now will link to VER_1 of xyz.xyz@@VER_2 (new default) + xyz@VER_1 (old compat version). New programs use VER_2. Old programs still use their VER_1.When a program is linked against the library, the linker records the version tag of each symbol it used (e.g., xyz@@VER_1). At runtime, the dynamic linker matches that specific versioned symbol — even if a newer version exists in the same .so file.
/* sv_lib_v1.c — version 1 of the library */
#include <stdio.h>
void xyz(void) {
printf("v1 xyz: original implementation\n");
}
# sv_v1.map — version script for V1
VER_1 {
global: xyz;
local: *; # hide everything else
};
# Build V1 library
gcc -g -fPIC -c -Wall sv_lib_v1.c
gcc -g -shared -o libsv.so sv_lib_v1.o -Wl,--version-script,sv_v1.map
/* sv_prog.c — program that uses the library */
#include <stdlib.h>
int main(int argc, char *argv[]) {
void xyz(void); /* forward declaration */
xyz();
return 0;
}
# Link program p1 against the library
gcc -g -o p1 sv_prog.c libsv.so
# Run it
LD_LIBRARY_PATH=. ./p1
# Output: v1 xyz: original implementation
# Check which version of xyz p1 is bound to
readelf --syms p1 | grep xyz
# Shows: xyz@@VER_1 (bound to VER_1)
/* sv_lib_v2.c — version 2 with a new xyz AND the old xyz for compatibility */
#include <stdio.h>
/* Old v1 implementation kept for backward compatibility */
__asm__(".symver xyz_old, xyz@VER_1");
void xyz_old(void) {
printf("v1 xyz: original implementation (compat)\n");
}
/* New v2 implementation — default for new programs */
__asm__(".symver xyz_new, xyz@@VER_2");
void xyz_new(void) {
printf("v2 xyz: improved new implementation!\n");
}
# sv_v2.map — version script for V2
VER_1 {
global: xyz;
local: *;
};
VER_2 {
global: xyz;
} VER_1; # VER_2 depends on VER_1 (inheritance)
# Build V2 library — replaces libsv.so
gcc -g -fPIC -c -Wall sv_lib_v2.c
gcc -g -shared -o libsv.so sv_lib_v2.o -Wl,--version-script,sv_v2.map
# Link new program p2 against V2 library
gcc -g -o p2 sv_prog.c libsv.so
# Test — the old program p1 still uses v1 xyz
LD_LIBRARY_PATH=. ./p1
# Output: v1 xyz: original implementation (compat)
# New program p2 gets v2 xyz
LD_LIBRARY_PATH=. ./p2
# Output: v2 xyz: improved new implementation!
p1 was compiled when only VER_1 existed. Its ELF file contains the requirement xyz@VER_1. Even after the library is updated, the dynamic linker routes p1‘s call to the VER_1 implementation. p2, compiled after the upgrade, gets xyz@@VER_2.| Syntax Element | Explanation | Example |
|---|---|---|
| Version tag | Name of the version node. Can be any identifier. Older ld versions required it; modern ld allows anonymous tags (no name). | VER_1 { ... }; |
| Anonymous tag | Version node with no name. Only one can exist in the script. | { global: foo; local: *; }; |
global: |
Starts list of exported symbols (semicolon-separated) | global: func_a; func_b; |
local: |
Starts list of hidden symbols | local: *; |
Wildcard * |
Matches all remaining symbols. Commonly used as local: *; to hide everything not explicitly exported. |
local: *; |
Wildcard ? |
Matches a single character, like shell globbing | global: get_?; |
| Version dependency | Declares that one version inherits all symbols of another | VER_2 { ... } VER_1; |
# comment |
Everything after # is ignored |
# This is a comment |
@@ default version |
Symbol with @@ is the default version for new links |
xyz@@VER_2 |
@ compat version |
Symbol with single @ is a compatibility version only, not the default |
xyz@VER_1 |
# Show dynamic symbol table with versions
readelf --dyn-syms libsv.so
# Show version definitions in the library
readelf --version-info libsv.so
# Check which symbol versions a program requires
readelf --version-info p1
After building with a version script, readelf --dyn-syms will show something like:
Symbol table '.dynsym' contains N entries:
Num: Value Size Type Bind Vis Ndx Name
5: 00001234 45 FUNC GLOBAL DEFAULT 12 xyz@@VER_2
6: 00001200 45 FUNC GLOBAL DEFAULT 12 xyz@VER_1
@@ means this is the default version (what new programs link to). A single @ means compatibility-only (old programs already linked to this version will use it, but new links won’t).The GNU C Library is the most famous example of symbol versioning in practice. All versions of glibc from 2.0 onward ship as a single major library file: libc.so.6.
Inside this single file, there are versioned implementations of many functions. For example, memcpy has different versions for different glibc releases. A program compiled against glibc 2.14 gets memcpy@@GLIBC_2.14. A program compiled against glibc 2.2.5 uses the older memcpy@GLIBC_2.2.5.
# See all versioned symbols in libc.so.6
readelf --version-info /lib/x86_64-linux-gnu/libc.so.6 | head -30
# See what versions a compiled binary depends on
readelf --version-info /bin/ls
.map extension, though this is a convention and not required. It is activated with the linker option -Wl,--version-script,script.map.global: begins a semicolon-separated list of symbols that will be exported from the shared library — these are visible to programs linking against it. local: begins a list of symbols that will be hidden from the outside world. The wildcard * can be used in either section. The common pattern local: *; means “hide everything not explicitly declared global,” which is the safest default.xyz@@VER_2 (double @) marks the default version — when a new program links against the library and references xyz, the linker binds it to this version. xyz@VER_1 (single @) marks a compatibility version — it exists in the library for old programs that were already linked against VER_1, but new links will not use it by default. Only one version can be the default (@@); others are compat-only (@)..symver GNU assembly directive is used in C source code to associate a function implementation with a specific version tag. The syntax is: __asm__(".symver actual_fn_name, exported_name@VERSION_TAG");. This allows you to have two C functions with different names (e.g., xyz_v1 and xyz_v2) that are both exported under the same public name (xyz) but with different version tags. New programs calling xyz get xyz_v2; old programs that recorded xyz@VER_1 at link time get xyz_v1.__attribute__((visibility("hidden"))) on individual functions and compile with -fvisibility=hidden. This is source-code level and affects one function at a time. Advantage: explicit in the source; Disadvantage: requires modifying every source file. (2) Version scripts — write a .map file with global: and local: *; entries and pass it to the linker. This is linker-level and can control many symbols at once using wildcards without touching source files. Version scripts also enable symbol versioning, which visibility attributes cannot do.