When you build a shared library (.so file), every function you write in C is global by default. This means the entire world — any program that links against your library — can see and use all your functions, even the ones you intended as internal helpers.
This is a real problem. Imagine you write a helper function _internal_parse() that is just for your library’s use. If some application developer discovers it and starts using it, you can never change or remove it without breaking their code.
Controlling symbol visibility solves this by letting you declare exactly which functions are public API and which are internal implementation details.
The simplest way to hide a function is to declare it static inside the C source file. A static function is only visible within that single .c file. No other file — not even another .c file in the same library — can call it directly.
INVISIBLE ❌
VISIBLE ✅
Bonus effect of static: When a symbol is static, all calls to it within the same source file are bound at compile time. This means they cannot be interposed at runtime by a symbol with the same name in another library — a useful performance and correctness benefit.
The static keyword limits a function to a single file. But what if you need a function to be shared across multiple .c files within your library, but still hidden from the outside world?
GCC’s visibility attribute handles this case. There are two main values:
| Attribute | Visibility to other .c files in the library | Visibility to external programs |
|---|---|---|
visibility("default") |
Yes | Yes — exported publicly |
visibility("hidden") |
Yes | No — hidden from outside |
hidden as “internal linkage across the whole library, but external linkage stops at the library boundary.”static, the hidden attribute also prevents symbol interposition. References inside the library are resolved at link time and cannot be overridden by an identically-named symbol in another library./* mylib.c — part of mylib.so */
#include <stdio.h>
#include <string.h>
/* PRIVATE: only used internally, not part of the public API */
static int validate_input(const char *s) {
if (s == NULL || strlen(s) == 0) return 0;
return 1;
}
/* PUBLIC: this is the API that users of the library call */
void mylib_process(const char *input) {
if (!validate_input(input)) {
printf("Invalid input!\n");
return;
}
printf("Processing: %s\n", input);
}
# Build the shared library
gcc -g -fPIC -c -Wall mylib.c
gcc -g -shared -o mylib.so mylib.o
# Check exported symbols — validate_input should NOT appear
readelf --dyn-syms mylib.so | grep -E "validate|process"
# Expected: only mylib_process is GLOBAL, validate_input is LOCAL
/* internal.h — shared header for library internals */
#ifndef INTERNAL_H
#define INTERNAL_H
/* This function is available to all .c files in the library
but is NOT exported to external users */
__attribute__((visibility("hidden")))
void internal_log(const char *msg);
#endif
/* internal.c — defines the hidden helper */
#include <stdio.h>
#include "internal.h"
__attribute__((visibility("hidden")))
void internal_log(const char *msg) {
printf("[LIB INTERNAL] %s\n", msg);
}
/* api.c — defines the public API, uses the hidden helper */
#include <stdio.h>
#include "internal.h"
/* Mark as default visibility = publicly exported */
__attribute__((visibility("default")))
void api_do_work(const char *data) {
internal_log("api_do_work called"); /* can call internal helper */
printf("Working on: %s\n", data);
}
__attribute__((visibility("default")))
void api_cleanup(void) {
internal_log("api_cleanup called");
printf("Cleanup done.\n");
}
# Build the library from two source files
gcc -g -fPIC -c -Wall internal.c api.c
gcc -g -shared -o libwork.so internal.o api.o
# Check symbol visibility
readelf --dyn-syms libwork.so
# api_do_work → GLOBAL DEFAULT (exported ✅)
# api_cleanup → GLOBAL DEFAULT (exported ✅)
# internal_log → LOCAL (hidden ❌ from outside)
A very clean approach is to compile with -fvisibility=hidden to make ALL symbols hidden by default, then explicitly mark only the public API with visibility("default"). This way you don’t have to tag every internal function — they’re hidden automatically.
/* libmath.h — public API header */
#ifndef LIBMATH_H
#define LIBMATH_H
/* Only these functions are publicly exported */
#define PUBLIC_API __attribute__((visibility("default")))
PUBLIC_API int lib_add(int a, int b);
PUBLIC_API int lib_multiply(int a, int b);
#endif
/* libmath.c */
#include <stdio.h>
#include "libmath.h"
/* Internal helper — NOT tagged, so hidden by -fvisibility=hidden */
static int clamp(int val, int min, int max) {
if (val < min) return min;
if (val > max) return max;
return val;
}
/* These are tagged PUBLIC_API = visible outside */
PUBLIC_API int lib_add(int a, int b) {
return a + b;
}
PUBLIC_API int lib_multiply(int a, int b) {
/* internal call to clamp — fine within the library */
a = clamp(a, -1000, 1000);
b = clamp(b, -1000, 1000);
return a * b;
}
# -fvisibility=hidden makes all symbols hidden by default
gcc -g -fPIC -c -Wall -fvisibility=hidden libmath.c
gcc -g -shared -o libmath.so libmath.o
readelf --dyn-syms libmath.so
# lib_add → GLOBAL DEFAULT (exported, tagged with PUBLIC_API)
# lib_multiply → GLOBAL DEFAULT (exported, tagged with PUBLIC_API)
# clamp → not in dynamic table (static = truly private)
-fvisibility=hidden and use a PUBLIC_API macro to explicitly mark your ABI. This is how major projects like Qt, GStreamer, and OpenSSL manage their exported symbols.| Technique | Scope | Cross-file in Library? | Prevents Interposition? | Use Case |
|---|---|---|---|---|
static |
Single .c file | No | Yes | File-private helpers |
visibility("hidden") |
Whole library | Yes | Yes | Shared internals, multi-file private APIs |
visibility("default") |
Exported globally | Yes | No (by design) | Public API / ABI |
| Version scripts | Whole library | Yes | Yes | Fine-grained control, wildcard patterns |
static limits a symbol to a single .c source file — no other file, even in the same library, can use it. visibility("hidden") makes a symbol available across all .c files within the shared library but prevents it from being exported outside the library. Use static for truly internal file-only helpers. Use hidden when multiple library files need to share an internal function without exposing it publicly.-fvisibility=hidden GCC compiler flag changes the default visibility of all symbols in that compilation unit from “default” (exported) to “hidden”. This means that every function and variable is hidden unless you explicitly mark it with __attribute__((visibility("default"))). It is a best practice because it enforces a “hide by default, export intentionally” approach, preventing accidental symbol leakage and making the public API explicit and auditable.hidden or static, all references to it within the library are resolved at link time to the local definition and are baked into the library. The dynamic linker never sees these references at runtime, so another library cannot interpose them.readelf --dyn-syms libname.so and examine the output. Public symbols will show GLOBAL DEFAULT in the Bind and Vis columns. Hidden symbols will either not appear in the dynamic symbol table at all, or show LOCAL as their binding. Static symbols will not appear in the dynamic table. You can also use nm -D libname.so where uppercase letters (T, D, B) indicate exported symbols and lowercase indicate local/hidden ones.← Prev: Accessing Symbols in Main Next: Linker Version Scripts →
