The Shared Library Soname

 

The Shared Library Soname
Linux System Programming ยท Chapter 41 ยท Section 41.4.4
๐Ÿท๏ธ
Soname
๐Ÿ”„
Versioning
๐Ÿ”—
Symlinks
๐Ÿ“ฆ
DT_SONAME

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

Soname DT_SONAME Real Name Linker Name Symbolic Link -Wl,-soname objdump -p readelf -d Library Versioning ABI Compatibility ldconfig

๐Ÿ“› The Three Names of a Shared Library

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)
๐Ÿ’ก Convention: The soname encodes only the major version number (the part that changes when the ABI breaks). Minor and patch versions only change the real filename. This way, bug-fix releases (1.2.3 โ†’ 1.2.4) are transparent to existing programs โ€” they just update the real file and move the soname symlink.

๐Ÿ› ๏ธ Coding Example 1 โ€” Creating a Shared Library with a Soname

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]
 ...

๐Ÿ” What Gets Embedded in the Executable?

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

โš ๏ธ The Problem: Dynamic Linker Looks for the Soname, Not the Real File
# 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)
โš ๏ธ Key Rule: When a library has a soname, you must create a symbolic link from the soname to the real library filename. The dynamic linker looks for the soname, not the real filename.

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
โœ… It works! The dynamic linker finds libbar.so (the soname) which is a symlink to libfoo.so (the real name).

๐Ÿ› ๏ธ Coding Example 2 โ€” Real-World Library Versioning with Sonames

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

๐Ÿ› ๏ธ Coding Example 3 โ€” ABI Break: Bumping the Major Version

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 โš ๏ธ

๐Ÿ“– Book Example Walkthrough (Exact Steps from TLPI 41.4.4)

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
๐Ÿ”‘ Key Insight: The program was linked against libfoo.so (real name), but the executable stores libbar.so (soname). The dynamic linker at runtime searches for libbar.so. Because libbar.so is a symlink to libfoo.so, the library is found and loaded correctly.

๐Ÿ”ฌ Examining Real System Library Sonames

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

Q1. What is a soname? What ELF tag stores it?
A soname (shared object name) is an alias name embedded inside a shared library. It is stored in the ELF dynamic section as the DT_SONAME tag. When the linker creates an executable and detects that a shared library has a soname, it embeds the soname (not the real filename) as the DT_NEEDED entry in the executable. At runtime, the dynamic linker searches for a file with the soname, not the original library filename.
Q2. What are the three names of a shared library? Give an example for each.
1. Real name โ€” the actual library file on disk, containing full version numbers: libfoo.so.1.2.3

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

Q3. What gcc command creates a shared library with a soname?
gcc -g -shared -Wl,-soname,libfoo.so.1 -o libfoo.so.1.2.3 mod1.o mod2.o

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.

Q4. Why must you create a symbolic link from the soname to the real library file?
Because the dynamic linker at runtime searches for a file whose name matches the soname (the DT_NEEDED entry in the executable). The actual library file is named with full version information (e.g., libfoo.so.1.2.3). Without the symbolic link libfoo.so.1 โ†’ libfoo.so.1.2.3, the dynamic linker would fail with “cannot open shared object file: No such file or directory” because it cannot find a file literally named libfoo.so.1.
Q5. What is the purpose of the soname โ€” why not just use the real filename everywhere?
The soname provides a level of indirection for library versioning. It allows executables to use, at runtime, a different version of the library than the one they were compiled against โ€” as long as the soname (major version) is the same.

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.

Q6. How do you check the soname of an existing shared library on your system?
Two commands work:

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.

Q7. When do you need to change the soname (bump the major version)?
You must change the soname (bump the major version) whenever you make a backward-incompatible ABI (Application Binary Interface) change. This includes:

โ€ข 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.

Q8. What does ldconfig do and how does it relate to sonames?
ldconfig scans the standard library directories (listed in /etc/ld.so.conf) and:

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.

โ† Previous: Using a Shared Library โ†ฉ Chapter Index

Leave a Reply

Your email address will not be published. Required fields are marked *