What happens when the same function name (a “global symbol”) is defined in multiple places — for example, in both the main executable and in a shared library? Which version actually gets called at run time?
This is the symbol resolution problem. The answer might surprise you: the main program wins — its definition overrides the library’s definition, even when the library is calling a function defined within itself. This behavior is called symbol interposition.
Consider a shared library that defines both xyz() and func(). func() calls xyz(). The main program also defines its own xyz(). Which xyz() does func() call?
The executable’s global symbol interposes over the library’s definition
/* foo.c — shared library source */
#include <stdio.h>
/* xyz defined inside the library */
void xyz(void) {
printf("foo-xyz\n");
}
/* func calls xyz — but which xyz? */
void func(void) {
xyz(); /* This will call main's xyz, not foo's xyz! */
}
/* prog.c — main program */
#include <stdio.h>
/* xyz also defined here — same name as the library's xyz */
void xyz(void) {
printf("main-xyz\n");
}
/* func() is declared in libfoo but called here */
void func(void);
int main(void) {
func(); /* calls libfoo's func(), which calls xyz() */
return 0;
}
# Build and run
gcc -g -c -fPIC -Wall -c foo.c
gcc -g -shared -o libfoo.so foo.o
gcc -g -o prog prog.c libfoo.so
LD_LIBRARY_PATH=. ./prog
# Output: main-xyz
# --> main program's xyz() OVERRIDES library's xyz()!
Linux shared libraries follow these rules for global symbol resolution (inherited from early UNIX static library semantics):
A global symbol defined in the main program overrides any definition with the same name in a shared library.
Even if the library calls that symbol internally, the main program’s version is used.
If a global symbol is defined in multiple shared libraries, the first library listed on the link command line wins.
Libraries are scanned left-to-right; first definition found wins.
# Rule 2 demonstration: two libraries both define greet()
# Link order: libfirst.so listed before libsecond.so
gcc -o prog main.c -L. -lfirst -lsecond
# At run time: libfirst.so's greet() is used
# (even if main.c calls greet() and greet() is also in libsecond.so)
# If you want libsecond.so's greet(), reverse the order:
gcc -o prog main.c -L. -lsecond -lfirst
The default behaviour breaks the idea that a shared library is a self-contained subsystem. A library cannot guarantee that its own internal function calls use its own definitions.
/* Problem scenario: library's internal call gets hijacked */
/* libmath.so defines both square() and cube() */
/* cube() is implemented using square() */
double square(double x) { return x * x; }
double cube(double x) {
return x * square(x); /* normally calls libmath's square() */
}
/* main.c overrides square() with a buggy version */
double square(double x) { return x + 1; } /* BUG: wrong formula */
int main(void) {
/* This calls libmath's cube(), which calls main's square()! */
/* cube(3) should be 27, but returns 3 * (3+1) = 12 */
printf("%.0f\n", cube(3)); /* prints 12, not 27! */
}
When you build a shared library with the -Bsymbolic linker option, you tell the linker: “when a global symbol is referenced from within this library, prefer the definition inside this library over any same-name definition outside.” This gives the library self-contained behavior.
# Build libfoo.so with -Bsymbolic
gcc -g -c -fPIC -Wall -c foo.c
gcc -g -shared -Wl,-Bsymbolic -o libfoo.so foo.o
gcc -g -o prog prog.c libfoo.so
LD_LIBRARY_PATH=. ./prog
# Output: foo-xyz
# Now func() in libfoo calls libfoo's own xyz(), not main's xyz()
# NOTE: calling xyz() DIRECTLY from main() still calls main's xyz()
# -Bsymbolic only affects calls WITHIN the library
/* Demonstrating -Bsymbolic effect */
/* libmath.so built WITH -Bsymbolic */
double square(double x) { return x * x; }
double cube(double x) {
/* With -Bsymbolic: this calls libmath's own square() */
/* main's override of square() does NOT affect this */
return x * square(x);
}
/* Result: cube(3) correctly returns 27, even if main overrides square() */
-Wl,-Bsymbolic. This prevents accidental symbol interposition from breaking your library’s internal logic.Symbol interposition is not always accidental. LD_PRELOAD lets you intentionally inject a library that overrides functions from any other library — without modifying either the application or its libraries. This is used for debugging, profiling, and testing.
/* intercept_malloc.c — intercept malloc() calls */
#include <stdio.h>
#include <stdlib.h>
/* Override malloc with our version */
void *malloc(size_t size) {
fprintf(stderr, "[intercept] malloc(%zu) called\n", size);
/* Call the real malloc using dlsym(RTLD_NEXT) */
void *(*real_malloc)(size_t) = dlsym(RTLD_NEXT, "malloc");
return real_malloc(size);
}
# Build the interception library
gcc -fPIC -shared -o intercept_malloc.so intercept_malloc.c -ldl
# Run any program with malloc intercepted
LD_PRELOAD=./intercept_malloc.so ls
# Output: [intercept] malloc(768) called
# [intercept] malloc(120) called
# ... (every malloc call logged)
# Another use: replace rand() with a deterministic version for testing
/* fake_rand.c */
#include <stdlib.h>
int rand(void) { return 42; } /* always returns 42 */
gcc -fPIC -shared -o fake_rand.so fake_rand.c
LD_PRELOAD=./fake_rand.so ./my_game /* game always gets 42 from rand() */
A weak symbol is a symbol that can be overridden by a strong (regular) definition elsewhere. The library provides a default implementation as a weak symbol, allowing users to replace it simply by defining their own version.
/* library.c — defines a weak default implementation */
#include <stdio.h>
/* Weak definition — can be overridden by the application */
__attribute__((weak)) void error_handler(const char *msg) {
fprintf(stderr, "Default error: %s\n", msg);
}
void do_work(void) {
/* calls error_handler — uses application's version if defined */
error_handler("something went wrong");
}
/* app.c — override the weak symbol with a strong definition */
#include <stdio.h>
/* Strong definition overrides the weak one in the library */
void error_handler(const char *msg) {
fprintf(stderr, "[APP ERROR LOG] %s\n", msg);
/* maybe write to log file, send to syslog, etc. */
}
void do_work(void); /* from library */
int main(void) {
do_work(); /* calls our custom error_handler */
return 0;
}
/* Output: [APP ERROR LOG] something went wrong */
gcc -shared -Wl,-Bsymbolic.static — they won’t be in the global symbol table at all.2. Use
__attribute__((visibility("hidden"))) to hide symbols from external linking.3. Build the library with
-Wl,-Bsymbolic to bind internal references to internal definitions.4. Use a linker version script to control which symbols are exported vs. kept internal.
← 41.11 Finding Libraries Next → 41.13 Static vs Shared Libraries
