Static vs Dynamic Linking Explained
Understand how a cross toolchain binds your embedded Linux application to its libraries — and why that choice changes your binary size, boot behavior, and update strategy.
cross toolchain
gcc -static
shared libraries
free embedded systems course
free linux device drivers course
If you have ever wondered why one Linux binary is 6 KB and another that does almost the same thing is 600 KB, the answer is almost always static and dynamic linking. This lecture is part of EmbeddedPathashala’s free embedded systems course on cross toolchains, and it walks you through exactly how your cross compiler decides where library code lives — inside your executable, or out on the target filesystem — and why that decision matters for a resource-constrained board just as much as it matters for a desktop.
By the end of this lecture you will be able to look at any Linux binary and know, without guessing, whether it needs a runtime linker, which shared objects it depends on, and how to force either linking strategy on purpose.
What You Will Learn
- Why every C or C++ program on Linux links against libc automatically
- The difference between static and dynamic linking at the object-file level
- How to build and inspect a static library with a cross toolchain
- How to build and inspect a position-independent shared library
- How to read
readelfoutput to see exactly what a binary depends on - When to choose static linking for an embedded target, and when to avoid it
Prerequisites
- A working cross toolchain on your PATH (see Lecture 8, “Using Your New Cross Toolchain”)
- Basic familiarity with compiling a single C file with gcc
- A Linux host — any recent distribution is fine
Every Program Links Against libc — Whether You Ask or Not
On Linux, the C library (glibc, musl, or uClibc-ng depending on how you configured your toolchain) is so fundamental that your cross compiler pulls it in automatically. You never have to write -lc on the command line. Every other library, though — libm for math functions, libpthread-style threading symbols in modern glibc, a vendor SDK, or your own code split into a reusable component — has to be named explicitly with the -l flag, where the library name has its lib prefix and file extension stripped off.
-lpthread looks for libpthread.so or libpthread.a
-lep_mathutil looks for libep_mathutil.so or libep_mathutil.a
Two completely different mechanisms can satisfy that -l request, and the toolchain does not care which one you pick unless you tell it to. That choice is the core of this lecture.
Static Linking: Copy the Code In
With static linking, the linker copies the actual machine code for every function your program calls — plus anything those functions themselves depend on — out of a library archive and welds it directly into your final executable. Once the build finishes, that executable is self-contained: it needs nothing else on the target’s filesystem to run those functions.
Static library archives use the extension .a and are built with the ar tool, not the linker directly. Say you have two small original source files that make up a tiny math helper library:
// ep_square.c
int ep_square(int x) {
return x * x;
}
// ep_cube.c
int ep_cube(int x) {
return x * x * x;
}
Turn them into object files, then archive them into a static library:
$ ${CROSS_COMPILE}gcc -c ep_square.c
$ ${CROSS_COMPILE}gcc -c ep_cube.c
$ ${CROSS_COMPILE}ar rc libep_mathutil.a ep_square.o ep_cube.o
$ ls -l
-rw-r--r-- 1 ravi ravi 912 Aug 12 09:10 ep_cube.o
-rw-r--r-- 1 ravi ravi 904 Aug 12 09:10 ep_square.o
-rw-r--r-- 1 ravi ravi 2208 Aug 12 09:10 libep_mathutil.a
Now write a tiny demo that calls both functions and link it statically against your new library:
// ep_democalc.c
#include <stdio.h>
int ep_square(int);
int ep_cube(int);
int main(void) {
printf("square(5) = %d\n", ep_square(5));
printf("cube(5) = %d\n", ep_cube(5));
return 0;
}
$ ${CROSS_COMPILE}gcc ep_democalc.c -L. -lep_mathutil -o ep_democalc
$ ${CROSS_COMPILE}gcc -static ep_democalc.c -L. -lep_mathutil -o ep_democalc-static
$ ls -l ep_democalc ep_democalc-static
-rwxr-xr-x 1 ravi ravi 9832 Aug 12 09:14 ep_democalc
-rwxr-xr-x 1 ravi ravi 712104 Aug 12 09:14 ep_democalc-static
Notice the size jump once -static forces even glibc itself to be pulled into the binary. Static linking is genuinely useful in a few specific embedded situations: an early-boot recovery tool that must run before the root filesystem with your runtime libraries is even mounted, a minimal BusyBox-only rescue image where you would rather avoid shipping the runtime linker at all, or a single-purpose appliance image where you are only ever going to use a handful of libc symbols and don’t want the overhead of the full shared library.
Dynamic Linking: Reference the Code, Resolve It at Runtime
Dynamic (shared) linking takes the opposite approach: your executable stores only a reference to the library and its symbols. The actual code stays in a separate .so file on the target, and a runtime linker locates and loads it the moment your program starts.
To build a shared library, the object code must be position-independent, because the runtime linker is free to place it anywhere in memory. That means compiling with -fPIC, then linking with -shared instead of archiving with ar:
$ ${CROSS_COMPILE}gcc -fPIC -c ep_square.c
$ ${CROSS_COMPILE}gcc -fPIC -c ep_cube.c
$ ${CROSS_COMPILE}gcc -shared -o libep_mathutil.so ep_square.o ep_cube.o
$ ${CROSS_COMPILE}gcc ep_democalc.c -L. -lep_mathutil -o ep_democalc_dyn
This time the -l flag resolves to the shared object, and instead of code being copied in, a dependency reference is recorded. You can prove it with readelf:
$ ${CROSS_COMPILE}readelf -a ep_democalc_dyn | grep "Shared library"
0x00000001 (NEEDED) Shared library: [libep_mathutil.so]
0x00000001 (NEEDED) Shared library: [libc.so.6]
$ ${CROSS_COMPILE}readelf -a ep_democalc_dyn | grep "program interpreter"
[Requesting program interpreter: /lib/ld-linux-aarch64.so.1]
That “program interpreter” line is the runtime linker itself — it must exist on the target filesystem, and it is the piece of software that actually finds libep_mathutil.so at process start, using the default search paths /lib and /usr/lib, or any extra directories you list in the colon-separated LD_LIBRARY_PATH environment variable.
[ ep_democalc-static : contains its own libc + libep_mathutil code ] — runs directlyDynamic binary:
[ ep_democalc_dyn ] –NEEDED–> [ ld-linux.so ] –loads–> [ libc.so.6 ]
–loads–> [ libep_mathutil.so ]
Comparing the Two Approaches
| Aspect | Static Linking | Dynamic Linking |
|---|---|---|
| Binary size | Large — includes all dependency code | Small — only references |
| Startup time | Slightly faster (no resolution step) | Small runtime resolution overhead |
| Memory use with multiple processes | Each process has its own copy | One copy shared across processes |
| Updating a library | Must rebuild every dependent binary | Replace the .so, no rebuild needed |
| Works before rootfs is mounted | Yes | No — needs the runtime linker present |
| Typical embedded use | Recovery tools, minimal rescue images | Normal application images |
Common Mistakes and Troubleshooting
- Forgetting -fPIC: linking without it produces relocation errors when building the shared object on some architectures — always compile shared-library sources with
-fPIC. - Assuming -static links everything: some NSS-based glibc features (like certain hostname resolution paths) still need dynamic loading even in a “static” binary — check your libc’s release notes if you rely on those.
- Library not found at runtime: if
ep_democalc_dynfails with “cannot open shared object file,” your.soisn’t in a directory the runtime linker searches — setLD_LIBRARY_PATHor install it to/usr/libon the target. - Mixing architectures: a shared library built for the wrong target architecture will fail to load with a format error — always double check you’re using the matching
${CROSS_COMPILE}prefix.
Best Practices
- Default to dynamic linking for normal application images — it keeps your rootfs smaller across multiple binaries and simplifies security updates.
- Reach for static linking only for a specific, justified case: early-boot tools, rescue shells, or single-binary appliances.
- Performance: dynamic linking adds a small, usually negligible, startup cost from symbol resolution — profile before assuming it matters on your target.
- Security: shared libraries let you patch a vulnerability (e.g., in libc) once and have every process pick it up on next start, without rebuilding your whole image — a real advantage over static linking at scale.
Summary and Key Takeaways
- libc is always linked automatically; every other library needs an explicit
-lflag. - Static linking copies dependency code directly into your executable via a
.aarchive built withar. - Dynamic linking keeps code in a separate
.so, resolved at runtime by a program interpreter shown inreadelf -aoutput. -fPICplus-sharedbuilds a shared object;-staticforces full static linking.- Choose based on your target’s constraints — boot-time availability of a filesystem, image size budget, and how often you’ll need to patch dependencies.
Static and dynamic linking is one of those cross-toolchain fundamentals that quietly shapes every embedded Linux image you’ll ever build. Once you can read a readelf -a dependency list confidently, you’ll never be surprised by a missing shared library on target again — and you’re ready for the next piece of the puzzle: how Linux handles versioning when a shared library’s interface changes underneath your running programs.
FAQ: Static and Dynamic Linking
What is the main difference between static and dynamic linking?
Static linking copies library code directly into your executable at build time, producing a self-contained but larger binary. Dynamic linking keeps the code in a separate shared object that’s loaded at runtime, producing a smaller binary that depends on that library being present on the target.
Why does gcc always link libc without me asking?
The C library is considered so fundamental to any C or C++ program that gcc and g++ link it in automatically; only additional libraries need an explicit -l flag.
Do I need -fPIC for static libraries too?
No. Position-independent code is only required for shared objects, since the runtime linker must be free to place them anywhere in memory. Static archives don’t need it.
How do I check which shared libraries a binary depends on?
Run readelf -a <binary> | grep "Shared library" on the target’s toolchain-prefixed readelf, which lists every NEEDED entry recorded in the binary.
Is static linking ever a security risk?
Yes — if a statically linked binary embeds a vulnerable version of a library, you must rebuild and redeploy the entire binary to patch it, unlike a dynamically linked one where replacing the shared object is enough.
Can I mix static and dynamic linking in the same program?
Yes. It’s common to statically link a small internal helper library while dynamically linking libc and other system libraries — gcc handles this automatically based on which archive or .so it finds for each -l flag.
What happens if the runtime linker can’t find a shared library?
The program fails to start with an error like “cannot open shared object file.” Fix it by installing the .so to a searched path such as /usr/lib, or by adding its directory to LD_LIBRARY_PATH.
Keep Building Your Toolchain Skills
This lecture is part of EmbeddedPathashala’s free embedded Linux toolchain course — a free linux device drivers course and free linux kernel development course companion, built for engineers who want real depth, not surface-level tutorials.
