Controlling Symbol Visibility ABI Design · static keyword · GCC visibility attribute

 

42.2 — Controlling Symbol Visibility
ABI Design · static keyword · GCC visibility attribute | Chapter 42 · TLPI
EmbeddedPathashala.com — Free Embedded & Linux Tutorials

Why Does Symbol Visibility Matter?

When you build a shared library in C, by default every global function and variable is exported — it becomes visible to any program that links against the library. This might seem harmless, but it causes three real problems that library designers must manage carefully:

  1. ABI stability problem: Users start depending on your internal functions. Now you cannot change or remove them without breaking those users.
  2. Symbol interposition: A global symbol in your library can accidentally “shadow” or be shadowed by same-named symbols in other libraries, causing subtle bugs at runtime.
  3. Performance: Every exported symbol adds an entry to the dynamic symbol table that must be loaded and processed at runtime, even if it is never called externally.

The solution is to only export the symbols that form your documented public API. Everything else should be hidden. This section covers three techniques to achieve this.

Key Concepts

ABI Symbol Visibility static keyword __attribute__((visibility(“hidden”))) __attribute__((visibility(“default”))) Symbol Interposition Dynamic Symbol Table -fvisibility=hidden nm / readelf

The Three Problems of Uncontrolled Visibility

Problem 1: ABI Breakage

Internal function impl_parse() is exported → Users call it → You cannot change its signature in v2 → Version bump required

Problem 2: Interposition

Your library exports init() → Another library also has init() → Dynamic linker picks the wrong one → Silent malfunction

Problem 3: Overhead

500 internal helpers all exported → Dynamic symbol table 500 entries larger → Linker must process all of them at load time

Technique 1: The static Keyword (File-Local Visibility)

The simplest way to hide a symbol is to mark it static. A static function or variable is visible only within the single source file where it is defined.

/* mylib_internal.c */

/* This function is VISIBLE externally — exported by default */
void public_api(void) {
    /* calls the internal helper */
    internal_helper();
}

/* This function is HIDDEN — only visible within this .c file */
static void internal_helper(void) {
    /* implementation detail */
}

/* This variable is HIDDEN — file-local only */
static int internal_counter = 0;

Limitation: static only hides from other .c files. If your shared library is built from multiple .c files and you want a helper function accessible from several of those files but hidden from outside the library — static does not help. You need the visibility attribute instead.

Visibility scope static hidden attribute
Same .c file ✓ Visible ✓ Visible
Other .c files in same library ✗ Hidden ✓ Visible
Programs linking the library ✗ Hidden ✗ Hidden

Technique 2: GCC visibility Attribute

GCC provides a __attribute__((visibility(...))) declaration that controls symbol visibility more precisely than static.

The two main values are:

/* "default" visibility — exported, visible outside the library (this is the default) */
void __attribute__((visibility("default"))) public_function(void) {
    /* ... */
}

/* "hidden" visibility — NOT exported, not visible outside the library
   but CAN be used by other .c files within the same shared library */
void __attribute__((visibility("hidden"))) internal_helper(void) {
    /* ... */
}

Practical usage — define macros for cleaner code:

/* visibility_macros.h */
#if defined(__GNUC__) && __GNUC__ >= 4
    #define LIB_PUBLIC  __attribute__((visibility("default")))
    #define LIB_PRIVATE __attribute__((visibility("hidden")))
#else
    #define LIB_PUBLIC
    #define LIB_PRIVATE
#endif
/* math_lib.c — uses visibility macros */
#include "visibility_macros.h"

/* Public API — visible to callers of the library */
LIB_PUBLIC double fast_sqrt(double x);
LIB_PUBLIC double fast_pow(double base, double exp);

/* Internal helpers — hidden from outside, shared within library */
LIB_PRIVATE double newton_step(double x, double guess);
LIB_PRIVATE double log_approx(double x);

Alternatively, use the compiler flag to hide everything by default and only selectively expose what you want:

# Hide everything by default — requires explicit "default" for each public symbol
gcc -fvisibility=hidden -fPIC -shared -o mylib.so mylib.c
Best practice: Use -fvisibility=hidden when building your library so everything is hidden by default. Then explicitly mark only your public API functions with __attribute__((visibility("default"))). This inverts the default and gives you opt-in exports instead of opt-out.

Code Example 1: Seeing the Problem — All Symbols Exported

/* badlib.c — no visibility control — everything exported (bad practice) */
#include <stdio.h>

/* Internal helper — should NOT be public but will be */
void internal_helper(void) {
    printf("[internal] helper running\n");
}

/* Another internal function */
int compute_internal(int x) {
    return x * x;
}

/* Intended public API */
void public_compute(int x) {
    int result = compute_internal(x);
    internal_helper();
    printf("Result: %d\n", result);
}
gcc -fPIC -shared -o badlib.so badlib.c
readelf --syms --use-dynamic badlib.so | grep -v UND

# Output (all three symbols are GLOBAL — visible to everyone!):
#  ... FUNC GLOBAL DEFAULT ... internal_helper
#  ... FUNC GLOBAL DEFAULT ... compute_internal
#  ... FUNC GLOBAL DEFAULT ... public_compute

Code Example 2: Proper Visibility Control

/* goodlib.c — proper visibility control */
#include <stdio.h>

/* Mark internal functions hidden */
__attribute__((visibility("hidden")))
static void internal_helper(void) {
    printf("[internal] helper running\n");
}

__attribute__((visibility("hidden")))
static int compute_internal(int x) {
    return x * x;
}

/* Public API — explicitly default visibility */
__attribute__((visibility("default")))
void public_compute(int x) {
    int result = compute_internal(x);
    internal_helper();
    printf("Result: %d\n", result);
}
gcc -fPIC -shared -o goodlib.so goodlib.c
readelf --syms --use-dynamic goodlib.so | grep -v UND

# Output (only the intended public function is visible!):
#  ... FUNC GLOBAL DEFAULT ... public_compute
#
# internal_helper and compute_internal do NOT appear in the dynamic
# symbol table — they are hidden
You can verify with nm -D goodlib.so — hidden symbols will not appear. Only public_compute will show with type T (defined global text symbol).

Code Example 3: Using -fvisibility=hidden (Opt-in Export)

/* api.h — public API header for our library */
#define API_EXPORT __attribute__((visibility("default")))

API_EXPORT void  lib_init(void);
API_EXPORT int   lib_process(const char *data, int len);
API_EXPORT void  lib_cleanup(void);
/* lib.c — implementation; all symbols hidden unless marked API_EXPORT */
#include <stdio.h>
#include <string.h>
#include "api.h"

/* Private state — hidden */
static int initialized = 0;
static int total_processed = 0;

/* Private helpers — hidden */
static int validate_input(const char *data, int len) {
    return data != NULL && len > 0;
}

static void log_internal(const char *msg) {
    fprintf(stderr, "[lib internal] %s\n", msg);
}

/* Public functions — marked API_EXPORT in api.h */
void lib_init(void) {
    if (!initialized) {
        log_internal("initializing");
        initialized = 1;
    }
}

int lib_process(const char *data, int len) {
    if (!initialized) { log_internal("not initialized!"); return -1; }
    if (!validate_input(data, len)) return -1;
    total_processed += len;
    printf("Processed %d bytes (total: %d)\n", len, total_processed);
    return len;
}

void lib_cleanup(void) {
    log_internal("cleanup");
    initialized = 0;
    total_processed = 0;
}
# Build with -fvisibility=hidden: everything is hidden UNLESS marked API_EXPORT
gcc -fPIC -fvisibility=hidden -shared -o lib.so lib.c

# Check exports — only lib_init, lib_process, lib_cleanup should appear:
nm -D lib.so | grep ' T '
# T lib_cleanup
# T lib_init
# T lib_process
# (validate_input, log_internal, initialized, total_processed NOT listed)

Interview Questions & Answers

Q1. What are the three problems caused by exporting too many symbols from a shared library?
First, ABI stability: users may start calling internal functions that you did not intend to be public, making it impossible to change them in future versions without breaking backward compatibility. Second, symbol interposition: exported symbols may accidentally shadow or be shadowed by same-named symbols in other libraries, causing wrong functions to be called at runtime. Third, performance overhead: every exported symbol adds an entry to the dynamic symbol table that must be loaded and processed by the dynamic linker at startup.
Q2. What is the difference between using static and using __attribute__((visibility(“hidden”)))?
The static keyword restricts a symbol’s visibility to a single source file — other .c files in the same library cannot use it. The hidden visibility attribute makes a symbol invisible outside the shared library but still accessible from any source file within the same library. Use static when a helper is truly private to one .c file; use hidden when multiple .c files inside the library need to share an internal function without exposing it to library users.
Q3. What does -fvisibility=hidden do and how does it change the development workflow?
The -fvisibility=hidden gcc flag sets the default visibility of all symbols in a compilation unit to hidden. Without this flag, the default is “default” (exported). With it, you must explicitly mark every symbol you want to export with __attribute__((visibility(“default”))). This inverts the workflow: instead of hiding selected symbols, you export selected symbols. It enforces a minimal-export discipline and is considered best practice for production shared libraries.
Q4. How can you verify which symbols a shared library exports?
Two tools work well. nm -D libname.so lists all dynamic symbols; entries marked ‘T’ are defined global functions in the text (code) section — these are the exported functions. readelf –syms –use-dynamic libname.so gives more detail including symbol type, size, and binding (GLOBAL vs LOCAL). Symbols with LOCAL binding are internal/hidden; GLOBAL symbols are exported. You can also use objdump -T libname.so for a similar view.
Q5. What is symbol interposition and how does hidden visibility prevent it?
Symbol interposition occurs when two shared libraries both export a symbol with the same name. The dynamic linker resolves all references to that name to the first definition it finds, which may not be the one the library intended. For example, if your library exports a helper called “init” and the main program also loads another library with an “init” function, calls may go to the wrong one. The hidden visibility attribute prevents this by removing the symbol from the dynamic symbol table entirely — other libraries cannot see or interpose on it.

Leave a Reply

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