The Beauty of Shared Library Upgrades
One of the most powerful features of shared libraries is the ability to install a new version while existing programs continue to run using the old version. This is a fundamental advantage over static libraries.
When a program is already running, its process holds an open file reference to the old .so file. Replacing the file on disk does not affect the running process. The new version is only picked up by programs launched after the upgrade. This gives you zero-downtime library upgrades — no need to stop long-running servers or daemons.
| Time | Running Program (OLD) | New Program Launched (NEW) |
|---|---|---|
| Before upgrade | Uses libdemo.so.1.0.1 (loaded into its address space) |
— |
| During upgrade | Still using 1.0.1 — inode still open. Unaffected. |
— |
| After upgrade | Still using 1.0.1 — no change |
Loads libdemo.so.1 → libdemo.so.1.0.2 — gets new version! |
1. Minor Version Upgrade — Step by Step
A minor version upgrade is a compatible change — bug fixes, performance improvements, or new functions. Programs using the old version continue to work with the new version without recompiling.
libdemo.so.1)libdemo.so.1.0.2 (bumped patch/minor)/usr/lib/ or /usr/local/lib/Code Example 1 — Full Minor Version Upgrade
/* ===== BEFORE UPGRADE: libdemo.so.1.0.1 ===== */
/* mod1.c — version 1.0.1 */
#include <stdio.h>
#include <string.h>
int add_numbers(int a, int b) {
return a + b;
}
/* Bug: this had an off-by-one in certain conditions */
int count_chars(const char *s, char c) {
int count = 0;
while (*s) {
if (*s == c) count++;
s++;
}
return count; /* was: count - 1; (the bug) */
}
void hello(void) {
printf("libdemo v1.0.1\n");
}
/* ===== AFTER UPGRADE: libdemo.so.1.0.2 ===== */
/* mod1.c — version 1.0.2: bug fixed in count_chars */
#include <stdio.h>
#include <string.h>
int add_numbers(int a, int b) {
return a + b; /* unchanged */
}
/* Bug fixed! Now returns correct count */
int count_chars(const char *s, char c) {
int count = 0;
while (*s) {
if (*s == c) count++;
s++;
}
return count; /* fixed */
}
/* New function added — compatible */
int subtract_numbers(int a, int b) {
return a - b;
}
void hello(void) {
printf("libdemo v1.0.2 (bug fixed!)\n");
}
# === BUILD THE NEW VERSION ===
gcc -g -c -fPIC -Wall mod1.c mod2.c
# soname stays libdemo.so.1 — same major version, compatible change
gcc -g -shared -Wl,-soname,libdemo.so.1 \
-o libdemo.so.1.0.2 \
mod1.o mod2.o
# === INSTALL ===
sudo cp libdemo.so.1.0.2 /usr/local/lib/
# Check before ldconfig
ls -l /usr/local/lib/libdemo*
# lrwxrwxrwx libdemo.so.1 -> libdemo.so.1.0.1 ← still pointing to old
# -rwxr-xr-x libdemo.so.1.0.1
# -rwxr-xr-x libdemo.so.1.0.2
# === RUN LDCONFIG ===
sudo ldconfig -v | grep libdemo
# libdemo.so.1 -> libdemo.so.1.0.2 (changed) ← NOW points to new!
# Check after ldconfig
ls -l /usr/local/lib/libdemo*
# lrwxrwxrwx libdemo.so.1 -> libdemo.so.1.0.2 ← updated!
# -rwxr-xr-x libdemo.so.1.0.1 ← old file still exists
# -rwxr-xr-x libdemo.so.1.0.2 ← new file
# === WHAT HAPPENS TO RUNNING PROGRAMS ===
# Programs already running have their own mmap of libdemo.so.1.0.1
# That file (inode) stays valid until all processes close it
# Running programs: still use 1.0.1 (unaffected by symlink change)
# New programs: load libdemo.so.1 -> libdemo.so.1.0.2 -> get the fix!
sudo rm /usr/local/lib/libdemo.so.1.0.1. Already-running programs still hold a reference to the inode, so they keep working until they exit. Once all processes using the old file have exited, the OS frees the inode.2. Major Version Upgrade — Step by Step
A major version upgrade introduces incompatible changes. Old programs must NOT use the new version. Both versions must coexist on the system simultaneously.
libdemo.so.2 instead of libdemo.so.1libdemo.so.2.0.0libdemo.so.1.x.y filelibdemo.so.2 → libdemo.so.2.0.0 while keeping libdemo.so.1 intactln -sf libdemo.so.2 libdemo.so so new compilations use v2libdemo.so.1, which still exists| File in /usr/lib/ | Type | Used By |
|---|---|---|
libdemo.so.1.0.2 |
Real file (v1 latest) | Old programs at runtime |
libdemo.so.1 → libdemo.so.1.0.2 |
Soname symlink | Dynamic linker resolves old programs |
libdemo.so.2.0.0 |
Real file (v2) | New programs at runtime |
libdemo.so.2 → libdemo.so.2.0.0 |
Soname symlink | Dynamic linker resolves new programs |
libdemo.so → libdemo.so.2 |
Linker name symlink | gcc -ldemo at compile time (new code gets v2) |
Code Example 2 — Full Major Version Upgrade
# === CURRENT STATE: libdemo v1 is installed ===
ls -l /usr/lib/libdemo*
# lrwxrwxrwx libdemo.so -> libdemo.so.1
# lrwxrwxrwx libdemo.so.1 -> libdemo.so.1.0.2
# -rwxr-xr-x libdemo.so.1.0.2
# === BUILD v2 (INCOMPATIBLE — new soname) ===
# Assume mod1_v2.c and mod2_v2.c have breaking changes
gcc -g -c -fPIC -Wall mod1_v2.c mod2_v2.c
# NOTE: soname is libdemo.so.2 ← DIFFERENT from before
gcc -g -shared -Wl,-soname,libdemo.so.2 \
-o libdemo.so.2.0.0 \
mod1_v2.o mod2_v2.o
# Verify the embedded soname
readelf -d libdemo.so.2.0.0 | grep SONAME
# (SONAME) Library soname: [libdemo.so.2]
# === INSTALL (alongside v1 — do NOT remove v1!) ===
sudo cp libdemo.so.2.0.0 /usr/lib/
# === RUN LDCONFIG ===
sudo ldconfig -v | grep libdemo
# libdemo.so.1 -> libdemo.so.1.0.2 ← v1 unchanged
# libdemo.so.2 -> libdemo.so.2.0.0 ← v2 newly created
# Verify all files
ls -l /usr/lib/libdemo*
# lrwxrwxrwx libdemo.so -> libdemo.so.1 ← still v1 linker name
# lrwxrwxrwx libdemo.so.1 -> libdemo.so.1.0.2
# -rwxr-xr-x libdemo.so.1.0.2
# lrwxrwxrwx libdemo.so.2 -> libdemo.so.2.0.0 ← new soname created!
# -rwxr-xr-x libdemo.so.2.0.0
# === UPDATE LINKER NAME TO POINT TO v2 ===
# (so that new compilations use v2)
sudo ln -sf libdemo.so.2 /usr/lib/libdemo.so
ls -l /usr/lib/libdemo.so
# lrwxrwxrwx libdemo.so -> libdemo.so.2 ← now points to v2!
# === VERIFY OLD AND NEW PROGRAMS ===
# Old program (compiled against v1):
ldd old_program | grep libdemo
# libdemo.so.1 => /usr/lib/libdemo.so.1.0.2
# It still works! libdemo.so.1 exists and points to v1 code.
# Compile new program (uses v2 API):
gcc -o new_program new_main.c -ldemo -L/usr/lib
# -ldemo finds /usr/lib/libdemo.so -> libdemo.so.2
ldd new_program | grep libdemo
# libdemo.so.2 => /usr/lib/libdemo.so.2.0.0
# New program uses v2!
./new_program
Code Example 3 — Installing a New 2.x Minor Version
Once you have v2 installed, later minor releases (2.0.1, 2.1.0) follow the same minor upgrade process as before — ldconfig keeps the linker name up to date automatically.
# Current state: libdemo.so.2.0.0 is installed
ls -l /usr/lib/libdemo*
# lrwxrwxrwx libdemo.so -> libdemo.so.2
# lrwxrwxrwx libdemo.so.2 -> libdemo.so.2.0.0
# -rwxr-xr-x libdemo.so.2.0.0
# === BUILD 2.0.1 — compatible fix within v2 ===
gcc -g -c -fPIC -Wall mod1_v2.c mod2_v2.c
# soname is still libdemo.so.2 — still v2 major
gcc -g -shared -Wl,-soname,libdemo.so.2 \
-o libdemo.so.2.0.1 \
mod1_v2.o mod2_v2.o
# Install
sudo mv libdemo.so.2.0.1 /usr/lib/
# Run ldconfig
sudo ldconfig -v | grep libdemo
# libdemo.so.2 -> libdemo.so.2.0.1 (changed) ← soname updated!
# Verify
ls -l /usr/lib/libdemo*
# lrwxrwxrwx libdemo.so -> libdemo.so.2
# lrwxrwxrwx libdemo.so.2 -> libdemo.so.2.0.1 ← updated by ldconfig
# -rwxr-xr-x libdemo.so.2.0.0
# -rwxr-xr-x libdemo.so.2.0.1
# Notice: libdemo.so -> libdemo.so.2 -> libdemo.so.2.0.1
# The linker name chain is automatically "up to date" because:
# libdemo.so points to the soname, and ldconfig updated the soname.
# So the linker name is also effectively updated — no manual step needed!
libdemo.so → libdemo.so.2), and ldconfig updates the soname (libdemo.so.2 → libdemo.so.2.0.1), the linker name chain is automatically up to date for free. This is another reason why the two-level symlink design (linker name → soname → real file) is elegant.4. Why Running Programs Survive — The inode Mechanism
Understanding why live upgrades work requires understanding how Linux handles file deletion and inode reference counts.
| What Happens to the Old File | |
|---|---|
| Before upgrade Filesystem: libdemo.so.1.0.1 → inode #1234
Running Process: |
After upgrade (new file installed, ldconfig run) Filesystem: libdemo.so.1 → libdemo.so.1.0.2 (symlink updated)libdemo.so.1.0.2 → inode #5678 (new file)libdemo.so.1.0.1 → inode #1234 (still on disk!)
Running Process: New Process: |
| When the old process exits, inode #1234 ref count drops to 0 and the OS frees it (even if the directory entry was deleted). The old file is not truly deleted from disk until all processes holding it close it. | |
# === Demonstration: old process survives upgrade ===
# Terminal 1: Start a long-running process using the old library
./long_running_server &
SERVER_PID=$!
echo "Server PID: $SERVER_PID"
# Check which library version it loaded
cat /proc/$SERVER_PID/maps | grep libdemo
# 7f8abc000000-7f8abc001000 r-xp ... /usr/lib/libdemo.so.1.0.1
# Terminal 2: Install new version
sudo cp libdemo.so.1.0.2 /usr/lib/
sudo ldconfig -v | grep libdemo
# libdemo.so.1 -> libdemo.so.1.0.2 (changed)
# Check if server is still running — it is!
kill -0 $SERVER_PID && echo "Server still alive!"
# Server still references the OLD inode
cat /proc/$SERVER_PID/maps | grep libdemo
# 7f8abc000000-7f8abc001000 r-xp ... /usr/lib/libdemo.so.1.0.1
# ← STILL using 1.0.1 even though symlink now points to 1.0.2
# Start a NEW server instance
./long_running_server &
NEW_PID=$!
# New server gets the new version
cat /proc/$NEW_PID/maps | grep libdemo
# ... /usr/lib/libdemo.so.1.0.2 ← gets new version!
5. Minor vs Major Upgrade — Summary
| Aspect | Minor Version Upgrade | Major Version Upgrade |
|---|---|---|
| Example | 1.0.1 → 1.0.2 | 1.x.y → 2.0.0 |
| Soname changes? | No — same soname (libdemo.so.1) | Yes — new soname (libdemo.so.2) |
| Old programs recompile? | No — they automatically get the fix | Optional — they keep using v1 |
| Keep old .so file? | Optional (safe to delete after all processes restart) | YES — must keep v1 files for old programs |
| ldconfig updates symlink? | Yes — libdemo.so.1 → new file | Yes — creates new libdemo.so.2 |
| Linker name update? | Automatic (via soname chain) | Manual: ln -sf libdemo.so.2 libdemo.so |
| Running programs affected? | No — live upgrade is transparent | No — they continue with old version |
Interview Questions & Answers
-fPIC.(2) Link with the same soname as before:
gcc -shared -Wl,-soname,libXYZ.so.N -o libXYZ.so.N.M+1 ...(3) Copy the new .so to the library directory.
(4) Run
ldconfig — it automatically updates the soname symlink.That’s it. The linker name symlink does not need manual updating because it points to the soname, which ldconfig just updated.
systemctl restart service) to benefit from a library bug fix. This is a conscious design trade-off: stability of running processes vs. getting fixes immediately.libdemo.so → libdemo.so.1), and ldconfig updates the soname symlink to point to the new real file (e.g., libdemo.so.1 → libdemo.so.1.0.2). So the full resolution chain is: libdemo.so → libdemo.so.1 → libdemo.so.1.0.2. The linker name still points to the soname, and the soname now points to the latest minor version. No manual update needed. However, for a major version upgrade, you must manually update the linker name to point to the new soname (e.g., libdemo.so → libdemo.so.2).libdemo.so.1) embedded in their ELF file as a required library. When they run, the dynamic linker looks up this soname. If the old library and its soname symlink are deleted, the old programs will fail to start with “cannot open shared object file” errors. The old files must remain until all programs using them have been recompiled against the new version and redeployed, or until the old programs are no longer needed./proc/<PID>/maps: cat /proc/1234/maps | grep libdemo. This shows memory-mapped regions and includes the full path of each mapped file. You’ll see something like /usr/lib/libdemo.so.1.0.1 — the actual real name of the file currently in use, not the symlink. This tells you exactly which version the running process has loaded.ld-linux.so.2 or ld-linux-x86-64.so.2) is itself a shared library that is invoked by the kernel when starting a dynamically-linked program. It reads the program’s ELF dynamic section to find which libraries are needed (by soname), looks them up in /etc/ld.so.cache and search paths, loads them into the process’s address space using mmap(), resolves symbol references (relocations), and then transfers control to the program’s main(). All of this happens before any application code runs.ldconfig -p | grep libdemo. The -p option prints the current contents of /etc/ld.so.cache. After a successful ldconfig run, you should see an entry like: libdemo.so.1 (libc6,x86-64) => /usr/lib/libdemo.so.1.0.2. This confirms the cache is aware of the new file and the dynamic linker will find it.gcc -ldemo → linker searches for libdemo.so → resolves to libdemo.so → libdemo.so.2 (linker name symlink). The linker records the soname libdemo.so.2 (not the full path) in the executable’s ELF dynamic section.
At run time: dynamic linker reads ELF — needs libdemo.so.2 → looks in /etc/ld.so.cache → finds /usr/lib/libdemo.so.2.0.0 → mmaps the real file into process memory. The soname symlink (libdemo.so.2 → libdemo.so.2.0.0) is just for directory browsing; the cache stores the real path directly.
Live patching: Security fixes can be deployed by replacing the .so file and restarting services — without rebuilding every binary.
Modular driver architecture: Drivers and protocol stacks (like BlueZ for BLE) expose a shared library interface. Applications link against it and get automatic improvements when the library is updated.
Reduced flash footprint: In embedded Linux systems with multiple applications, shared libraries save significant flash storage compared to statically linked equivalents.
Series Complete! 🎉
You have covered ldconfig, ABI compatibility rules, and live library upgrades — the full picture of Linux shared library management.
