Every program you run on Linux — whether written in C, Python, or Java — eventually calls into the C library to reach the kernel. Picking the right one is one of the most consequential decisions in this free embedded linux course, because it affects your binary size, your boot time, your standards compliance, and how much pain you’ll have porting third-party code later.
This lecture walks through the three C libraries you’ll actually encounter on modern embedded Linux — glibc, musl, and uClibc-ng — and gives you a clear decision path instead of vague “it depends” advice.
musl libc
uclibc-ng
posix compliance
static linking
free embedded linux course
What You Will Learn
- What the C library actually does between your application and the kernel
- The current state of glibc, musl, and uClibc-ng (not the outdated eglibc-era picture)
- A practical decision tree for choosing one on a real project
- How to check which C library a prebuilt toolchain or binary is linked against
- An original demo comparing binary size across libraries
Prerequisites
You should understand what a system call is and be comfortable running basic cross-compilation commands, both covered earlier in this free embedded linux course.
The C Library’s Job
The kernel exposes a raw system call interface — numbered, low-level entry points for things like reading a file or allocating memory. Almost nobody calls these directly. Instead, the C library wraps them in a friendly, standardized POSIX API: fopen(), malloc(), printf(). Any language runtime — Python’s interpreter, the JVM, Go’s early runtime versions — ultimately funnels down through this same layer to reach the kernel.
| Application |
+——+——+
| malloc(), open(), printf()…
v
+————-+
| C Library | <– glibc / musl / uClibc-ng
+——+——+
| syscall numbers
v
+————-+
| Linux Kernel|
+————-+
You can bypass the C library and issue raw syscall() instructions directly, and some highly specialized bootstrap code does exactly that, but for essentially all application and driver-adjacent userspace work it’s unnecessary trouble.
The Three Real Options Today
glibc
The standard GNU C library and the most complete POSIX implementation available. It is what desktop and server Linux uses, and it remains the safest default whenever storage and RAM are not tightly constrained. Historically there was a fork called eglibc, created to add configurability and support for architectures glibc didn’t cover well at the time; that fork’s improvements were merged back into mainline glibc years ago, and eglibc itself is no longer maintained or relevant — if you see it mentioned in older material, mentally read it as “glibc.”
musl libc
A from-scratch, standards-focused C library built specifically with small size, static-linking friendliness, and correctness in mind. It has become the default choice for lightweight container base images (notably Alpine Linux) and is a strong fit for embedded targets that want a modern, actively maintained, POSIX-conformant library without glibc’s footprint. musl deliberately declines to implement some legacy glibc extensions, which occasionally trips up older third-party code that leans on non-standard glibc behavior.
uClibc-ng
The maintained continuation of the original uClibc project (whose name comes from the Greek “mu,” for micro-controller). It was built for uClinux — Linux running on MMU-less CPUs — and has since been adapted for full MMU-based Linux too. It ships a menuconfig-style configuration tool so you can strip out exactly the POSIX features you don’t need, producing very small footprints, at the cost of being a less complete standards implementation than glibc or musl.
Comparing the Three
| Library | Typical Size | POSIX Completeness | Best Fit |
|---|---|---|---|
| glibc | Largest | Most complete | General-purpose targets with adequate storage/RAM |
| musl | Small | High, standards-strict | Modern embedded/container targets, static binaries |
| uClibc-ng | Smallest (configurable) | Reduced, configurable | Severely storage/RAM-constrained, MMU-less (uClinux) targets |
A Practical Decision Tree
|
no
v
Storage/RAM extremely tight? — yes —> uClibc-ng or musl (benchmark both)
|
no
v
Need smallest modern, POSIX-strict libc? — yes –> musl
|
no
v
Default: glibc (best compatibility, most third-party code “just works”)
Checking What a Toolchain or Binary Uses
# Which C library is a native toolchain linked against?
$ ldd --version | head -1
ldd (Ubuntu GLIBC 2.39-0ubuntu8) 2.39
# Which shared libc does an existing binary need?
$ file ./my_app
my_app: ELF 64-bit LSB pie executable, x86-64, dynamically linked,
interpreter /lib/ld-musl-x86_64.so.1, ...
# For a cross toolchain, the sysroot's dynamic linker name tells you too
$ find /opt/toolchain/arm-*/sysroot -name 'ld-*.so*'
/opt/toolchain/arm-linux-gnueabihf/sysroot/lib/ld-linux-armhf.so.3
The interpreter path (ld-linux-armhf.so.3 vs ld-musl-armhf.so.1 vs ld-uClibc.so.0) is the fastest tell for which library a prebuilt binary or toolchain targets.
Original Example: Measuring the Size Difference
Here’s a small original C program, ep_hello.c, and a script that builds it against whichever libc-backed toolchains you have installed, so you can see the size difference for yourself instead of trusting a table.
/* ep_hello.c - minimal static binary for libc size comparison */
#include <stdio.h>
int main(void) {
printf("ep_hello: linked and running\n");
return 0;
}
#!/usr/bin/env bash
# ep_libc_size_compare.sh - build ep_hello.c statically against
# every cross-compiler prefix passed as an argument, and report size.
set -euo pipefail
SRC="ep_hello.c"
for CC in "$@"; do
OUT="ep_hello_$(basename "$CC")"
"$CC" -static -O2 -o "$OUT" "$SRC"
SIZE=$(stat -c%s "$OUT")
echo "$CC -> $OUT : ${SIZE} bytes"
done
$ ./ep_libc_size_compare.sh arm-linux-gnueabihf-gcc arm-linux-musleabihf-gcc
arm-linux-gnueabihf-gcc -> ep_hello_arm-linux-gnueabihf-gcc : 719312 bytes
arm-linux-musleabihf-gcc -> ep_hello_arm-linux-musleabihf-gcc : 20144 bytes
Numbers will vary by toolchain version, but the pattern — glibc’s static binaries running many times larger than musl’s — holds consistently, and is exactly why musl dominates size-sensitive modern embedded and container use cases.
Real-World Use Cases
- Industrial gateway with 256 MB flash running a full Linux distribution with package management — glibc, no contest, since compatibility with off-the-shelf packages matters more than a few extra megabytes.
- Battery-powered sensor node with 8 MB flash running a handful of statically-linked binaries — musl, for small static binaries with strict POSIX behavior.
- Legacy MMU-less microcontroller running uClinux — uClibc-ng is close to your only realistic option.
Common Mistakes and Troubleshooting
- Assuming all C libraries are drop-in compatible — code relying on glibc-only extensions (certain non-standard
*_gnufunctions) can fail to compile or misbehave under musl. - Mixing libraries across a build — never link objects built against different C libraries into one binary.
- Trusting old references to eglibc — it’s dead; any modern toolchain description mentioning it can be read as glibc.
- Ignoring locale/NSS limitations in musl — musl’s locale and name-service-switch support is intentionally minimal; verify early if your application depends on either.
Best Practices
- Benchmark real firmware size on your actual application, not just a “hello world,” before committing to musl or uClibc-ng purely for size.
- Default to glibc unless you have a concrete, measured reason to move away from it.
- Pin your C library choice early — switching mid-project means rebuilding your entire toolchain and root filesystem.
Performance Considerations
musl’s simpler internals (notably its allocator) can behave differently under heavy multi-threaded allocation workloads compared to glibc’s more elaborate arena-based allocator — profile before assuming either is faster for your specific workload.
Summary and Key Takeaways
- The C library is the mandatory gateway between your application and the kernel’s system call interface.
- glibc = most complete and compatible; musl = small, modern, POSIX-strict; uClibc-ng = smallest and configurable, best for uClinux or extreme constraints.
- eglibc is dead and merged into glibc — ignore it in modern decisions.
- Check the dynamic linker interpreter path to identify what any existing binary or toolchain targets.
Conclusion
There is no universally “correct” C library — only the right trade-off for your target’s storage, RAM, and compatibility requirements. Most projects in this free embedded linux course will do fine on glibc by default; reach for musl when size and modern POSIX strictness matter, and drop to uClibc-ng only when you’re on genuinely constrained or MMU-less hardware.
FAQ
Is eglibc still a valid choice today?
No. Its improvements were merged back into glibc years ago and eglibc itself is unmaintained — treat any reference to it as glibc.
Can I switch a running project from glibc to musl later?
Technically yes, but it means rebuilding your entire toolchain, root filesystem, and re-testing every dependency, so it’s best decided early.
Why is musl popular in containers if this is an embedded course?
Containers and embedded systems share the same underlying goal — small, predictable, statically-friendly binaries — so the same library serves both well.
Does uClibc-ng work on modern MMU-based ARM boards?
Yes, it has been adapted to work on full Linux with an MMU, not just uClinux, though it remains most associated with MMU-less targets.
How do I tell which libc a prebuilt binary needs?
Check its dynamic linker interpreter path with the file command or readelf — for example ld-musl-*.so.1 versus ld-linux-*.so.
Is glibc always the safest default?
For general-purpose targets with adequate storage, yes — it has the broadest compatibility with existing open-source packages.
Ready to Find a Toolchain?
Continue this free embedded linux course with the next lecture on sourcing or building a cross toolchain.
