Creating a Shared Library Fundamentals of Shared Librarie

 

Creating a Shared Library
Chapter 41 โ€” Fundamentals of Shared Libraries (TLPI)
Step-by-Step: -fPIC, gcc -shared, soname, symbolic links, and linking programs
๐Ÿ—๏ธ
Build Process
compile โ†’ link โ†’ symlinks
โš™๏ธ
-fPIC Flag
position-independent code
๐Ÿ”—
soname Embedding
-Wl,-soname
๐ŸŽ“
10 Interview Q
with answers

๐Ÿ”‘ Key Terms in This Tutorial
-fPIC Position Independent Code gcc -shared -Wl,-soname ln -s LD_LIBRARY_PATH object files .o GOT PLT -L flag -l flag shared object

Overview: The 4-Step Build Process

Creating a shared library correctly involves four distinct steps. Skipping any of them leads to runtime errors or versioning chaos. The complete workflow is:

Step 1
Compile source files into position-independent object files using -fPIC
Step 2
Link object files into a shared library with the real name, embedding the soname
Step 3
Create symbolic links for the soname and linker name
Step 4
Compile your program using the linker name and run it

1. What is -fPIC? (Position Independent Code)
-fPIC flag GOT โ€” Global Offset Table why required for .so

When compiling code for a shared library, you must use the -fPIC flag. Without it, the library code would contain absolute memory addresses, which would only work if the library was always loaded at exactly the same memory address โ€” impossible when multiple programs share the same library.

What -fPIC does:

  • Generates code that uses relative addresses instead of absolute addresses
  • Global variables and function calls go through the GOT (Global Offset Table) and PLT (Procedure Linkage Table)
  • The dynamic linker fills in the GOT at load time with actual addresses for the current load location
  • Multiple processes can share the same physical pages of library code, saving memory

PIC vs Non-PIC Code โ€” memory addressing:

Without -fPIC With -fPIC
mov eax, [0x7f3a4000] ; absolute addr
Code contains hardcoded address.
Library must always load at same address.
Cannot be shared between processes.
mov eax, [GOT + offset] ; relative
Code uses GOT for indirection.
Library loads at any address.
Pages shared across all processes.
# Step 1: Compile source files to position-independent object files
# -g    = include debug info
# -c    = compile only, don't link
# -fPIC = generate Position Independent Code
# -Wall = enable all warnings

$ gcc -g -c -fPIC -Wall mod1.c mod2.c mod3.c

# This produces: mod1.o  mod2.o  mod3.o
# These are PIC object files, ready to be linked into a shared library
๐Ÿ“Œ Note: -fPIC vs -fpic โ€” The uppercase -fPIC always works on all platforms. -fpic may generate slightly smaller/faster code on some architectures but can fail if the GOT size exceeds a platform limit. On x86-64, both are identical. Use -fPIC to be safe.

2. Creating the Shared Library with Real Name & Embedded Soname
gcc -shared -Wl,-soname -o real-name

The second step links the object files into a shared library. The key flags are:

Flag Meaning Example
-shared Tells gcc to produce a shared library (.so) instead of an executable gcc -shared …
-Wl,-soname,X Passes -soname=X to the linker (ld). Embeds the soname string inside the .so file’s ELF header -Wl,-soname,libdemo.so.1
-o filename Sets the output filename โ€” this should be the real name (with major+minor version) -o libdemo.so.1.0.1
-g Include debug symbols (optional but useful during development) -g
# Step 2: Create the shared library
# Real name: libdemo.so.1.0.1  (what's created on disk)
# Soname: libdemo.so.1         (what gets embedded in ELF header)

$ gcc -g -shared -Wl,-soname,libdemo.so.1 \
      -o libdemo.so.1.0.1 \
      mod1.o mod2.o mod3.o

# Verify the soname was embedded correctly:
$ readelf -d libdemo.so.1.0.1 | grep SONAME
 0x000000000000000e (SONAME) Library soname: [libdemo.so.1]

# Check the file exists:
$ ls -l libdemo.so.1.0.1
-rwxr-xr-x 1 user user 12345 Jun 1 10:00 libdemo.so.1.0.1
๐Ÿ“Œ Understand -Wl: The -Wl, prefix tells gcc to pass the following comma-separated options directly to the linker (ld). So -Wl,-soname,libdemo.so.1 passes -soname libdemo.so.1 to ld. You could also write -Wl,-soname -Wl,libdemo.so.1 โ€” same result.

3. Creating Symbolic Links (Soname & Linker Name)
ln -s soname symlink linker name symlink ls -l verification

After creating the real name file, you need two symbolic links:

  • Soname symlink: libdemo.so.1 โ†’ points to libdemo.so.1.0.1
  • Linker name symlink: libdemo.so โ†’ points to libdemo.so.1 (via soname, not directly to real name)

The symbolic link chain:

libdemo.so

linker name
(symlink)
โŸถ
libdemo.so.1

soname
(symlink)
โŸถ
libdemo.so.1.0.1

real name
(regular file)
# Step 3a: Create the soname symbolic link
$ ln -s libdemo.so.1.0.1 libdemo.so.1
# libdemo.so.1  โ†’  libdemo.so.1.0.1

# Step 3b: Create the linker name symbolic link
# Best practice: point linker name to SONAME (not directly to real name)
# This way, when soname is updated to point to a newer minor version,
# the linker name automatically benefits too.
$ ln -s libdemo.so.1 libdemo.so
# libdemo.so  โ†’  libdemo.so.1  โ†’  libdemo.so.1.0.1

# Verify all three names are in place:
$ ls -l libdemo.so* | awk '{print $1, $9, $10, $11}'
lrwxrwxrwx libdemo.so      -> libdemo.so.1       (linker name symlink)
lrwxrwxrwx libdemo.so.1    -> libdemo.so.1.0.1   (soname symlink)
-rwxr-xr-x libdemo.so.1.0.1                      (real name โ€” regular file)
โœ… Correct setup: linker name โ†’ soname โ†’ real name. This two-hop chain is intentional and important. When you release version 1.0.2 (bugfix), you only update the soname pointer, and both the soname and linker name automatically serve the new version.

4. Building and Running a Program Against the Shared Library
-L flag -l flag LD_LIBRARY_PATH dynamic linker

Now that the library and its symbolic links are ready, you can compile a program against it.

Flag Meaning
-L. Add current directory (.) to the linker search path. The linker will look for libdemo.so here.
-ldemo Link against libdemo. The linker prepends lib and appends .so โ†’ searches for libdemo.so
LD_LIBRARY_PATH=. Tell the dynamic linker (at runtime) to search the current directory for shared libraries
# Step 4a: Compile the program using the linker name (no version numbers needed!)
$ gcc -g -Wall -o prog prog.c -L. -ldemo
#                                 ^^ add '.' to linker search path
#                                     ^^^^^ link against libdemo (finds libdemo.so)

# The linker embeds libdemo.so.1 (the soname) into the prog binary.
# Verify what soname was recorded:
$ readelf -d prog | grep NEEDED
 0x0000000000000001 (NEEDED) Shared library: [libdemo.so.1]
 0x0000000000000001 (NEEDED) Shared library: [libc.so.6]

# Step 4b: Run the program
# LD_LIBRARY_PATH tells the runtime dynamic linker where to look
$ LD_LIBRARY_PATH=. ./prog
Called mod1-x1
Called mod2-x2

# Without LD_LIBRARY_PATH, you'd get:
# ./prog: error while loading shared libraries: libdemo.so.1: cannot open shared object file
โš ๏ธ Development vs Production: LD_LIBRARY_PATH is a convenient development tool but should never be used in production applications. For production use, install the library in a standard system directory (see Part 3 of this series on Installing Shared Libraries).

5. Complete End-to-End Example
source files header file full build steps test program

Example 1: Source files for the demonstration library

/* ===== mod1.c ===== */
#include <stdio.h>
#include "demo.h"

void mod1_func(void) {
    printf("Called mod1-x1\n");
}

/* ===== mod2.c ===== */
#include <stdio.h>
#include "demo.h"

void mod2_func(void) {
    printf("Called mod2-x2\n");
}

/* ===== mod3.c ===== */
#include <stdio.h>
#include "demo.h"

void mod3_func(void) {
    printf("Called mod3-x3\n");
}

/* ===== demo.h ===== */
#ifndef DEMO_H
#define DEMO_H
void mod1_func(void);
void mod2_func(void);
void mod3_func(void);
#endif

Example 2: Test program using the library

/* ===== prog.c ===== */
#include <stdio.h>
#include "demo.h"

int main(void) {
    printf("Program started\n");
    mod1_func();   /* calls function in mod1.c */
    mod2_func();   /* calls function in mod2.c */
    mod3_func();   /* calls function in mod3.c */
    printf("Program ended\n");
    return 0;
}

Example 3: Complete build script

#!/bin/bash
# build_demo_library.sh - Complete build script for libdemo shared library

echo "=== Step 1: Compile source files to PIC object files ==="
gcc -g -c -fPIC -Wall mod1.c mod2.c mod3.c
echo "Created: mod1.o mod2.o mod3.o"

echo ""
echo "=== Step 2: Create shared library with soname embedded ==="
gcc -g -shared -Wl,-soname,libdemo.so.1 \
    -o libdemo.so.1.0.1 \
    mod1.o mod2.o mod3.o
echo "Created: libdemo.so.1.0.1"

echo ""
echo "=== Step 3: Create symbolic links ==="
ln -sf libdemo.so.1.0.1 libdemo.so.1   # soname link
ln -sf libdemo.so.1 libdemo.so          # linker name link
echo "Created: libdemo.so.1 -> libdemo.so.1.0.1"
echo "Created: libdemo.so   -> libdemo.so.1"

echo ""
echo "=== Verify symlinks ==="
ls -l libdemo.so* | awk '{print $1, $9, $10, $11}'

echo ""
echo "=== Step 4: Build the test program ==="
gcc -g -Wall -o prog prog.c -L. -ldemo
echo "Created: prog"

echo ""
echo "=== Step 5: Run the program ==="
LD_LIBRARY_PATH=. ./prog

echo ""
echo "=== Verify soname embedded in binary ==="
readelf -d prog | grep NEEDED

Example 4: Simulating a minor version upgrade

#!/bin/bash
# Simulate releasing version 1.0.2 (a bugfix release)

echo "--- Releasing libdemo version 1.0.2 (bugfix) ---"

# 1. Compile updated sources (with the bug fix) into new version file
gcc -g -shared -Wl,-soname,libdemo.so.1 \
    -o libdemo.so.1.0.2 \
    mod1.o mod2.o mod3.o

echo "New file: libdemo.so.1.0.2"

# 2. Update soname symlink to point to new version
ln -sf libdemo.so.1.0.2 libdemo.so.1
# NOTE: libdemo.so still points to libdemo.so.1, so it also picks up 1.0.2!

echo "Updated: libdemo.so.1 -> libdemo.so.1.0.2"

# 3. Verify
ls -l libdemo.so*
# lrwxrwxrwx libdemo.so      -> libdemo.so.1        (unchanged)
# lrwxrwxrwx libdemo.so.1    -> libdemo.so.1.0.2    (NOW points to 1.0.2)
# -rwxr-xr-x libdemo.so.1.0.1                       (old version, still exists)
# -rwxr-xr-x libdemo.so.1.0.2                       (new version)

# 4. Run the program โ€” it automatically uses 1.0.2 WITHOUT recompilation!
LD_LIBRARY_PATH=. ./prog
echo "Program now uses libdemo 1.0.2 (bugfix release)"

๐ŸŽฏ Interview Questions โ€” Creating Shared Libraries

Q1: What is Position Independent Code (PIC) and why is it required for shared libraries?

PIC is code that uses relative addressing (via GOT/PLT) instead of absolute memory addresses. It is required for shared libraries because a shared library can be loaded at any memory address by the dynamic linker. Without PIC, the library would only work at one specific address, making it impossible to share across multiple processes.
Q2: What is the difference between -fPIC and -fpic compiler flags?

-fPIC always generates correct PIC code on all architectures. -fpic may generate slightly smaller/faster code on some architectures but can fail if the GOT size exceeds a platform-specific limit. On x86-64, both are equivalent. Best practice is to use -fPIC.
Q3: What does the -Wl,-soname,libfoo.so.1 flag do?

-Wl, passes options to the linker (ld). -soname,libfoo.so.1 tells the linker to embed the string libfoo.so.1 into the ELF SONAME field of the shared library file. This soname is later copied into executables that link against the library.
Q4: Why should the linker name symlink point to the soname rather than the real name?

If linker name โ†’ soname โ†’ real name, then updating the soname symlink (for a minor version release) automatically keeps the linker name current. If linker name โ†’ real name directly, you’d need to manually update both the soname and linker name on every minor version release.
Q5: What is LD_LIBRARY_PATH and when should it be used?

LD_LIBRARY_PATH is an environment variable that adds directories to the dynamic linker’s search path at runtime. It is useful for development and testing (loading a library from a non-standard location without installing it). It should NOT be used in production โ€” instead, install libraries in standard directories or use rpath.
Q6: What is the difference between -L and -l flags when compiling?

-L/path adds a directory to the linker’s library search path at link time. -lfoo tells the linker to link against libfoo.so (or libfoo.a). They work together: -L. -ldemo means “search current directory for libdemo.so.”
Q7: After compiling a program with -ldemo, what is actually embedded in the binary โ€” the linker name or soname?

The soname is embedded, not the linker name. The linker name (libdemo.so) is only used to locate the library during compilation. The linker reads the soname from the library’s ELF header and writes that into the executable’s NEEDED entries. You can verify this with readelf -d prog | grep NEEDED.
Q8: What happens if you forget to embed the soname when building a shared library?

If you omit -Wl,-soname, the linker will either embed the full filename (real name including minor version) or leave the SONAME field empty. In either case, runtime loading may fail or the versioning semantics break โ€” the program may refuse to load if the exact filename from the NEEDED entry doesn’t exist as a symlink or file.
Q9: How do you verify that a shared library has the correct soname embedded?

Use readelf -d libfoo.so.1.0.1 | grep SONAME. This displays the SONAME entry from the ELF dynamic section. You can also use objdump -p libfoo.so.1.0.1 | grep SONAME.
Q10: What is the Global Offset Table (GOT) and how does it relate to shared libraries?

The GOT is a table of pointers in a shared library that the dynamic linker fills in at load time with the actual runtime addresses of global variables and external functions. PIC code accesses global symbols through the GOT rather than hardcoding addresses. This allows the same physical code pages to be shared among multiple processes even though each process may have the library at a different virtual address.

๐Ÿ“– Chapter 41 โ€” Shared Libraries Series
Continue to learn how to install libraries properly in production

โ† Part 1: Library Names Next: Installing Shared Libraries โ†’

Leave a Reply

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