Step-by-Step: -fPIC, gcc -shared, soname, symbolic links, and linking programs
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:
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
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
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)
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
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
-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.-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.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.-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.”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.-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.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.โ Part 1: Library Names Next: Installing Shared Libraries โ
