Shared libraries use a three-number versioning scheme: major.minor.patch. For example, libdemo.so.1.0.2 has major=1, minor=0, patch=2.
A major version increment (e.g., 1.x โ 2.0.0) signals ABI-breaking changes โ the new library is NOT backward compatible with programs compiled against the old one. Both versions can coexist on the same system because they have different sonames (libdemo.so.1 vs libdemo.so.2).
Every shared library on a Linux system has three name levels. Understanding them is critical before you can do version upgrades.
| Name Type | Example | Who Creates It | Purpose |
|---|---|---|---|
| Real name | libdemo.so.2.0.0 |
Developer (gcc) | Actual .so file on disk |
| soname | libdemo.so.2 โ libdemo.so.2.0.0 |
ldconfig (auto) | Dynamic linker uses this at runtime |
| Linker name | libdemo.so โ libdemo.so.2 |
Developer (manual) | Used during gcc -ldemo at compile time |
(linker name)
(soname)
(real file)
Here is the complete sequence of commands to release a new major version of a shared library alongside the existing one.
Step 1: Compile the source files with -fPIC (Position Independent Code)
# Compile all modules for the new major version
gcc -g -c -fPIC -Wall mod1.c mod2.c mod3.c
Step 2: Link the shared library with the new soname
# Create the new shared library with soname = libdemo.so.2
gcc -g -shared -Wl,-soname,libdemo.so.2 \
-o libdemo.so.2.0.0 \
mod1.o mod2.o mod3.o
-Wl,-soname,libdemo.so.2 flag embeds the soname inside the .so file. The dynamic linker reads this soname at runtime, not the filename.Step 3: Install the new library into /usr/lib
mv libdemo.so.2.0.0 /usr/lib
Step 4: Run ldconfig to auto-create soname symlink
# ldconfig scans /usr/lib and creates soname symlinks automatically
ldconfig -v | grep libdemo
# Output:
# libdemo.so.1 -> libdemo.so.1.0.2
# libdemo.so.2 -> libdemo.so.2.0.0 (changed)
libdemo.so.2 โ libdemo.so.2.0.0. You do NOT need to create this manually.Step 5: Manually update the linker name symlink
cd /usr/lib
# Point the linker name to the NEW major version soname
ln -sf libdemo.so.2 libdemo.so
gcc -ldemo will still link against version 1.A shell script that automates building and installing both old and new major versions side by side.
#!/bin/bash
# demo_version_upgrade.sh
# Demonstrates creating v1 and v2 of a shared library side by side
set -e
mkdir -p /tmp/libdemo_demo
cd /tmp/libdemo_demo
# ---- Create source files ----
cat > mod1.c << 'EOF'
#include <stdio.h>
// v2 has a NEW function signature (ABI break)
void mod1_hello(const char *name, int times) {
for (int i = 0; i < times; i++)
printf("[mod1 v2] Hello, %s!\n", name);
}
EOF
cat > mod2.c << 'EOF'
#include <stdio.h>
void mod2_info(void) {
printf("[mod2 v2] Library version 2.0.0\n");
}
EOF
cat > main.c << 'EOF'
#include <stdio.h>
void mod1_hello(const char *name, int times);
void mod2_info(void);
int main(void) {
mod1_hello("Linux", 3);
mod2_info();
return 0;
}
EOF
echo "=== Compiling object files ==="
gcc -g -c -fPIC -Wall mod1.c mod2.c
echo "=== Building libdemo.so.2.0.0 with soname libdemo.so.2 ==="
gcc -g -shared -Wl,-soname,libdemo.so.2 \
-o libdemo.so.2.0.0 mod1.o mod2.o
echo "=== Checking the embedded soname ==="
readelf -d libdemo.so.2.0.0 | grep SONAME
# Expected: (SONAME) Library soname: [libdemo.so.2]
echo "=== Creating soname symlink manually ==="
ln -sf libdemo.so.2.0.0 libdemo.so.2
echo "=== Creating linker name symlink ==="
ln -sf libdemo.so.2 libdemo.so
echo "=== Building the program ==="
gcc -g -Wall -o prog main.c -L. -ldemo -Wl,-rpath,.
echo "=== Running the program ==="
./prog
echo "=== ldd output ==="
ldd prog
echo "Done!"
bash demo_version_upgrade.sh โ it compiles, links, and runs everything in /tmp/libdemo_demo.Use readelf and objdump to inspect what version information is actually baked into a shared library.
#!/bin/bash
# inspect_soname.sh โ tools to verify soname embedding
LIB="libdemo.so.2.0.0"
echo "=== Method 1: readelf -d ==="
readelf -d $LIB | grep -E 'SONAME|NEEDED'
# SONAME tells the dynamic linker which symlink to look for
echo ""
echo "=== Method 2: objdump -p ==="
objdump -p $LIB | grep SONAME
echo ""
echo "=== Method 3: objdump -p on executable ==="
# After linking prog against this library:
objdump -p prog | grep NEEDED
# You should see: NEEDED libdemo.so.2
# This means prog will look for the soname symlink, not the real filename
echo ""
echo "=== Show all symlinks in current directory ==="
ls -la libdemo* 2>/dev/null | awk '{print $9, $10, $11}'
# Should show:
# libdemo.so -> libdemo.so.2
# libdemo.so.2 -> libdemo.so.2.0.0
# libdemo.so.2.0.0 (real file)
This example shows how a running program continues to use the old library even after a new version is installed. You need to understand how the dynamic linker resolves shared libraries at process startup.
/* long_runner.c โ simulates a long-running process */
#include <stdio.h>
#include <unistd.h>
// Imagine this is in libdemo.so.1
void mod1_greet(void); // old v1 signature
int main(void) {
printf("Process PID = %d started. Using libdemo.so.1\n", getpid());
for (int i = 0; i < 5; i++) {
mod1_greet();
sleep(2);
}
printf("Process finished. Still used v1 throughout its life.\n");
return 0;
}
/*
* EXPLANATION:
* ------------
* When this process was STARTED, the dynamic linker resolved
* "libdemo.so.1" (from the NEEDED entry) to libdemo.so.1.0.2
* via the soname symlink. That resolution is LOCKED for the
* process lifetime.
*
* Even if you later:
* ldconfig (which creates libdemo.so.1 -> libdemo.so.1.2.0)
* ...this running process is NOT affected.
*
* Only NEW processes started after ldconfig picks up the update.
*
* Compile: gcc -o long_runner long_runner.c -ldemo_v1 -Wl,-rpath,.
*/
dlopen() is called). After that, the resolved file handles are kept open. Updating symlinks has no effect on already-running processes.libdemo.so.1.0.2 ย ย ย โ real file
libdemo.so.1 โ libdemo.so.1.0.2 ย โ soname
libdemo.so ย โ libdemo.so.1 ย ย ย ย โ linker name
libdemo.so.1.0.2 ย ย ย โ v1 real file (still here!)
libdemo.so.1 โ libdemo.so.1.0.2 โ v1 soname
libdemo.so.2.0.0 ย ย ย โ v2 real file (new)
libdemo.so.2 โ libdemo.so.2.0.0 โ v2 soname (new, by ldconfig)
libdemo.so ย โ libdemo.so.2 ย ย ย ย โ linker name updated manually
libdemo.so.1), so ldconfig just updates the soname symlink to point to the new real file. A major version upgrade breaks ABI compatibility (changed function signatures, removed symbols, etc.), so a new soname is introduced (e.g., libdemo.so.2). Both major versions can coexist on the system.ldconfig automatically creates and maintains the soname symlink (e.g., libdemo.so.2 โ libdemo.so.2.0.0). The linker name symlink (e.g., libdemo.so โ libdemo.so.2) must be created and updated manually by the package maintainer using ln -sf.-Wl,-soname,libname.so.MAJOR flag. The -Wl, prefix passes the following comma-separated argument to the linker. Example: gcc -shared -Wl,-soname,libdemo.so.2 -o libdemo.so.2.0.0 mod1.o mod2.o. Without this flag, the soname is not embedded and the dynamic linker uses the filename directly, which breaks versioning.libdemo.so.1.x.y and libdemo.so.2.0.0) and different soname symlinks (libdemo.so.1 and libdemo.so.2). When a program is compiled, its NEEDED entry records the soname (e.g., libdemo.so.1). At runtime, the dynamic linker follows exactly that soname symlink. So program A using v1 and program B using v2 both work correctly on the same system.readelf -d libdemo.so.2.0.0 | grep SONAME or objdump -p libdemo.so.2.0.0 | grep SONAME. These inspect the ELF dynamic section of the library and display the embedded soname string.-v (verbose) flag makes ldconfig print all the directories it scans and all the soname symlinks it creates or updates, along with a note of which ones changed. This is useful for verifying that a newly installed library was picked up and that its soname symlink was created correctly.