What Is a Linker Version Script?
A version script (also called a linker script or symbol map) is a plain text file that gives instructions to the GNU linker (ld) about symbol visibility and versioning. It is specified using the --version-script linker option and commonly identified with the .map extension.
Version scripts give you precise, declarative control over which symbols your shared library exports. Unlike the static keyword (file-by-file) or the visibility attribute (symbol-by-symbol), a version script covers the entire library in one configuration file and supports wildcard patterns.
Version Script Syntax
A version script contains one or more version nodes. Each node has:
- An optional version tag (a name like
VER_1) - A
global:section listing symbols to export - A
local:section listing symbols to hide - Wildcard patterns are allowed in both sections (
*,?)
# Basic version script structure (myscript.map)
VERSION_TAG {
global:
func1; # Exported — visible to all callers
func2;
var_x; # Variables can be listed too
local:
*; # Hide everything else (wildcard)
};
To use it when building the library:
gcc -g -shared -o mylib.so obj1.o obj2.o \
-Wl,--version-script,myscript.map
Anonymous Version Node (Visibility Only)
When you only want visibility control and do not need the symbol versioning feature, use an anonymous version node:
# anon_vis.map — anonymous version node for visibility control only
{
global:
lib_init;
lib_process;
lib_cleanup;
local:
*; /* Hide everything not listed above */
};
gcc -fPIC -shared -o mylib.so *.o -Wl,--version-script,anon_vis.map
local: *; line, every symbol not listed under global: still gets exported with its default (global) visibility. The wildcard * in the local section says “hide everything I have not explicitly exported.”Wildcard Patterns in Version Scripts
Version scripts support the same wildcard characters as shell glob patterns:
| Pattern | Matches | Example |
|---|---|---|
* |
Any string of any length | * matches everything |
? |
Any single character | fn? matches fn1, fn2, fnX |
lib_* |
Any symbol starting with lib_ | lib_init, lib_process, lib_cleanup |
*_internal |
Any symbol ending with _internal | parse_internal, hash_internal |
# Pattern-based version script
VER_1 {
global:
lib_*; /* Export all symbols starting with lib_ */
local:
*; /* Hide everything else */
};
Code Example 1: The TLPI vis Example (Step by Step)
This reproduces the complete example from TLPI Section 42.3.1:
/* vis_comm.c — internal communication helper, NOT for public use */
#include <stdio.h>
void vis_comm(void) {
printf("vis_comm: internal shared helper\n");
}
/* vis_f1.c — public API function 1 */
#include <stdio.h>
extern void vis_comm(void);
void vis_f1(void) {
printf("vis_f1: public function, calling internal...\n");
vis_comm();
}
/* vis_f2.c — public API function 2 */
#include <stdio.h>
extern void vis_comm(void);
void vis_f2(void) {
printf("vis_f2: public function, calling internal...\n");
vis_comm();
}
Step 1 — Build without version script (all three symbols exported):
gcc -g -c -fPIC -Wall vis_comm.c vis_f1.c vis_f2.c
gcc -g -shared -o vis.so vis_comm.o vis_f1.o vis_f2.o
readelf --syms --use-dynamic vis.so | grep vis_
# Shows: vis_comm (GLOBAL), vis_f1 (GLOBAL), vis_f2 (GLOBAL)
# Problem: vis_comm is visible to everyone!
Step 2 — Create the version script:
# vis.map — only export vis_f1 and vis_f2
VER_1 {
global:
vis_f1;
vis_f2;
local:
*; # Hide vis_comm and everything else
};
Step 3 — Build with the version script:
gcc -g -shared -o vis.so vis_comm.o vis_f1.o vis_f2.o \
-Wl,--version-script,vis.map
readelf --syms --use-dynamic vis.so | grep vis_
# Now shows: vis_f1 (GLOBAL), vis_f2 (GLOBAL)
# vis_comm is LOCAL — hidden from outside the library!
Step 4 — Test the library:
/* test_vis.c */
#include <stdio.h>
extern void vis_f1(void);
extern void vis_f2(void);
/* extern void vis_comm(void); -- this would cause a link error! */
int main(void) {
vis_f1();
vis_f2();
return 0;
}
gcc test_vis.c ./vis.so -o test_vis
./test_vis
# vis_f1: public function, calling internal...
# vis_comm: internal shared helper
# vis_f2: public function, calling internal...
# vis_comm: internal shared helper
Code Example 2: Wildcard-Based Version Script
/* biglib.c — a library with many functions following a naming convention */
#include <stdio.h>
/* Public API — all start with "biglib_" */
void biglib_init(void) { printf("biglib_init\n"); }
void biglib_process(void) { printf("biglib_process\n"); }
void biglib_close(void) { printf("biglib_close\n"); }
/* Internal helpers — start with "impl_" */
void impl_alloc(void) { printf("impl_alloc (internal)\n"); }
void impl_validate(void) { printf("impl_validate (internal)\n"); }
static int impl_counter; /* file-local, not exported anyway */
# biglib.map — use prefix wildcard to export only public API
{
global:
biglib_*; /* Export everything starting with biglib_ */
local:
*; /* Hide everything else (impl_*, etc.) */
};
gcc -fPIC -shared -o biglib.so biglib.c -Wl,--version-script,biglib.map
nm -D biglib.so | grep ' T '
# T biglib_close
# T biglib_init
# T biglib_process
# (impl_alloc and impl_validate do NOT appear)
Code Example 3: Comparing All Three Visibility Approaches
| Approach | Where Specified | Granularity | Works Across .c Files | Requires Recompile to Change |
|---|---|---|---|---|
| static keyword | In source code | Per-symbol, per-file | No | Yes |
| visibility attribute | In source code | Per-symbol | Yes | Yes |
| Version script | External .map file | Per-symbol or wildcard | Yes | No (only relink) |
