Chapter 41 covers the fundamentals of shared libraries on Linux. Two types of object libraries exist on Linux:
Archive of compiled .o files. The linker copies needed code directly into the executable. Result: self-contained binary, no run-time dependency, but larger binary size.
Single copy loaded into memory at run time, shared among all programs that need it. Smaller executables, easy updates, but requires the .so to exist on the system.
The chapter explains how to build, install, version, and use shared libraries, and covers important advanced topics like the dynamic linker’s search algorithm and symbol resolution semantics.
§41.11 — Finding Shared Libraries at Run Time
- The dynamic linker (
ld-linux.so) finds and loads shared libraries when a program starts. - If a dependency string contains a slash → it’s used as a direct path (no searching).
- Otherwise: search order is DT_RPATH → LD_LIBRARY_PATH → DT_RUNPATH → /etc/ld.so.cache → /lib → /usr/lib.
LD_LIBRARY_PATHis ignored for SUID/SGID programs (security).$ORIGINin rpath expands to the directory of the executable at run time — enables portable “turn-key” applications.ldconfigrebuilds/etc/ld.so.cache— must be run after installing a new library.
§41.12 — Run-Time Symbol Resolution
- Global symbols in the main executable override same-named symbols in shared libraries (symbol interposition).
- Between multiple libraries, left-to-right order on the link command line determines priority.
- These semantics mirror static library behavior (for historical compatibility).
-Bsymbolicmakes a library bind its own internal symbol references to its own definitions.LD_PRELOADenables intentional interposition for testing and debugging.- Weak symbols (
__attribute__((weak))) allow overridable default implementations.
§41.13 — Using Static Libraries
- When both .so and .a exist, the linker prefers .so by default.
- Three ways to force static: explicit .a path |
-staticflag |-Wl,-Bstatic/-Bdynamictoggles. -Wl,-Bstatic/-Wl,-Bdynamicallows mixing static and shared libraries selectively.- Static linking is appropriate when the runtime environment lacks shared libraries or ABI stability is critical.
| Command / Flag | Purpose |
|---|---|
| gcc -fPIC -shared -o libfoo.so foo.c | Build a shared library |
| ar rcs libfoo.a foo.o | Build a static library |
| gcc -o prog main.c -L. -lfoo | Link against a library (shared preferred) |
| gcc -Wl,-rpath,/path/to/libs | Embed DT_RPATH in the executable |
| gcc -Wl,–enable-new-dtags,-rpath,/path | Embed DT_RUNPATH instead of DT_RPATH |
| -Wl,-rpath,’$ORIGIN/lib’ | Portable rpath relative to executable location |
| export LD_LIBRARY_PATH=/path | Runtime library search path (env var) |
| sudo ldconfig | Rebuild /etc/ld.so.cache after installing a library |
| ldconfig -p | grep libname | Check if library is in the cache |
| ldd ./prog | Show runtime library dependencies |
| readelf -d prog | grep -E “RPATH|RUNPATH|NEEDED” | Inspect ELF dynamic section |
| LD_DEBUG=libs ./prog | Trace library search at runtime |
| LD_PRELOAD=./mylib.so ./prog | Preload a library (override symbols) |
| gcc -shared -Wl,-Bsymbolic | Make library bind its own internal symbols |
| gcc -static | Link everything statically |
| gcc -Wl,-Bstatic -lfoo -Wl,-Bdynamic | Selectively link libfoo statically |
This end-to-end example builds a shared library, installs it, and shows all the concepts together.
/* ===== libstring.c — our shared library ===== */
#include <string.h>
#include <ctype.h>
/* Count vowels in a string */
int count_vowels(const char *s) {
int count = 0;
while (*s) {
char c = tolower((unsigned char)*s);
if (c=='a'||c=='e'||c=='i'||c=='o'||c=='u') count++;
s++;
}
return count;
}
/* Reverse a string in-place */
void reverse_str(char *s) {
int len = strlen(s);
for (int i = 0, j = len-1; i < j; i++, j--) {
char tmp = s[i]; s[i] = s[j]; s[j] = tmp;
}
}
/* Weak symbol — can be overridden by application */
__attribute__((weak)) void on_error(const char *msg) {
fprintf(stderr, "[libstring default error] %s\n", msg);
}
/* ===== app.c — the application ===== */
#include <stdio.h>
#include <string.h>
int count_vowels(const char *s);
void reverse_str(char *s);
/* Override the weak symbol from the library */
void on_error(const char *msg) {
fprintf(stderr, "[APP] Error: %s\n", msg);
}
int main(void) {
char buf[] = "Hello EmbeddedPathashala";
printf("Vowels in '%s': %d\n", buf, count_vowels(buf));
reverse_str(buf);
printf("Reversed: %s\n", buf);
return 0;
}
# ===== Build and deploy script =====
# 1. Compile shared library
gcc -fPIC -shared -o libstring.so.1 libstring.c
ln -sf libstring.so.1 libstring.so
# 2. Build app with $ORIGIN rpath (portable)
gcc -o app app.c -L. -lstring -Wl,-rpath,'$ORIGIN'
# 3. Check dependencies
ldd ./app
# libstring.so.1 => ./libstring.so.1
# 4. Run — works from current directory (rpath=$ORIGIN)
./app
# Vowels in 'Hello EmbeddedPathashala': 10
# Reversed: alahsahtaPdeddebmE olleH
# 5. System-wide installation alternative:
sudo cp libstring.so.1 /usr/local/lib/
sudo ldconfig
ldconfig -p | grep libstring
# libstring.so.1 => /usr/local/lib/libstring.so.1
# 6. Verify binary details
readelf -d app | grep -E "NEEDED|RPATH|RUNPATH"
file app
2. DT_RPATH directories (if no DT_RUNPATH in the binary)
3. LD_LIBRARY_PATH directories (skipped for SUID/SGID binaries)
4. DT_RUNPATH directories
5. /etc/ld.so.cache
6. /lib then /usr/lib
1. ldd ./app — check which .so version is loaded now
2. Check if the MAJOR version changed (ABI break) vs MINOR (should be compatible)
3. readelf -d app | grep NEEDED — see what version was expected
4. LD_DEBUG=all ./app 2>&1 — trace symbol resolution failures
Fixes:
– If ABI-incompatible upgrade: keep old .so alongside new (different soname), rebuild app against new version
– If bug in new library: revert to old .so
– Long-term: statically link the library to pin the exact version
41.11 Finding Libraries at Run Time 41.12 Symbol Resolution 41.13 Static vs Shared 41.14 Summary (this page)
