What is a Soname?
So far we’ve seen that when you link a program against a shared library, the real filename (e.g., libfoo.so) is embedded into the executable as a DT_NEEDED tag.
But what if you ship a library update โ say you fix a bug in libfoo.so? Every executable already linked against the old libfoo.so would still load the old file unless you replace it. And what if you make an incompatible API change? You’d break all existing programs.
The solution is the soname โ a kind of alias name embedded in the shared library itself (stored as a DT_SONAME tag in ELF parlance). When the linker detects a soname in a library, it embeds the soname (not the real filename) into executables that link against it. The dynamic linker then looks for the soname at runtime. A symbolic link from the soname to the actual versioned library file completes the chain.
This scheme lets you update or replace the library file without recompiling every program that uses it โ as long as the soname stays the same.
Key Terms
A shared library on Linux typically has three different names, each serving a different purpose in the build and deployment process:
| Name Type | Example | Purpose | Used By |
|---|---|---|---|
| Real Name | libfoo.so.1.2.3 | Actual library file on disk with full version info | Package manager, installer |
| Soname | libfoo.so.1 | Major-version alias embedded in library and executables | Dynamic linker at runtime |
| Linker Name | libfoo.so | Version-independent name used when compiling programs | Compiler/linker (-lfoo) |
How the three names relate (symlink chain):
| What you see in /usr/lib | Type | Points to |
|---|---|---|
| libfoo.so.1.2.3 | Real file (actual .so) | โ (the actual library binary) |
| libfoo.so.1 | Symbolic link (soname) | โ libfoo.so.1.2.3 |
| libfoo.so | Symbolic link (linker name) | โ libfoo.so.1 (or directly to libfoo.so.1.2.3) |
The soname is specified at library creation time using the -Wl,-soname,<name> linker flag.
Step 1: Compile source files to position-independent object files
/* mod1.c */
#include <stdio.h>
void mod1_func(void) {
printf("Called mod1-x1\n");
}
/* mod2.c */
#include <stdio.h>
void mod2_func(void) {
printf("Called mod2-x2\n");
}
/* mod3.c */
#include <stdio.h>
void mod3_func(void) {
printf("Called mod3-x3\n");
}
# Compile all modules with -fPIC (Position Independent Code)
$ gcc -g -c -fPIC -Wall mod1.c mod2.c mod3.c
# Verify object files are created
$ ls -lh *.o
-rw-r--r-- 1 ravi ravi 1.8K mod1.o
-rw-r--r-- 1 ravi ravi 1.8K mod2.o
-rw-r--r-- 1 ravi ravi 1.8K mod3.o
Step 2: Create the shared library WITH a soname
# -shared = create a shared library
# -Wl,-soname,... = pass -soname option to the linker (ld)
# -o libfoo.so = real name of the output file
# The soname we embed is "libbar.so" (a different name for demonstration)
$ gcc -g -shared -Wl,-soname,libbar.so -o libfoo.so mod1.o mod2.o mod3.o
# In real projects, you'd use versioned names like:
# Real name: libfoo.so.1.2.3
# Soname: libfoo.so.1
$ gcc -g -shared -Wl,-soname,libfoo.so.1 -o libfoo.so.1.2.3 mod1.o mod2.o mod3.o
Step 3: Inspect the soname embedded in the library
# Method 1: objdump
$ objdump -p libfoo.so | grep SONAME
SONAME libbar.so
# Method 2: readelf (more detailed)
$ readelf -d libfoo.so | grep SONAME
0x000000000000000e (SONAME) Library soname: [libbar.so]
# For the versioned library:
$ readelf -d libfoo.so.1.2.3 | grep SONAME
0x000000000000000e (SONAME) Library soname: [libfoo.so.1]
# Full dynamic section dump (see all ELF dynamic tags):
$ readelf -d libfoo.so
Dynamic section at offset 0x2de8 contains 23 entries:
Tag Type Name/Value
0x000000000000000e (SONAME) Library soname: [libbar.so]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
...
When the linker sees that a library has a soname, it embeds the soname (not the real filename) into the executable’s DT_NEEDED entry.
/* prog.c */
void mod1_func(void);
void mod2_func(void);
int main(void) {
mod1_func();
mod2_func();
return 0;
}
# Link the program against libfoo.so (which has soname libbar.so)
$ gcc -g -Wall -o prog prog.c libfoo.so
# Check what DT_NEEDED tag was embedded:
$ readelf -d prog | grep NEEDED
0x0000000000000001 (NEEDED) Shared library: [libbar.so] โ soname! Not libfoo.so
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
# The linker automatically detected the soname and used it instead of "libfoo.so"
# This is why the dynamic linker will look for "libbar.so" at runtime
Soname embedding at link time vs runtime:
| Phase | Action | Result |
|---|---|---|
| Link time | Linker reads soname from libfoo.so | Embeds “libbar.so” as DT_NEEDED in prog |
| Runtime | Dynamic linker reads DT_NEEDED from prog | Searches for a file named “libbar.so” |
| Runtime | Finds “libbar.so” symlink โ libfoo.so | Loads libfoo.so into memory |
# Try to run the program โ it fails!
$ LD_LIBRARY_PATH=. ./prog
./prog: error in loading shared libraries: libbar.so: cannot open
shared object file: No such file or directory
# The dynamic linker is searching for "libbar.so" (the soname)
# but only "libfoo.so" (the real name) exists in the current directory
$ ls *.so
libfoo.so โ exists
libbar.so โ does NOT exist (yet)
Fix: Create the soname symbolic link
# Create symbolic link: libbar.so โ libfoo.so
$ ln -s libfoo.so libbar.so
# Verify the symlink was created:
$ ls -la libbar.so
lrwxrwxrwx 1 ravi ravi 9 Jun 5 10:00 libbar.so -> libfoo.so
# Now run the program:
$ LD_LIBRARY_PATH=. ./prog
Called mod1-x1
Called mod2-x2
Here is the complete, real-world workflow used to create, deploy, and update a versioned shared library.
Create the versioned library (version 1.2.3, major version 1):
# Real name: libmylib.so.1.2.3
# Soname: libmylib.so.1 (major version only)
$ gcc -g -shared -fPIC -Wall \
-Wl,-soname,libmylib.so.1 \
-o libmylib.so.1.2.3 \
mod1.o mod2.o mod3.o
# Verify:
$ readelf -d libmylib.so.1.2.3 | grep SONAME
0x000000000000000e (SONAME) Library soname: [libmylib.so.1]
Create the required symbolic links:
# Soname symlink (used by dynamic linker at runtime)
$ ln -s libmylib.so.1.2.3 libmylib.so.1
# Linker name symlink (used by gcc -lmylib at compile time)
$ ln -s libmylib.so.1 libmylib.so
# See what we have:
$ ls -la libmylib*
-rwxr-xr-x 1 ravi ravi 12345 libmylib.so.1.2.3 โ REAL FILE
lrwxrwxrwx 1 ravi ravi 17 libmylib.so.1 -> libmylib.so.1.2.3
lrwxrwxrwx 1 ravi ravi 13 libmylib.so -> libmylib.so.1
Compile a program using the linker name:
# -lmylib causes the linker to search for libmylib.so
# libmylib.so โ libmylib.so.1 (symlink) โ has soname "libmylib.so.1"
# So the executable gets DT_NEEDED = libmylib.so.1
$ gcc -g -Wall -o prog prog.c -L. -lmylib
$ readelf -d prog | grep NEEDED
0x0000000000000001 (NEEDED) Shared library: [libmylib.so.1] โ soname embedded
Install system-wide and run ldconfig to auto-create soname links:
# Copy the real library to /usr/local/lib
$ sudo cp libmylib.so.1.2.3 /usr/local/lib/
# ldconfig automatically creates the soname symlink
# You still need to create the linker name link manually
$ sudo ldconfig
$ ls -la /usr/local/lib/libmylib*
-rwxr-xr-x 1 root root 12345 libmylib.so.1.2.3
lrwxrwxrwx 1 root root 17 libmylib.so.1 -> libmylib.so.1.2.3
$ sudo ln -s /usr/local/lib/libmylib.so.1 /usr/local/lib/libmylib.so
Now upgrade the library (bug fix: 1.2.3 โ 1.2.4), same soname:
# Build the new version
$ gcc -g -shared -fPIC -Wall \
-Wl,-soname,libmylib.so.1 \ โ SAME soname (compatible update)
-o libmylib.so.1.2.4 \
mod1_fixed.o mod2_fixed.o mod3.o
# Install the new real file
$ sudo cp libmylib.so.1.2.4 /usr/local/lib/
# Move the soname symlink to point to the new version
$ sudo ln -sf libmylib.so.1.2.4 /usr/local/lib/libmylib.so.1
$ sudo ldconfig
# Existing programs that have DT_NEEDED = libmylib.so.1 now
# automatically use the NEW library โ NO RECOMPILE NEEDED!
$ ls -la /usr/local/lib/libmylib*
-rwxr-xr-x 1 root root 12345 libmylib.so.1.2.3 โ old (can be deleted)
-rwxr-xr-x 1 root root 12678 libmylib.so.1.2.4 โ new
lrwxrwxrwx 1 root root 17 libmylib.so.1 -> libmylib.so.1.2.4
lrwxrwxrwx 1 root root 13 libmylib.so -> libmylib.so.1
When you make an incompatible change to a library (change a function signature, remove a function, change struct layout), you must bump the major version in the soname. Old programs continue to use the old soname; new programs use the new one. Both versions can coexist on the same system.
# --- OLD library (version 1.x.x) ---
$ gcc -g -shared -fPIC -Wall \
-Wl,-soname,libmylib.so.1 \
-o libmylib.so.1.2.3 \
mod1_v1.o mod2_v1.o
# --- NEW library (version 2.x.x, ABI break) ---
$ gcc -g -shared -fPIC -Wall \
-Wl,-soname,libmylib.so.2 \ โ NEW soname with major version 2
-o libmylib.so.2.0.0 \
mod1_v2.o mod2_v2.o
# Install BOTH versions on the system:
$ sudo cp libmylib.so.1.2.3 /usr/local/lib/
$ sudo cp libmylib.so.2.0.0 /usr/local/lib/
$ sudo ldconfig
# Two soname symlinks coexist:
$ ls -la /usr/local/lib/libmylib*
-rwxr-xr-x 1 root root 12345 libmylib.so.1.2.3
-rwxr-xr-x 1 root root 14567 libmylib.so.2.0.0
lrwxrwxrwx 1 root root 17 libmylib.so.1 -> libmylib.so.1.2.3
lrwxrwxrwx 1 root root 17 libmylib.so.2 -> libmylib.so.2.0.0
lrwxrwxrwx 1 root root 13 libmylib.so -> libmylib.so.2 โ default for new builds
# Old program: still works (DT_NEEDED = libmylib.so.1 โ libmylib.so.1.2.3)
$ ldd old_prog | grep mylib
libmylib.so.1 => /usr/local/lib/libmylib.so.1 (0x00007f...)
# New program compiled today: uses version 2
$ ldd new_prog | grep mylib
libmylib.so.2 => /usr/local/lib/libmylib.so.2 (0x00007f...)
Version compatibility summary:
| Change Type | Example | Action Required | Recompile Programs? |
|---|---|---|---|
| Bug fix (no API change) | 1.2.3 โ 1.2.4 | New real file, move soname symlink | No โ |
| New function added | 1.2.3 โ 1.3.0 | New real file, move soname symlink | No โ (backward compatible) |
| ABI break (removed/changed function) | 1.2.3 โ 2.0.0 | New real file, NEW soname (libmylib.so.2) | Yes โ programs using v2 API must recompile โ ๏ธ |
Here is the exact sequence from the textbook, fully annotated:
# โโ Step 1: Compile sources โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
$ gcc -g -c -fPIC -Wall mod1.c mod2.c mod3.c
# โโ Step 2: Create libfoo.so with soname "libbar.so" โโโโโโโโโโโโ
# Real name = libfoo.so
# Soname = libbar.so (embedded in the library via DT_SONAME)
$ gcc -g -shared -Wl,-soname,libbar.so -o libfoo.so mod1.o mod2.o mod3.o
# โโ Step 3: Verify the soname โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
$ objdump -p libfoo.so | grep SONAME
SONAME libbar.so
$ readelf -d libfoo.so | grep SONAME
0x000000000000000e (SONAME) Library soname: [libbar.so]
# โโ Step 4: Link a program against libfoo.so โโโโโโโโโโโโโโโโโโโโโ
# The linker detects the soname "libbar.so" and embeds it as DT_NEEDED
$ gcc -g -Wall -o prog prog.c libfoo.so
# Check what ended up in the executable:
$ readelf -d prog | grep NEEDED
0x0000000000000001 (NEEDED) Shared library: [libbar.so] โ soname, not libfoo.so!
# โโ Step 5: Try to run โ fails! โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
$ LD_LIBRARY_PATH=. ./prog
./prog: error in loading shared libraries: libbar.so: cannot open
shared object file: No such file or directory
# Dynamic linker searches for libbar.so but only libfoo.so exists!
# โโ Step 6: Create the soname symbolic link โโโโโโโโโโโโโโโโโโโโโโ
$ ln -s libfoo.so libbar.so
$ ls -la libbar.so
lrwxrwxrwx 1 ravi ravi 9 Jun 5 10:00 libbar.so -> libfoo.so
# โโ Step 7: Run successfully โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
$ LD_LIBRARY_PATH=. ./prog
Called mod1-x1
Called mod2-x2
You can see this pattern in action with the real system libraries already installed on your Linux machine:
# Look at the three-name structure for libc on Ubuntu/Debian:
$ ls -la /lib/x86_64-linux-gnu/libc*
-rwxr-xr-x 1 root root 1.9M libc.so.6 โ this IS the soname symlink on some distros
-rwxr-xr-x 1 root root 1.9M libc-2.35.so โ real file (actual glibc 2.35)
# Or on some systems:
lrwxrwxrwx 1 root root 12 libc.so.6 -> libc-2.35.so
# Check the soname embedded in libc.so:
$ readelf -d /lib/x86_64-linux-gnu/libc.so.6 | grep SONAME
0x000000000000000e (SONAME) Library soname: [libc.so.6]
# Look at libpthread:
$ ls -la /lib/x86_64-linux-gnu/libpthread*
lrwxrwxrwx 1 root root 18 libpthread.so -> libpthread.so.0
lrwxrwxrwx 1 root root 23 libpthread.so.0 -> libpthread-2.35.so
-rwxr-xr-x 1 root root 160K libpthread-2.35.so
$ readelf -d /lib/x86_64-linux-gnu/libpthread.so.0 | grep SONAME
0x000000000000000e (SONAME) Library soname: [libpthread.so.0]
# Look at OpenSSL for a clear versioning example:
$ ls -la /usr/lib/x86_64-linux-gnu/libssl*
lrwxrwxrwx 1 root root 14 libssl.so -> libssl.so.1.1
lrwxrwxrwx 1 root root 16 libssl.so.1.1 -> libssl.so.1.1.1f
-rw-r--r-- 1 root root 598K libssl.so.1.1.1f
See the full picture with ldconfig:
# List all sonames that ldconfig knows about in /etc/ld.so.cache
$ ldconfig -p | head -30
libz.so.1 (libc6,x86-64) => /lib/x86_64-linux-gnu/libz.so.1
libxml2.so.2 (libc6,x86-64) => /usr/lib/x86_64-linux-gnu/libxml2.so.2
libssl.so.1.1 (libc6,x86-64) => /usr/lib/x86_64-linux-gnu/libssl.so.1.1
libc.so.6 (libc6,x86-64) => /lib/x86_64-linux-gnu/libc.so.6
...
# The format is: soname (abi,arch) => real_file_path_after_symlink_resolution
๐ฏ Interview Questions & Answers
2. Soname โ contains only the major version; embedded in the library as DT_SONAME and in executables as DT_NEEDED: libfoo.so.1
3. Linker name โ version-independent name, used when compiling with gcc -lfoo: libfoo.so
On disk, the soname and linker name are symbolic links pointing to the real file: libfoo.so โ libfoo.so.1 โ libfoo.so.1.2.3
The key part is -Wl,-soname,libfoo.so.1. The -Wl, prefix passes options directly to the linker (ld). The -soname,libfoo.so.1 part tells the linker to embed libfoo.so.1 as the DT_SONAME tag in the shared library. You can verify this with readelf -d libfoo.so.1.2.3 | grep SONAME.
For example, if you build a program against libfoo.so.1.2.3 (soname: libfoo.so.1), and later a bug-fixed libfoo.so.1.2.4 is installed (same soname), the program automatically uses the new library without recompilation. You just move the soname symlink to point to the new file. This enables transparent bug-fix upgrades.
1. readelf: readelf -d /path/to/libfoo.so | grep SONAME
Output: 0x000000000000000e (SONAME) Library soname: [libfoo.so.1]
2. objdump: objdump -p /path/to/libfoo.so | grep SONAME
Output: SONAME libfoo.so.1
Both commands parse the ELF dynamic section and display the DT_SONAME tag value.
โข Removing a function that was previously exported
โข Changing a function’s signature (different parameter types or count)
โข Changing the size or layout of a struct that the library exports
โข Removing or renaming an exported global variable
For backward-compatible changes (adding new functions, bug fixes without signature changes), you increment only the minor or patch version in the real filename. The soname stays the same, and existing programs automatically get the bug fix.
1. Automatically creates soname symlinks โ it reads each library’s embedded DT_SONAME tag and creates or updates the corresponding symlink (e.g., libfoo.so.1 โ libfoo.so.1.2.3). This is why you need to run sudo ldconfig after installing a library.
2. Rebuilds the binary cache /etc/ld.so.cache โ a binary lookup table that allows the dynamic linker to quickly find libraries without searching all directories on every program launch.
Note: ldconfig does NOT create the linker name symlink (libfoo.so) โ that must be created manually or by the package install script.
You’ve Completed This Chapter!
You now understand how shared libraries work, how the dynamic linker loads them, and how sonames enable library versioning.
