Understanding the C Library Parts-Free Embedded Linux Training


PREV_LEC | NEXT_LEC

Understanding the C Library Parts
Why libc, libm, libpthread, and librt aren’t one library

Every embedded Linux binary you cross-compile links against “the C library” — but that phrase hides an important detail. In this lecture of our free embedded linux course, you’ll learn that the C library your toolchain ships is actually split into several distinct pieces, each implementing a different slice of the POSIX API, and why that split still matters when you’re sizing a root filesystem or diagnosing an undefined-reference linker error.

C library components
glibc
POSIX threads
shared libraries
free linux kernel development course

What You Will Learn

  • Why the C library is implemented as several separate shared objects, not one monolith
  • What libc, libm, libpthread, and librt each provide
  • How modern glibc changed this split, and what it means for your build
  • How to see, at the binary level, exactly which of these libraries your program actually needs
  • Common linker errors caused by missing one of these libraries and how to fix them

Prerequisites

This lecture assumes you can already cross-compile and link a simple C program, and that you’re comfortable with the sysroot layout covered in the previous lecture of this free linux development course series.

Why POSIX Isn’t One Library

The POSIX specification defines a huge surface area: string handling, math functions, threading, real-time signal and memory operations, and much more. When glibc (and its lighter alternatives like musl) was originally designed, implementers split this surface into separate shared objects rather than one giant library. The historical reasoning was straightforward: not every program needs threading or real-time extensions, and a smaller default footprint meant faster loading and less memory pressure on constrained targets — a genuinely important concern in embedded work.

The Four Traditional Components

Library Provides Typical link flag
libc Core POSIX functions — printf, open, read, write, malloc, string functions Linked automatically, always
libm Math functions — cos, exp, log, sqrt, pow -lm
libpthread POSIX threading — every function starting with pthread_ -lpthread (legacy)
librt Real-time extensions — POSIX shared memory, message queues, asynchronous I/O, high-resolution timers -lrt (legacy)

Why This Matters: A Practical Example

Here’s a small original demo that uses functions from three of these areas at once — math, threading, and a POSIX timer:

/* ep_libc_demo.c */
#include <stdio.h>
#include <math.h>
#include <pthread.h>
#include <time.h>

static void *ep_worker(void *arg)
{
    double result = sqrt(2.0) * M_PI;
    printf("ep_worker: computed %f\n", result);
    return NULL;
}

int main(void)
{
    pthread_t tid;
    struct timespec ts;

    clock_gettime(CLOCK_MONOTONIC, &ts);
    printf("ep_libc_demo: start at %ld.%09ld\n", ts.tv_sec, ts.tv_nsec);

    pthread_create(&tid, NULL, ep_worker, NULL);
    pthread_join(tid, NULL);

    return 0;
}

Building this on an older toolchain requires explicitly linking each piece:

$ aarch64-none-linux-gnu-gcc ep_libc_demo.c -o ep_libc_demo -lpthread -lm -lrt

Expected output:

$ ./ep_libc_demo
ep_libc_demo: start at 481932.204551200
ep_worker: computed 4.442883
How a Binary Pulls In Multiple C Library Pieces
ep_libc_demo.c
|
| uses printf/clock_gettime -> libc
| uses sqrt, M_PI -> libm
| uses pthread_create/join -> libpthread
|
v
gcc -lpthread -lm -lrt ep_libc_demo.c -o ep_libc_demo
|
v
ep_libc_demo (ELF binary)
dynamic section lists: libc.so.6, libm.so.6, libpthread.so.0
loaded at runtime by ld-linux, resolved from the sysroot’s lib/

The Modern glibc Change You Need to Know

On current glibc releases (2.34 and newer, which ship with essentially every actively maintained embedded distro toolchain today), the pthread and real-time functions have been merged directly into libc.so itself. This means on a modern toolchain, the same demo above links and runs correctly with no -lpthread or -lrt flag at all:

$ aarch64-none-linux-gnu-gcc ep_libc_demo.c -o ep_libc_demo -lm

The -lpthread and -lrt flags still work on modern glibc — they now resolve to empty stub libraries kept purely for backward compatibility with older build scripts, so you won’t get an error, but they’re no longer strictly necessary. -lm for math functions remains a separate shared object even on current glibc, so don’t drop that one.

musl libc, a common choice for smaller embedded root filesystems, has always combined everything into a single libc.so, so this distinction never applied there in the first place.

Inspecting What a Binary Actually Needs

Rather than guessing which libraries a compiled binary depends on, ask the binary itself using tools from the previous lecture’s toolchain suite:

$ aarch64-none-linux-gnu-readelf -d ep_libc_demo | grep NEEDED
 0x0000000000000001 (NEEDED)             Shared library: [libm.so.6]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]

On the target device, ldd shows the same information resolved against the actual runtime libraries installed there:

$ ldd ep_libc_demo
    linux-vdso.so.1 (0x0000ffff8a1b0000)
    libm.so.6 => /lib/libm.so.6 (0x0000ffff89f30000)
    libc.so.6 => /lib/libc.so.6 (0x0000ffff89d90000)
    /lib/ld-linux-aarch64.so.1 (0x0000ffff8a180000)

Real-World Use Cases

  • Minimal root filesystem builds: knowing exactly which shared objects a binary needs lets you avoid shipping libraries your image never uses.
  • Diagnosing “undefined reference” errors: a missing -lm or an outdated -lpthread assumption is one of the most common first-week cross-compilation errors.
  • Choosing between glibc and musl: understanding this historical split explains why musl-based images are often smaller — there’s less duplicated infrastructure to carry.

Common Mistakes and Troubleshooting

  • Copy-pasting old link flags forever: -lpthread -lrt from a ten-year-old Makefile still works on modern glibc, but relying on stale flags without understanding why can mask real linking problems on other libc implementations like musl.
  • Forgetting -lm: unlike pthread/rt, math functions are still a separate shared object on every current glibc release — omitting it produces “undefined reference to `sqrt'” style errors.
  • Assuming target and host libc versions match: always link against the sysroot’s libc, never your development host’s — mismatched glibc versions cause binaries that fail to load on the target with cryptic GLIBC_2.xx not found errors.

Best Practices

  • Use readelf -d or ldd to verify actual dependencies rather than assuming from memory which library a function lives in.
  • Keep explicit -lm, -lpthread, and -lrt flags in build systems even on modern glibc — they’re harmless and keep the build portable to musl or older glibc targets.
  • When space is tight, evaluate musl libc as an alternative sysroot — its single-library design avoids the historical fragmentation entirely.

Performance Considerations

Because pthread and rt symbols are now resident in libc.so on modern glibc, there’s no longer a separate library-load cost for threaded or real-time programs — one less shared object for the dynamic loader to resolve at program startup, which shaves a small but measurable amount off cold-start time on resource-constrained boards.

Summary and Key Takeaways

  • The C library historically split POSIX functionality across libc, libm, libpthread, and librt.
  • Modern glibc (2.34+) merged pthread and real-time functions into libc.so directly; libm remains separate.
  • musl libc has always kept everything in one library.
  • readelf -d and ldd tell you exactly what a given binary needs — never guess.

Conclusion

Knowing how the C library is actually organized — rather than treating it as one opaque blob — helps you write correct build rules, debug linker errors quickly, and make informed decisions about root filesystem size. It’s a small piece of knowledge that pays off constantly across the rest of this free linux device drivers course track, especially once you start writing userspace tools that talk directly to kernel drivers.

Frequently Asked Questions

Do I still need -lpthread on modern Linux systems?

On glibc 2.34 and newer, pthread functions live in libc.so itself, so -lpthread is no longer required — though it still works harmlessly as a compatibility stub.

Why is libm still separate when libpthread isn’t?

Math functions were never merged into libc.so; -lm remains necessary on every current glibc release for functions like sqrt, cos, and pow.

Does musl libc have the same split as glibc?

No, musl has always bundled all of this functionality into a single libc.so, so the historical libpthread/librt split never applied to musl-based systems.

How can I check which shared libraries my binary actually needs?

Use readelf -d binary | grep NEEDED on your development host, or ldd binary directly on the target device.

What happens if I link against the wrong glibc version?

You’ll typically see a runtime error like “GLIBC_2.xx not found” when the target’s libc is older than what the binary was linked against — always link against your sysroot’s libc, not the host’s.

Is it safe to remove -lpthread and -lrt from my Makefile?

It’s safe on modern glibc targets, but keeping them costs nothing and preserves portability to musl or older glibc-based toolchains.

Continue the Free Embedded Linux Course

More toolchain, driver, and kernel lectures are coming — all free, all original.

PREV_LEC | NEXT_LEC

 

Leave a Reply

Your email address will not be published. Required fields are marked *