Object Libraries: Static vs Shared What are static and shared libraries

 

Chapter 41 – Part 4 of 6
Object Libraries: Static vs Shared
What are static and shared libraries, how are they created, and when should you use each?
3
Code Examples
10
Interview Q&A
~20
min read

What Is an Object Library?

An object library is a file that bundles one or more compiled object files (.o) together so that programs can link against them. Instead of duplicating common code in every program, you put it in a library once and share it. Linux supports two types: static libraries (.a archive files) and shared libraries (.so dynamic shared objects).

This tutorial covers: how each is built, what tools are involved, memory and disk trade-offs, and the creation workflow from C source to installable library.

Key Terms
static library (.a) shared library (.so) ar command ranlib nm command objdump readelf -fPIC -shared DSO

1. Static Libraries (.a)

A static library is an archive of .o files, created with the ar tool. When you link a program against a static library, the linker extracts only the .o files that define symbols the program actually uses and copies them directly into the final executable.

Stage Files Involved Result
Compile sources mod1.c mod2.c mod3.c mod1.o mod2.o mod3.o
Archive into .a mod1.o mod2.o mod3.o libdemo.a (archive)
Link program main.o + libdemo.a myprog (self-contained executable)
Key fact: With a static library, the linker copies the needed .o files into the final executable. The .a file is no longer needed at runtime. Each program gets its own private copy of the library code in its binary.

2. Shared Libraries (.so)

A shared library is an ELF file (Executable and Linkable Format) that is loaded into a process’s address space at runtime by the dynamic linker. Multiple processes share the same physical memory pages of the library’s read-only code segment. Only private data is duplicated per process.

Process Virtual Address Space Physical RAM
Process A 0x7f000000 → libdemo code One physical page of libdemo code (shared by MMU)
Process B 0x7e800000 → libdemo code
Different virtual addresses → same physical RAM page (read-only code is shared, writable data is private)

Code Example 1 — Building a Static Library

/* math_utils.c — utility functions */
#include "math_utils.h"
#include <math.h>

int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}

double circle_area(double radius) {
    return M_PI * radius * radius;
}

int gcd(int a, int b) {
    while (b != 0) {
        int t = b;
        b = a % b;
        a = t;
    }
    return a;
}
/* string_utils.c */
#include "string_utils.h"
#include <string.h>
#include <ctype.h>

void str_to_upper(char *s) {
    while (*s) { *s = toupper(*s); s++; }
}

int str_count_words(const char *s) {
    int count = 0, in_word = 0;
    while (*s) {
        if (isspace(*s)) { in_word = 0; }
        else if (!in_word) { in_word = 1; count++; }
        s++;
    }
    return count;
}
# Step 1: Compile to object files (NO -fPIC needed for static libs)
gcc -g -c -Wall math_utils.c -o math_utils.o
gcc -g -c -Wall string_utils.c -o string_utils.o

# Step 2: Create the static library archive with 'ar'
# r = insert/replace members
# c = create archive if it doesn't exist
# s = create symbol index (same as running ranlib)
ar rcs libutils.a math_utils.o string_utils.o

# Verify: list contents of the archive
ar -t libutils.a
# math_utils.o
# string_utils.o

# Check symbols defined in the archive
nm libutils.a
# math_utils.o:
# 0000000000000000 T factorial
# 0000000000000030 T circle_area
# 0000000000000060 T gcd
# string_utils.o:
# 0000000000000000 T str_to_upper
# 0000000000000030 T str_count_words

# Step 3: Link a program against the static library
# -lutils tells linker to use libutils.a
# -L. says "look in current directory"
# -lm links the math library (for M_PI)
gcc -o myprog main.c -L. -lutils -lm

# The resulting binary is self-contained — libutils.a not needed at runtime
ldd myprog
# linux-vdso.so.1
# libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6
# libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6
# (no libutils — it's been statically linked in!)

Code Example 2 — Building a Shared Library (Complete Workflow)

/* net_utils.c — networking helper functions */
#include "net_utils.h"
#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>

/* Convert IPv4 string to uint32 */
uint32_t ip_to_int(const char *ip_str) {
    struct in_addr addr;
    if (inet_pton(AF_INET, ip_str, &addr) != 1)
        return 0;
    return ntohl(addr.s_addr);
}

/* Convert uint32 back to IPv4 string */
void int_to_ip(uint32_t ip, char *buf, size_t buf_len) {
    struct in_addr addr;
    addr.s_addr = htonl(ip);
    inet_ntop(AF_INET, &addr, buf, buf_len);
}

/* Check if port number is valid */
int is_valid_port(int port) {
    return (port > 0 && port <= 65535);
}
/* net_utils.h */
#ifndef NET_UTILS_H
#define NET_UTILS_H
#include <stdint.h>
#include <stddef.h>

uint32_t ip_to_int(const char *ip_str);
void     int_to_ip(uint32_t ip, char *buf, size_t buf_len);
int      is_valid_port(int port);

#endif
# Step 1: Compile with -fPIC (MANDATORY for shared libraries)
gcc -g -c -fPIC -Wall net_utils.c -o net_utils.o

# Check that PIC was applied — look for @PLT (Procedure Linkage Table) entries
objdump -d net_utils.o | grep -c "PLT"   # will be > 0

# Step 2: Link into shared library
# -shared     : create a shared object
# -Wl,-soname : embed soname into the ELF
# -o          : real name of the output file
gcc -g -shared -Wl,-soname,libnetutils.so.1 \
    -o libnetutils.so.1.0.0 \
    net_utils.o

# Step 3: Verify the soname was embedded
readelf -d libnetutils.so.1.0.0 | grep SONAME
# (SONAME) Library soname: [libnetutils.so.1]

# Step 4: Create symlinks
ln -s libnetutils.so.1.0.0 libnetutils.so.1   # soname symlink
ln -s libnetutils.so.1      libnetutils.so      # linker name symlink

# Step 5: Compile test program
gcc -o netprog main_net.c -L. -lnetutils

# Step 6: Run (tell dynamic linker where to find the .so)
LD_LIBRARY_PATH=. ./netprog

3. Essential Tools for Working with Libraries

Tool Purpose Common Use
ar Create/modify static library archives ar rcs libfoo.a *.o
ranlib Generate symbol index for .a file ranlib libfoo.a (now built into ar s)
nm List symbols in object/library file nm libfoo.so, nm -D libfoo.so (dynamic)
readelf Display ELF file structure and sections readelf -d lib.so (dynamic section)
objdump Disassemble and inspect object files objdump -d lib.so
ldd List shared library dependencies of an ELF ldd ./myprog
ldconfig Build ld.so.cache and update soname symlinks ldconfig -v, ldconfig -p
strip Remove debug symbols to reduce .so size strip --strip-debug libfoo.so

Code Example 3 — Inspecting Library Symbols and Dependencies

# === nm: List exported symbols of a shared library ===
# -D : show dynamic (exported) symbols only
# T  : defined in text (code) section — these are functions
# U  : undefined — symbols this library needs from elsewhere

nm -D libnetutils.so.1.0.0
# 0000000000001139 T int_to_ip
# 0000000000001109 T ip_to_int
# 0000000000001180 T is_valid_port
#                  U htonl            ← from libc
#                  U inet_ntop        ← from libc
#                  U inet_pton        ← from libc
# === readelf: Inspect the ELF dynamic section ===
readelf -d libnetutils.so.1.0.0

# Dynamic section at offset 0x2db8 contains 26 entries:
#   Tag        Type                   Name/Value
#  0x000000000000000e (SONAME)       Library soname: [libnetutils.so.1]
#  0x0000000000000001 (NEEDED)       Shared library: [libc.so.6]
#  0x000000000000000c (INIT)         0x1000
#  0x000000000000000d (FINI)         0x11a4
#  ...
# === ldd: Check what a program needs at runtime ===
ldd netprog
# linux-vdso.so.1 (0x00007fff...)
# libnetutils.so.1 => ./libnetutils.so.1.0.0 (0x00007f...)
# libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f...)

# === objdump: Disassemble to verify PIC (look for @PLT calls) ===
objdump -d libnetutils.so.1.0.0 | grep "plt"
# callq  <htonl@plt>      ← PIC indirect call through PLT
# callq  <inet_ntop@plt>  ← PIC indirect call through PLT

# === file: Quick check of library type ===
file libnetutils.so.1.0.0
# libnetutils.so.1.0.0: ELF 64-bit LSB shared object, x86-64, dynamically linked

file libutils.a
# libutils.a: current ar archive

4. When to Use Static vs Shared Libraries

Use Static Library When:
  • Deploying single standalone binary (no install pain)
  • Embedded target with no dynamic linker (bare-metal)
  • Small utility where code size doesn’t matter
  • Library has no stable ABI (internal use only)
  • Security-sensitive app that can’t trust system .so
Use Shared Library When:
  • Library used by many programs (RAM sharing benefit)
  • Need to patch/update library without recompiling everything
  • Plugin architecture (dlopen)
  • System library distributed with OS (libc, libpthread…)
  • Large library code base (e.g. GTK, Qt, OpenSSL)

Interview Questions & Answers

Q1. What is the difference between a static library (.a) and a shared library (.so)?
A static library (.a) is an archive of .o files. At link time, the linker extracts needed object files and copies their code directly into the executable. The .a file is not needed at runtime. Each program gets its own copy of the code in its binary — larger executable, but self-contained. A shared library (.so) is an ELF file loaded at runtime by the dynamic linker. Multiple programs share one physical copy of the code in RAM. The .so file must be present at runtime. Executables are smaller but depend on the library being installed.
Q2. What is the ar command used for and what do the flags rcs mean?
ar (archive) creates and manages static library (.a) files. In ar rcs libfoo.a file.o: r = replace/insert member into archive, c = create archive if it doesn’t exist (suppress warning), s = write a symbol index to the archive (equivalent to running ranlib). The symbol index speeds up link-time symbol resolution by letting the linker quickly scan which object files define which symbols without reading every object file sequentially.
Q3. Why is -fPIC not needed for static libraries but mandatory for shared libraries?
Static library code is copied into the executable at a fixed, known address by the linker — absolute addressing works fine because the address is resolved at link time. Shared library code is loaded at an unknown, varying address at runtime (the OS maps it wherever free space exists). Without PIC, the code contains absolute addresses that the dynamic linker would have to patch (relocate) for every process — this would require making writable copies of all code pages, eliminating memory sharing. With PIC, all address references are relative (via PLT/GOT), so the same physical code page works at any virtual address in any process.
Q4. What does the nm command tell you about a library, and what does the T symbol type mean?
nm lists the symbols in an object file, archive, or shared library. Key symbol types: T = symbol defined in the text (code) section — a function or global code. D = defined in data section (initialized global variable). B = defined in BSS (uninitialized global variable). U = undefined — the symbol is referenced but defined elsewhere (in another library). For shared libraries, use nm -D to list only the dynamic (exported) symbols that programs can call.
Q5. What is the PLT and GOT in the context of shared libraries?
The PLT (Procedure Linkage Table) and GOT (Global Offset Table) are ELF mechanisms that enable PIC. When PIC code calls an external function (e.g., printf), it doesn’t call it directly with an absolute address. Instead, it calls a PLT stub. The PLT stub reads the real address from the GOT. The GOT is a table of absolute addresses that the dynamic linker fills in at program startup (or lazily on first call). The GOT is writable (per-process), while the PLT and code pages are read-only (shared). This design lets code be shared while function address resolution remains per-process.
Q6. A program was compiled against libfoo.a (static). If libfoo is updated with a security fix, does the program automatically get the fix?
No. With static linking, the library code is embedded in the executable binary. Updating the .a file on disk has zero effect on already-compiled binaries. The program must be recompiled and relinked against the updated library to incorporate the fix. This is a major operational disadvantage of static linking in production environments — every binary using the vulnerable library must be rebuilt and redeployed. With shared libraries, updating the .so file and restarting programs is sufficient.
Q7. What does readelf -d show and when would you use it?
readelf -d libfoo.so prints the ELF dynamic section, which contains metadata about the shared library: the embedded SONAME, NEEDED entries (which other libraries it depends on), symbol table offset, relocation table information, and run-time linker path (RPATH/RUNPATH). Use it to: (1) verify the embedded soname is correct, (2) check what other libraries a .so depends on, (3) check if an RPATH is baked in, (4) debug dynamic linking issues.
Q8. What is RPATH and how does it differ from LD_LIBRARY_PATH?
RPATH is a list of directories embedded into the ELF binary at link time using -Wl,-rpath,/some/path. The dynamic linker searches RPATH directories before LD_LIBRARY_PATH. RPATH is per-binary (only that binary uses it) and is permanent (baked into the ELF). LD_LIBRARY_PATH is an environment variable that adds directories to the search path for the current shell session. It affects all programs run from that shell. RPATH is preferred for deployment (stable, per-binary), while LD_LIBRARY_PATH is useful for development/testing of private libraries.
Q9. How do you check which symbols a shared library exports (its public API)?
Use nm -D libfoo.so — the -D flag shows only dynamic (exported) symbols. Alternatively, objdump -T libfoo.so shows the dynamic symbol table with additional information. For a human-readable list of only the exported function names: nm -D --defined-only libfoo.so | grep ' T '. You can also use readelf --syms libfoo.so for the full symbol table with section information.
Q10. What happens if you forget the -fPIC flag when compiling a .o file that will be used in a shared library?
The linker will either refuse to create the shared library with an error like “recompile with -fPIC”, or (on some older systems) will create the shared library but with non-PIC code that requires text-segment relocations. Text relocations mean the dynamic linker must modify code pages — making them non-shareable (each process gets its own copy) and in modern kernels with W^X policy, they may be rejected entirely. Always compile every source file with -fPIC if that .o will go into a shared library.

Leave a Reply

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