Keyboard Shortcuts

Ctrl+K
Open search
Esc
Close search / dialog
↑
Back to top

Symbol Visibility

15 minintermediateModule 5: SymbolsContinues from binding

Why This Matters

When you build a shared library (.so), you decide which functions are part of your public API and which are internal implementation details. Symbol visibility is the ELF mechanism for this — it controls what the dynamic linker can see and reference from outside the library.

Correct visibility prevents symbol collisions between libraries, reduces dynamic linker overhead, and enables better optimization.

A Simple Model

Think of visibility as four levels of "doors" on your library:

The Actual Detail

Visibility is stored in the low 2 bits of st_other in the Elf64_Sym structure:

VisibilityConstantValueBehavior
DefaultSTV_DEFAULT0Symbol is exported. Dynamic linker can interpose it (LD_PRELOAD).
InternalSTV_INTERNAL1Symbol is local to the module. Not visible outside. May have special meaning for processor.
HiddenSTV_HIDDEN2Symbol is not exported. Dynamic linker cannot resolve it from other modules.
ProtectedSTV_PROTECTED3Symbol is exported but references within the module resolve to the local definition (no interposition).

How to set visibility in C/C++:

VisibilityC/C++ syntaxEffect
Defaultvoid public_api() -- no attribute neededExported, interposable
Hidden__attribute__((visibility("hidden"))) void internal_fn()Not exported
Protected__attribute__((visibility("protected"))) void stable_api()Exported, not interposable

Compile with -fvisibility=hidden to make all symbols hidden by default.

A Real Example

A shared library with controlled visibility:

# libdemo.c defines 4 functions:
#   exported()     -- default visibility (EXPORTED)
#   hidden_fn()    -- hidden visibility (NOT exported)
#   protected_fn() -- protected visibility (exported, no interposition)
#   default_fn()   -- default visibility, but -fvisibility=hidden makes it HIDDEN

$ gcc -fvisibility=hidden -shared -o libdemo.so libdemo.c

$ readelf -sW libdemo.so | grep -E 'DEFAULT|HIDDEN|PROTECTED'
  1: 0000000000001129     5 FUNC    GLOBAL DEFAULT   13 exported
  2: 0000000000001132     5 FUNC    GLOBAL HIDDEN    13 hidden_fn
  3: 000000000000113b     5 FUNC    GLOBAL PROTECTED 13 protected_fn
  4: 0000000000001144     5 FUNC    GLOBAL HIDDEN    13 default_fn

Note: default_fn is hidden because we compiled with -fvisibility=hidden. Only exported is truly visible to other libraries.

Checking what a library actually exports:

$ nm -D --defined-only libdemo.so
0000000000001129 T exported
000000000000113b T protected_fn

# hidden_fn and default_fn don't appear — they're not in .dynsym

Try It

# Create a library with mixed visibility
# vis.c has pub_fn (default) and priv_fn (hidden)

# Build with hidden default
gcc -fvisibility=hidden -shared -o libvis.so vis.c

# See what's exported
nm -D --defined-only libvis.so
# Only pub_fn appears

# See everything
nm --defined-only libvis.so
# Both functions appear

Check Your Understanding

What does HIDDEN visibility mean for a symbol?

Apply It

Why would you use PROTECTED visibility instead of DEFAULT?

Connect

Now you understand how ELF symbols are named (symbol table), scoped (binding), and exported (visibility). The next module covers relocations — how the linker patches addresses when combining object files.

← Swipe to navigate →