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.
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) |
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
- 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
- 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
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.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.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.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.-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.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.-fPIC if that .o will go into a shared library.