Skip to main content

Static and Dynamic Linking in Operating Systems

Published: November 25, 2025 Updated: May 25, 2026 Larry Qu 12 min read

Linking is a crucial phase in the compilation process where object files are combined to create an executable program. In operating systems, particularly Unix-like systems like Linux, there are two primary methods for linking libraries: static linking and dynamic linking. This post explores both approaches, their mechanisms, pros and cons, and when to use each.

What is Linking?

Before diving into static and dynamic linking, let’s briefly recall what linking is. After compiling source code into object files (using tools like GCC), the linker combines these object files with libraries to resolve external references, such as function calls or global variables. This results in an executable file that can run on the operating system.

The linker (ld on Linux) performs two main tasks: symbol resolution — matching each symbol reference (function call, variable access) to its definition — and relocation — patching the addresses in the compiled code so that references point to the correct memory locations after all object files are combined.

Static Linking

Static linking embeds the library code directly into the executable at compile time. The entire library is copied into the final binary.

How It Works

When you compile a program with static libraries, the linker resolves all external symbols by including the necessary code from the library archive (.a files in Linux). The resulting executable is self-contained.

gcc -static main.o -lmylib -o myprogram

The -static flag tells GCC to prefer static libraries and to produce an executable with no dynamic dependencies.

Advantages

  • Self-contained executables: No external dependencies at runtime. The program runs independently.
  • Performance: Slightly faster startup since no dynamic loading is needed.
  • Portability: Easier to distribute as the binary includes everything needed.

Disadvantages

  • Larger file sizes: The executable includes all library code, even unused parts.
  • Memory usage: Each program loads its own copy of the library, wasting memory if multiple programs use the same library.
  • Updates: To update the library, you must recompile and redistribute the entire program.
  • Disk space: More storage required for multiple programs using the same library.

Dynamic Linking

Dynamic linking, also known as shared linking, links libraries at runtime. The executable contains references to shared libraries, which are loaded into memory when the program runs.

How It Works

Shared libraries (.so files in Linux) are loaded by the dynamic linker (ld-linux.so) at program startup or on-demand. The executable includes a table of symbols that need to be resolved at runtime.

gcc main.o -lmylib -o myprogram

Here, mylib is a shared library. The executable will look for it in standard paths like /usr/lib or via LD_LIBRARY_PATH.

Advantages

  • Smaller executables: Only references are included, not the full library code.
  • Memory efficiency: Shared libraries are loaded once and shared across processes.
  • Updates: Library updates can be applied without recompiling programs (as long as the API remains compatible).
  • Modularity: Easier to maintain and distribute updates.

Disadvantages

  • Dependencies: Programs require the shared libraries to be present at runtime, leading to potential “missing library” errors.
  • Startup overhead: Slight delay due to loading and linking at runtime.
  • Version conflicts: Different programs might need different versions of the same library, causing compatibility issues (DLL hell in Windows, similar in Linux).
  • Security: Shared libraries can be a vector for attacks if not properly managed.

Linker Mechanics

The linker transforms a set of object files into an executable or shared library through two distinct phases.

Symbol Resolution

Each object file exports symbols (functions and global variables it defines) and imports symbols (functions and variables it references). The linker walks all input files and builds a global symbol table. Every imported symbol must be matched to exactly one exported symbol. If a symbol is missing, the linker reports an undefined reference error. If a symbol is defined in more than one object file, the linker reports a multiple definition error.

# Undefined reference example
gcc -c main.c -o main.o        # main.c calls foo() but foo is not defined
gcc main.o -o program
# /usr/bin/ld: main.o: in function 'main':
# main.c:(.text+0x1e): undefined reference to 'foo'

Relocation

After resolving symbols, the linker assigns final memory addresses to each section (code, data, BSS — block started by symbol, which stores uninitialized global variables). It then goes back and patches every instruction that references a symbol, replacing placeholder offsets with the actual addresses. The result is a flat binary or an ELF (Executable and Linkable Format) file with all internal references fixed.

Linker Scripts

The linker uses a linker script to control the layout of sections in the output file. The default script is built into ld, but you can provide a custom one with the -T flag:

gcc -Wl,-T,custom.lds main.c -o program

Linker scripts define where .text (code), .data (initialized data), .bss (uninitialized data), and other sections are placed in memory. Embedded developers often use custom linker scripts to place code at specific memory addresses.

PIC vs PIE

Position Independent Code (PIC) and Position Independent Executable (PIE) are related but distinct concepts.

PIC (Position Independent Code)

PIC is a code generation technique used for shared libraries. The code can be placed at any memory address without requiring relocation at load time. Instead of using absolute addresses, PIC uses the Global Offset Table (GOT) and Procedure Linkage Table (PLT) to access global data and call functions indirectly.

Compile a shared library with PIC:

gcc -fPIC -c mylib.c -o mylib.o
gcc -shared -o libmylib.so mylib.o

The -fPIC flag tells GCC to generate position-independent code. Without it, shared library code would need to be patched at load time, making it non-shareable across processes.

PIE (Position Independent Executable)

PIE extends PIC to executables. A PIE binary can be loaded at a random base address, providing Address Space Layout Randomization (ASLR) — a security feature that makes buffer overflow attacks harder to execute.

# Default on modern GCC (Ubuntu 18.04+, Fedora 28+)
gcc -fPIE -pie main.c -o program

# Explicitly disable PIE
gcc -no-pie main.c -o program

Check whether a binary is PIE:

file program
# program: ELF 64-bit LSB pie executable, x86-64, ...

PIE executables are the default on modern Linux distributions. The output of file shows pie executable for PIE and executable for traditional position-dependent binaries.

PIC vs PIE Summary

Aspect PIC PIE
Target Shared libraries (.so) Executaries
Purpose Memory sharing across processes ASLR for security
Overhead Small (GOT indirection) Small (same as PIC)
Default Yes for shared libs Yes on modern distros
Flag -fPIC -fPIE -pie

Shared Object Versioning

Shared libraries use a versioning scheme with three components: real name, soname, and linker name.

Real Name

The actual file on disk, typically libfoo.so.MAJOR.MINOR.PATCH:

ls /usr/lib/x86_64-linux-gnu/libc.so.6
# libc-2.31.so

Soname

The soname is embedded in the shared library and encodes the API version. When an executable is linked against a shared library, the soname is recorded as the runtime dependency, not the real name.

Set the soname during linking:

gcc -shared -Wl,-soname,libfoo.so.1 -o libfoo.so.1.0.0 foo.o

The soname convention libfoo.so.MAJOR means that only major version changes break ABI (Application Binary Interface) compatibility. Minor and patch releases keep the same soname, so existing executables continue to work without relinking.

Linker Name

A symlink without the version number, used during compilation:

ln -s libfoo.so.1 libfoo.so
gcc main.c -L. -lfoo -o program

The linker resolves -lfoo by looking for libfoo.so (the linker name). At runtime, the dynamic linker looks for libfoo.so.1 (the soname).

ABI Compatibility

Breaking changes that require a soname bump include:

  • Changing function signatures (adding/removing arguments, changing return types).
  • Removing public symbols.
  • Changing the size or layout of public structs.
  • Changing the semantics of an existing function.

Additive changes (new functions, new struct fields at the end of existing structs) are backward-compatible.

LD_LIBRARY_PATH vs ldconfig

The dynamic linker searches for shared libraries in a standard set of paths: /lib, /usr/lib, and directories listed in /etc/ld.so.conf. Two mechanisms extend or override this search.

LD_LIBRARY_PATH

An environment variable that prepends directories to the search path. Useful for development and testing:

export LD_LIBRARY_PATH=/home/user/mylibs:$LD_LIBRARY_PATH
./myprogram

Security note: LD_LIBRARY_PATH is ignored for setuid and setgid binaries to prevent privilege escalation.

ldconfig

A system utility that updates the shared library cache (/etc/ld.so.cache) and manages symlinks. After installing a new shared library, run:

sudo ldconfig

To see which paths are searched:

ldconfig -p | head -10

Add custom paths to /etc/ld.so.conf:

echo "/usr/local/lib" | sudo tee -a /etc/ld.so.conf
sudo ldconfig

When to Use Each

Approach Use Case
LD_LIBRARY_PATH Development, testing, temporary overrides
ldconfig + /etc/ld.so.conf Permanent system-wide library installations
-rpath link flag Embedding search path into the binary

Embed a runtime library search path directly into the executable:

gcc main.c -L. -lfoo -Wl,-rpath,/opt/mylibs -o program

This avoids both LD_LIBRARY_PATH and ldconfig for a specific binary — useful for self-contained application bundles.

Comparison Table

Aspect Static Linking Dynamic Linking
File size Larger (library code included) Smaller (only references)
Startup time Faster (no runtime resolution) Slightly slower (symbol resolution)
Runtime memory Higher per-process (duplicated) Lower (shared across processes)
Patching/updates Requires full recompilation Library swap without rebuild
Distribution Single binary, easy Requires shipping .so files
Security patching Must rebuild the binary Update library, restart the app
ASLR effectiveness Dependent on PIE Automatic via GOT indirection
Disk usage High for multiple programs Low (shared files)
Dependency hell Impossible (self-contained) Possible (version conflicts)
Binary size (hello world) ~800KB (glibc statically linked) ~16KB (glibc dynamically linked)

Compiler Flags Reference

Goal Command
Force static linking gcc -static main.c -o program
Link a static library gcc main.c -L. -lmylib.a -o program
Create shared library (PIC) gcc -fPIC -shared -o libfoo.so foo.c
Create shared library (no PIC) gcc -shared -o libfoo.so foo.c (works but not shareable)
Compile with PIE gcc -fPIE -pie main.c -o program
Disable PIE gcc -no-pie main.c -o program
Set soname gcc -shared -Wl,-soname,libfoo.so.1 -o libfoo.so.1.0 foo.c
Embed rpath gcc main.c -L. -lfoo -Wl,-rpath,/opt/lib -o program
Verbose linker output gcc main.c -Wl,--verbose -o program
Print link map gcc main.c -Wl,-Map,output.map -o program

Real-World Examples

Check Dependencies with ldd

After compiling a dynamically linked program, inspect its dependencies:

gcc main.c -o myprogram
ldd myprogram

Output:

linux-vdso.so.1 (0x00007ffe3a7e0000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f8a1a800000)
/lib64/ld-linux-x86-64.so.2 (0x00007f8a1ae00000)

For a statically linked binary, ldd reports “not a dynamic executable”:

gcc -static main.c -o myprogram-static
ldd myprogram-static
# not a dynamic executable

Size Comparison

Compare the size of a minimal C program with static vs dynamic linking:

echo 'int main(){return 0;}' > tiny.c
gcc -no-pie tiny.c -o tiny-dynamic    # ~16KB
gcc -static tiny.c -o tiny-static     # ~800KB
ls -lh tiny-dynamic tiny-static

The static binary is roughly 50x larger because it embeds the entire C standard library.

Build a Shared Library from Scratch

# Create a simple library
cat > greet.c << 'EOF'
#include <stdio.h>
void greet(const char *name) {
    printf("Hello, %s!\n", name);
}
EOF

# Compile as shared library with soname
gcc -fPIC -c greet.c -o greet.o
gcc -shared -Wl,-soname,libgreet.so.1 -o libgreet.so.1.0.0 greet.o

# Create symlinks
ln -sf libgreet.so.1.0.0 libgreet.so.1
ln -sf libgreet.so.1 libgreet.so

# Compile and link the test program
cat > main.c << 'EOF'
void greet(const char *name);
int main() {
    greet("World");
    return 0;
}
EOF

gcc main.c -L. -lgreet -o greet-program
LD_LIBRARY_PATH=. ./greet-program
# Hello, World!

This workflow — create source, compile with PIC, link with soname, symlink, and run with LD_LIBRARY_PATH — mirrors how system libraries like glibc and OpenSSL are packaged on Linux distributions.

Inspect ELF Sections and Dynamic Entries

# List sections in the binary
readelf -S myprogram

# Show dynamic section (needed libraries, rpath, soname)
readelf -d myprogram

# Show the symbol table
nm myprogram | grep -i " T "

# Show runtime interpreter
readelf -l myprogram | grep interpreter

Static + Dynamic Mixed Linking

A single binary can use both static and dynamic libraries. This is useful when you want the portability of static linking for specific libraries while keeping the rest dynamically linked:

# Statically link libexpat, dynamically link everything else
gcc main.c -Wl,-Bstatic -lexpat -Wl,-Bdynamic -o program

The -Wl,-Bstatic flag tells the linker to use static versions for subsequent libraries, and -Wl,-Bdynamic switches back to dynamic linking. This pattern is common when distributing self-contained binaries for a single OS family without bundling every shared library.

Lazy Binding vs Eager Binding

By default, the dynamic linker resolves function addresses on first call (lazy binding) rather than at startup:

# Lazy binding (default) — resolves symbols on first use
gcc main.c -lfoo -o program

# Eager binding — resolves all symbols at startup
gcc main.c -lfoo -Wl,-z,now -o program

Eager binding slows startup but prevents unexpected failures mid-execution. It also improves security because the GOT (Global Offset Table) entries are marked read-only after resolution when combined with full RELRO (Relocation Read-Only).

Interposition with LD_PRELOAD

The dynamic linker allows replacing library functions at runtime without recompilation:

# Preload a custom malloc wrapper
LD_PRELOAD=/path/to/libmymalloc.so ./myprogram

This intercepts all calls to malloc, free, realloc from the program and any loaded libraries. Use cases include memory debugging (valgrind uses this approach), profiling, and sandboxing. The technique works because the dynamic linker resolves symbols in the order specified by the search path, placing preloaded symbols ahead of standard library symbols.

LTO performs cross-module optimization at link time, treating all object files as a single compilation unit:

gcc -flto main.c helper.c -O3 -o program

LTO enables aggressive inlining, dead code elimination across files, and interprocedural constant propagation. The trade-off is longer link times and higher memory usage during compilation.

The Full Compilation Pipeline

Understanding where linking fits in the compilation pipeline helps debug build issues:

Source (.c, .cpp)
  → Preprocessor (cpp) — expands #include, #define
  → Compiler (cc1) — generates assembly (.s)
  → Assembler (as) — generates object files (.o)
  → Linker (ld) — combines .o + libraries → executable

Errors at the linker stage (undefined references, multiple definitions) mean all source files compiled successfully but the object files cannot be combined into a working executable. This is the final gate before the binary is produced.

When to Use Each

  • Static Linking: Use for embedded systems, standalone applications, or when you want to avoid runtime dependencies. Common in scenarios where the environment is controlled and updates are infrequent. Also useful for containers where minimizing layers matters less than avoiding libc version mismatches.

  • Dynamic Linking: Preferred for most desktop and server applications in Linux. It promotes code reuse, easier maintenance, and efficient resource usage. Use when you distribute software via package managers (apt, dnf, pacman) that handle dependency resolution automatically.

For security-sensitive applications, prefer PIE executables with dynamic linking — ASLR is more effective and security patches to libraries apply without rebuilding the application.

Conclusion

Static and dynamic linking each have their place in operating systems. Static linking offers simplicity and portability for self-contained programs, while dynamic linking provides efficiency and flexibility for shared environments. Understanding the linker mechanics — symbol resolution, relocation, PIC, soname versioning — helps you make informed decisions during software development and deployment on platforms like Linux.

For more details, refer to the GCC documentation or man pages for ld and ldd.

Resources

Comments

👍 Was this article helpful?