Linker Version Scripts Controlling Symbol Visibility with .map Files

 

42.3.1 — Linker Version Scripts
Controlling Symbol Visibility with .map Files | Chapter 42 · TLPI
EmbeddedPathashala.com — Free Embedded & Linux Tutorials

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.

Key Concepts

Version Script .map file –version-script global: keyword local: keyword version node version tag wildcard * anonymous version readelf –syms

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
Version tag: Old versions of ld required a version tag even when only controlling visibility. Modern ld allows an anonymous version node (no tag), but only one anonymous node is allowed per script. When using the script only for visibility (not versioning), you can omit the tag entirely.

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
Why local: * is important: Without the 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
Note: vis_comm still runs — it is just hidden from the external symbol table. Code inside the library can still call it freely. The restriction is only on external callers.

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)
Version scripts are the most powerful approach because they are separate from source code. You can change the exported symbol set by modifying the .map file and relinking — no source recompilation needed. This is particularly useful for maintaining backward compatibility during library evolution.

Interview Questions & Answers

Q1. What is a linker version script and what file extension is typically used?
A linker version script is a plain text file containing instructions for the GNU linker (ld) about which symbols to export and how to version them. It is passed to the linker via the –version-script option. The conventional file extension is .map, though the linker does not require any specific extension.
Q2. What is the purpose of the local: *; line in a version script?
The local: *; line with a wildcard says “hide all symbols that were not explicitly listed under global:”. Without this line, all symbols not listed under global still get exported with their default visibility. The wildcard in the local section acts as a catch-all fallback that hides everything you did not explicitly choose to export. It is the key line that makes the version script actually restrict visibility.
Q3. What is an anonymous version node in a linker script?
Normally a version node is prefixed with a version tag name like VER_1. An anonymous version node has no tag name — the braces start immediately without a preceding identifier. Anonymous version nodes are allowed in modern ld when the script is used only for visibility control (not for symbol versioning). However, only one anonymous node is permitted per script file. Old versions of ld required a version tag even for visibility-only scripts.
Q4. How do you verify that a version script correctly hides symbols?
Use readelf –syms –use-dynamic libname.so or nm -D libname.so. Symbols with GLOBAL binding are exported and visible to callers. Symbols with LOCAL binding are hidden inside the library. After applying a version script with local: *;, any internal symbols should appear as LOCAL in the dynamic symbol table output, while only the explicitly listed global: symbols should appear as GLOBAL.
Q5. Can a symbol hidden by a version script still be called from within the library itself?
Yes. The local: specification in a version script only removes the symbol from the library’s dynamic symbol table, which is what external callers use. Code within the same shared library can still call any internally defined function by name, regardless of its visibility status. The visibility restriction is purely about external visibility — what other programs and libraries can see and link against.

Leave a Reply

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