Static vs Dynamic Libraries in Linux and Linking Executables
- Ashok Kumar Kumawat
- Apr 5
- 7 min read
Prerequisites
Before diving in, make sure you have:
A Linux environment (Ubuntu, Fedora, etc.)
GCC compiler (sudo apt install build-essential on Ubuntu)
Basic knowledge of C programming (printf, scanf, functions)
Familiarity with terminal commands (ls, nm, ldd)
Terminal commands
1. ls
👉 Colors may differ: .so often shows in green (shared object), .a in default color.
2. nm
Purpose: Shows symbols (functions, variables) inside object files or libraries.
Usage for libraries: Lets you check if functions like printf are defined inside a library.
Command:
bash
nm -D /usr/lib/x86_64-linux-gnu/libc.so.6 | grep printf
Output example:
Code
0000000000055410 T printf
👉 T means the symbol is defined (text section = function code).
3. ldd
Purpose: Lists dynamic libraries required by an executable.
Usage for libraries: Lets you see whether your program depends on dynamic .so files.
Command:
bash
ldd ./prog_dynamic
Output example:
Code
libmylib.so => ./libmylib.so (0x00007f...) libc.so.6 => /usr/lib/x86_64-linux-gnu/libc.so.6 (0x00007f...) /lib64/ld-linux-x86-64.so.2 (0x00007f...)
👉 Confirms dynamic linking.
How They Fit Together
ls → Identify what libraries exist (static .a or dynamic .so).
nm → Inspect what functions are inside those libraries.
ldd → Check what your executable actually links to at runtime.
Understanding Libraries
Static Library (.a): Code is copied into the executable at compile time.
Dynamic Library (.so): Code is linked at runtime by the dynamic linker.
C Standard Library (glibc): Provides printf, scanf, malloc, etc. → usually dynamic by default.
Small Code Example
Let’s write a simple program that uses both user-defined and standard functions.
main.c
c
#include <stdio.h>
#include "mylib.h" // our custom library header
int main() {
hello(); // custom function
printf("Enter a number: ");
int x;
scanf("%d", &x); // standard library function
printf("You entered: %d\n", x);
return 0;
}
mylib.c
c
#include <stdio.h>
void hello() {
printf("Hello from mylib!\n");
}
mylib.h
c
void hello();
Example 1 – Static Linking
bash
gcc -c mylib.c -o mylib.o
ar rcs libmylib.a mylib.o
gcc main.c -L. -lmylib -static -o prog_static
What each command does:
gcc -c mylib.c -o mylib.o : compile mylib.c to object file.
ar rcs libmylib.a mylib.o : create static archive libmylib.a.
gcc main.c -L. -lmylib -static -o prog_static : link main.c with libmylib.a and force static linking for all libraries (including libc).
Code Recap
Compile source into object file
bash
gcc -c mylib.c -o mylib.o
gcc compiles mylib.c into machine code.
-c means compile only, don’t link yet.
Output: mylib.o (object file).
👉 This is the raw compiled code for your custom function(s).
Create static library archive
bash
ar rcs libmylib.a mylib.o
ar = archive tool (bundles object files into .a).
rcs flags:
r → replace/add file into archive
c → create archive if it doesn’t exist
s → add an index for faster linking
Output: libmylib.a (static library).
👉 This is your reusable static library containing hello().
Link with static library
bash
gcc main.c -L. -lmylib -static -o prog_static
main.c → your main program.
-L. → look for libraries in current directory.
-lmylib → link with libmylib.a (prefix lib + suffix .a are implied).
-static → force static linking (no .so files).
-o prog_static → output executable named prog_static.
👉 Result: prog_static is a fully self-contained executable. It includes both your custom hello() and standard functions like printf baked in.
Verification
Run:
bash
ldd prog_static
Output:
Code
not a dynamic executable
👉 Confirms everything is statically linked.
Why This Matters
Static linking makes your program portable (no external .so needed).
But it increases size (because libc and your library code are copied inside).
Dynamic linking is smaller and more flexible, but depends on system libraries.
Linker-stage verbose check (example):
bash
gcc main.c -L. -lmylib -static -o prog_static -Wl,-v
Sample relevant lines you may see from the linker:
Code
attempt to open /usr/lib/gcc/x86_64-linux-gnu/9/libgcc.a succeeded
attempt to open /usr/lib/x86_64-linux-gnu/libc.a succeededRuntime Check output:
bash
ldd prog_static
Output:
Code
not a dynamic executable
Notes:
prog_static contains code for hello() and libc functions like printf and scanf.
File size will be significantly larger than dynamically linked executable.
Example 2 – Dynamic Linking (Custom Library)
bash
gcc -fPIC -c mylib.c -o mylib.o
gcc -shared -o libmylib.so mylib.o
gcc main.c -L. -lmylib -o prog_dynamic
What each command does:
-fPIC produces position-independent code required for shared libraries.
gcc -shared -o libmylib.so mylib.o creates libmylib.so.
gcc main.c -L. -lmylib -o prog_dynamic links main.c against libmylib.so (dynamic).
Code Recap
Compile position-independent code
bash
gcc -fPIC -c mylib.c -o mylib.o
-fPIC → Position Independent Code. This is required for shared libraries because they can be loaded at different memory addresses at runtime.
-c → compile only, don’t link yet.
Output: mylib.o (object file ready for dynamic linking).
👉 Without -fPIC, you might get relocation errors when creating .so.
Create shared library
bash
gcc -shared -o libmylib.so mylib.o
-shared → tells GCC to produce a shared object (.so).
Output: libmylib.so (dynamic library).
👉 This is your reusable dynamic library containing hello().
Link main program with shared library
bash
gcc main.c -L. -lmylib -o prog_dynamic
main.c → your main program.
-L. → look for libraries in current directory.
-lmylib → link with libmylib.so (prefix lib + suffix .so are implied).
Output: prog_dynamic (executable).
👉 At this stage, the executable doesn’t contain the full code of hello(). Instead, it stores a reference to libmylib.so.
Runtime Linking
When you run ./prog_dynamic, the dynamic linker (ld-linux.so) loads:
Your custom libmylib.so
Standard C library (libc.so.6) for printf, scanf
Check with:
bash
ldd prog_dynamic
Output:
Code
libmylib.so => ./libmylib.so (0x00007f...)
libc.so.6 => /usr/lib/x86_64-linux-gnu/libc.so.6 (0x00007f...)
/lib64/ld-linux-x86-64.so.2 (0x00007f...)
👉 Confirms dynamic linking: both your custom library and glibc are loaded at runtime.
Key Differences vs Static
Static (.a): Code copied into executable → no external dependency.
Dynamic (.so): Executable depends on external library → smaller size, shared in memory.
Linker-stage verbose check:
bash
gcc main.c -L. -lmylib -o prog_dynamic -Wl,-v
Sample relevant lines:
Code
attempt to open /usr/lib/x86_64-linux-gnu/libc.so succeededRuntime Check output:
bash
ldd prog_dynamic
Output:
Code
libmylib.so => ./libmylib.so (0x00007f...)
libc.so.6 => /usr/lib/x86_64-linux-gnu/libc.so.6 (0x00007f...)
/lib64/ld-linux-x86-64.so.2 (0x00007f...)
Notes:
hello() is resolved from libmylib.so at runtime.
printf and scanf are resolved from libc.so.6 at runtime.
If libmylib.so is not in a standard path, run LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH ./prog_dynamic or install the .so into a system library path.
Example 3 – Mixed Linking
Suppose you want your custom library static, but standard functions dynamic:
bash
gcc -c mylib.c -o mylib.o
ar rcs libmylib.a mylib.o
gcc main.c -L. -lmylib -o prog_mixed
What each command does:
Create libmylib.a as in Example 1.
Link main.c with libmylib.a but do not use -static. The linker will include hello() from the static archive, while standard libraries (libc) remain dynamically linked.
Code Recap
Step 1: Compile your custom library source
bash
gcc -c mylib.c -o mylib.o
Compiles mylib.c into an object file (mylib.o).
-c means compile only, don’t link yet. 👉 This is the raw machine code for your hello() function.
Step 2: Create a static library archive
bash
ar rcs libmylib.a mylib.o
ar bundles object files into a static library (.a).
Flags:
r → replace/add file
c → create archive
s → add index for faster linking 👉 Now you have libmylib.a, a static library containing your custom function.
Step 3: Link main program with static library
bash
gcc main.c -L. -lmylib -o prog_mixed
main.c → your main program.
-L. → look for libraries in current directory.
-lmylib → link with libmylib.a (prefix lib + suffix .a are implied).
Output: prog_mixed (executable).
👉 Notice: no -static flag here. That means:
Your custom library (libmylib.a) is linked statically.
Standard C library (libc.so.6) is linked dynamically.
Verification
Run:
bash
ldd prog_mixed
Output:
Code
libc.so.6 => /usr/lib/x86_64-linux-gnu/libc.so.6
/lib64/ld-linux-x86-64.so.2
👉 Confirms:
hello() is baked into the executable (from libmylib.a).
printf and scanf are dynamically linked (from libc.so.6).
Why This Is “Mixed”
Your custom functions → static (no external dependency).
Standard library functions (printf, scanf) → dynamic (shared at runtime).
This is actually the default behavior when you link with a .a library but don’t force -static.
Check output:
bash
ldd prog_mixed
Output:
Code
libc.so.6 => /usr/lib/x86_64-linux-gnu/libc.so.6 (0x00007f...)
/lib64/ld-linux-x86-64.so.2 (0x00007f...)
Notes:
hello() is part of the executable (static).
printf and scanf remain dynamic (from libc.so.6).
This is a common pattern: static custom libraries + dynamic system libraries.
Useful inspection commands and how to interpret them
ls --color=auto
Use to list files and see .a and .so files.
Color depends on LS_COLORS configuration; .so often appears with executable/shared-object color, .a appears as a regular file.
Example:
bash
ls -l
Possible output lines:
Code
-rw-r--r-- 1 user user 1234 Apr 5 20:00 libmylib.a
-rwxr-xr-x 1 user user 5678 Apr 5 20:01 libmylib.so
-rwxr-xr-x 1 user user 123456 Apr 5 20:02 prog_dynamic
nm — inspect symbols
Inspect static archive:
bash
nm libmylib.a | grep hello
Inspect shared object (dynamic symbols):
bash
nm -D libmylib.so | grep hello
Interpretation:
T or t indicates a defined function symbol in the text section.
U indicates an undefined symbol (the object expects it from elsewhere).
ldd — list dynamic dependencies of an executable
ldd prog_dynamic shows which .so files the executable will load at runtime.
If ldd prints not a dynamic executable, the binary is statically linked.
Linker verbose output
Add -Wl,-v to gcc to see what the linker attempts to open and which libraries it finds.
Example:
bash
gcc main.c -L. -lmylib -o prog_dynamic -Wl,-v
Look for lines like:
Code
attempt to open /usr/lib/x86_64-linux-gnu/libc.so succeeded
or for static linking:
Code
attempt to open /usr/lib/x86_64-linux-gnu/libc.a succeeded
Side-by-side summary table
Aspect | Static (prog_static) | Dynamic (prog_dynamic) | Mixed (prog_mixed) |
Custom library file used | libmylib.a | libmylib.a | |
Command to create library | ar rcs libmylib.a mylib.o | gcc -shared -o libmylib.so mylib.o | ar rcs libmylib.a mylib.o |
Link command example | gcc main.c -L. -lmylib -static -o prog_static | gcc main.c -L. -lmylib -o prog_dynamic | gcc main.c -L. -lmylib -o prog_mixed |
libc linking | static (inside exe) | dynamic (libc.so.6) | dynamic (libc.so.6) |
ldd output | not a dynamic executable | lists libmylib.so, libc.so.6 | lists libc.so.6 |
Executable size | larger | smaller | medium |
Practical tips and common pitfalls
When building .so, always compile with -fPIC for position-independent code on most architectures. Omit -fPIC only for special cases where you know it is safe.
If a dynamically linked executable cannot find a .so at runtime, you will see an error like:
Code
error while loading shared libraries: libmylib.so: cannot open shared object file: No such file or directory
Fix by placing the .so in a standard library path, using LD_LIBRARY_PATH, or configuring /etc/ld.so.conf.d/ and running ldconfig.
Static linking of glibc (-static) can cause portability issues across different kernel versions and glibc expectations; prefer dynamic libc unless you have a strong reason to statically link everything.
Final concise checklist (commands you will use)
Compile object: gcc -c mylib.c -o mylib.o
Create static lib: ar rcs libmylib.a mylib.o
Create shared lib: gcc -fPIC -c mylib.c -o mylib.o then gcc -shared -o libmylib.so mylib.o
Link static (all static): gcc main.c -L. -lmylib -static -o prog_static
Link dynamic (shared lib): gcc main.c -L. -lmylib -o prog_dynamic
Link mixed (static custom, dynamic libc): gcc main.c -L. -lmylib -o prog_mixed
Inspect dynamic deps: ldd prog_dynamic
Inspect symbols: nm libmylib.a or nm -D libmylib.so
Linker verbose: gcc ... -Wl,-v
This single consolidated output contains the code, the commands for static/dynamic/mixed linking, the expected linker/runtime outputs, and the inspection commands you will use to verify what the linker did.
Comments