Static Library vs Dynamic Library
When your program calls a function that lives in another file, the linker has to find it. Libraries are how we package reusable code, and there are two types: static (.a) and dynamic/shared (.so). This post covers how each is built, the difference between the two, and of course how it works.
Overview
Static (.a) | Dynamic (.so) | |
|---|---|---|
| Windows equivalent | .lib | .dll |
| Linked at | build time | load time (or run time) |
| Code ends up | copied into the executable | shared, mapped from the .so file |
| Executable size | larger | smaller |
| Update a library | must relink the app | replace the .so (if ABI compatible) |
| Needs library on target | no | yes |
Example
Examples to get a better understanding:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* mathlib.h */
int add(int a, int b);
/* mathlib.c */
#include "mathlib.h"
int add(int a, int b) {
return a + b;
}
/* main.c */
#include <stdio.h>
#include "mathlib.h"
int main(void) {
printf("%d\n", add(2, 3));
return 0;
}
Static Library
A static library is just an archive of object files made by ar.
1
2
3
gcc -c mathlib.c -o mathlib.o
ar rcs libmath.a mathlib.o # create archive, this only contain 1 .o file
gcc main.c -L. -lmath -o app_static
-L.tells the linker where to search(. is the current directory),-lmathmeanslibmath.a(or.so).- The linker pulls only the object files that are actually referenced, and copies them into
app_static. (hence the application would be bigger) - The result is self-contained: no library needed at run time.
Inspect it:
1
2
3
4
5
$ ar t libmath.a # list members
mathlib.o
$ nm libmath.a # inspect symbols
mathlib.o:
0000000000000000 T add
Dynamic (Shared) Library
Shared objects must be built as position-independent code (-fPIC) so that they can be mapped at any address.
1
2
3
gcc -fPIC -c mathlib.c -o mathlib.o #compile to object file
gcc -shared -o libmath.so mathlib.o #link object into shared lib
gcc main.c -L. -lmath -o app_dynamic # build app with shared lib
The executable only stores a reference to libmath.so. At startup the dynamic linker (ld-linux.so) finds and maps it:
1
2
3
4
5
$ ./app_dynamic
error while loading shared libraries: libmath.so: cannot open shared object file
$ LD_LIBRARY_PATH=. ./app_dynamic # works
5
Ways to tell the loader where to look:
LD_LIBRARY_PATH- quick, for development only.- Install into
/usr/libor/usr/local/liband runsudo ldconfig.
Inspect it:
1
2
3
ldd app_dynamic # which .so files it needs
readelf -d app_dynamic | grep NEEDED
nm -D libmath.so # exported symbols
Versioning and soname
Convention for shared libraries use a symlink chain:
1
2
3
libmath.so -> libmath.so.1 (used by the linker at build time)
libmath.so.1 -> libmath.so.1.0.0 (soname, used by the loader at run time)
libmath.so.1.0.0 (the real file)
1
gcc -shared -Wl,-soname,libmath.so.1 -o libmath.so.1.0.0 mathlib.o
Changing the Library: Rebuild vs Replace
The real difference shows up when the library implementation changes. Build the same main.c both ways, keeping the libraries in separate folders:
1
2
3
4
5
6
7
8
9
10
11
mkdir -p s d
gcc -fPIC -c mathlib.c
ar rcs s/libmath.a mathlib.o # static lib
gcc -shared -o d/libmath.so mathlib.o # dynamic lib
gcc main.c -Ls -l:libmath.a -o app_static
gcc main.c -Ld -lmath -Wl,-rpath,'$ORIGIN/d' -o app_dynamic
$ ./app_static; ./app_dynamic
add(2, 3) = 5
add(2, 3) = 5
Now change the implementation in mathlib.c (here a + b becomes a * b) and rebuild only the library:
1
2
3
4
5
6
sed -i 's/a + b/a * b/' mathlib.c
gcc -fPIC -shared -o d/libmath.so mathlib.c # new .so, apps untouched
$ ./app_static; ./app_dynamic
add(2, 3) = 5 # static: still the old code, it was copied into the binary
add(2, 3) = 6 # dynamic: new code, loaded from the new .so at startup
The static app only picks up the change after the library and the app are rebuilt:
1
2
3
4
5
gcc -c mathlib.c && ar rcs s/libmath.a mathlib.o
gcc main.c -Ls -l:libmath.a -o app_static # relink required
$ ./app_static
add(2, 3) = 6
This is the whole trade-off: with a .so you ship one file and every app gets the fix on its next start, with a .a you must relink and redeploy every app.
Runtime Risks of Dynamic Libraries
Because linking is finished at run time, a dynamic app can build fine and still fail on the target machine. There are two common cases.
1. Missing library
The loader cannot find the .so the executable asks for, so the app does not even start:
1
2
$ ./app
./app: error while loading shared libraries: libmath.so: cannot open shared object file: No such file or directory
Check with ldd app (look for not found). Typical causes: library not installed, wrong rpath / LD_LIBRARY_PATH, or a different distro or version on the target.
2. ABI break
The .so is replaced by a version whose interface changed, but the app is not rebuilt. Here add gains a third parameter:
1
2
/* v1.c */ int add(int a, int b) { return a + b; }
/* v2.c */ int add(int a, int b, int c) { return a + b + c; }
1
2
3
4
5
$ gcc -fPIC -shared -o libmath.so v1.c && ./app
add(2, 3) = 5
$ gcc -fPIC -shared -o libmath.so v2.c && ./app # app NOT rebuilt
add(2, 3) = -736414659 (unidentified)
How to reduce the risk:
- Use the soname scheme above and bump the major version on any ABI break
- Keep structs opaque (pointer + accessor functions) so their layout can change safely.
- Ship the exact
.soversion you tested with, and check withlddon the target.
With a static library none of this can happen at run time: any mismatch is a compile or link error, found before you ship.
Which One to Choose?
Prefer static when
- Bare-metal / MCU firmware: there is no OS loader, so everything is often linked statically.
- You want a single self-contained binary (containers,
scratchimages, tools shipped to unknown machines). - You want link-time optimization across the library boundary.
Prefer dynamic when
- Many programs share one library (saves disk and RAM).
- You need to patch a library (e.g. a security fix in OpenSSL) without rebuilding every app.