What are Trimming Root Filesystem Libraries- Free Embedded Linux Training

PREV_LEC NEXT_LEC

Trimming Root Filesystem Libraries
A free embedded linux course lecture on shared library discovery and stripping
readelf
strip
Smaller Image

Every dynamically linked program on your target needs its runtime libraries present on the root filesystem, but copying an entire glibc tree onto a device with a few megabytes of flash is rarely the right answer. This lecture in our free embedded linux course walks through exactly how to discover which shared libraries a given binary actually needs, how to copy only those libraries and their symlink chains correctly, and how to strip debug symbols to reclaim meaningful space, all using an original example binary built specifically for this walkthrough.

free linux kernel development course
free linux device drivers course
free embedded systems course
free embedded linux course
shared libraries
strip
readelf

What You Will Learn

  • Why static linking everything is usually the wrong tradeoff for embedded targets
  • How to use readelf to discover a binary’s exact shared library dependencies
  • How to locate and copy those libraries, including their symlink chains, correctly
  • How stripping reduces binary and library size, and what you lose by doing it
  • How to avoid the classic dlopen()-loaded plugin trap

Prerequisites

  • A cross toolchain with sysroot, from our free embedded linux course toolchain chapter
  • A staging root filesystem directory containing at least one compiled program
  • Comfort with basic shell commands (cd, cp, ls)

Static Linking vs. Copying Shared Libraries

There are really only two honest options for handling libraries on an embedded root filesystem. You can link every program statically, which means each executable carries its own private copy of every library function it calls, resulting in zero runtime library dependencies but noticeably larger individual binaries. Or you can link dynamically and ship a copy of the shared libraries your programs actually need, which keeps individual binaries small but requires you to manage a library tree on the target.

For a device running a single statically-linked program, static linking can genuinely be the simpler and smaller overall choice. The moment you have two or more dynamically linked programs sharing common library code, such as libc, dynamic linking wins on total image size, because the shared library is stored once rather than duplicated inside every binary.

Discovering Dependencies With readelf

Rather than guessing which libraries a program needs, or copying everything “just in case,” use readelf to ask the binary directly. Below we compile a small original example program, ep_netcheck, that intentionally links against libm for a floating-point calculation, then inspect its dependencies.

/* ep_netcheck.c - tiny example linked against libm, for EmbeddedPathashala */
#include <stdio.h>
#include <math.h>

int main(void)
{
    double latency_ms = 12.5;
    double jitter = sqrt(latency_ms);
    printf("ep_netcheck: estimated jitter bound = %.2f\n", jitter);
    return 0;
}
$ arm-linux-gnueabihf-gcc -o ep_netcheck ep_netcheck.c -lm
$ arm-linux-gnueabihf-readelf -a ep_netcheck | grep "program interpreter"
      [Requesting program interpreter: /lib/ld-linux-armhf.so.3]
$ arm-linux-gnueabihf-readelf -a ep_netcheck | grep "Shared library"
0x00000001 (NEEDED)   Shared library: [libm.so.6]
0x00000001 (NEEDED)   Shared library: [libc.so.6]

Two pieces of information matter here. The program interpreter line tells you which dynamic linker/loader your target needs present at a fixed path — without it, nothing dynamically linked will run at all. The Shared library (NEEDED) lines tell you exactly which .so files must exist on the target for this specific binary to run. A faster day-to-day alternative once your toolchain’s ldd works against target sysroot binaries is ldd ep_netcheck, which reports the same information in a more readable form, though readelf is the more reliable ground truth and works even when ldd itself cannot execute the target’s architecture.

Dependency Discovery to Staging Directory
ep_netcheck
|
v readelf -a | grep NEEDED
libm.so.6, libc.so.6
|
v find in $SYSROOT/lib
copy with cp -a (preserves symlinks)
|
v
rootfs/lib/libm.so.6 -> libm-x.y.so
rootfs/lib/libc.so.6 -> libc-x.y.so

Copying Libraries Without Breaking Symlinks

Toolchain sysroots almost always store libraries as a real file plus a versioned symlink pointing to it, for example libc.so.6 -> libc-2.38.so. If you copy the symlink with a plain cp, you may end up duplicating the target file’s contents into a new, disconnected file instead of preserving the link — silently doubling your storage use and breaking future updates. Always use cp -a, which preserves symbolic links, permissions, and timestamps.

$ export SYSROOT=$(arm-linux-gnueabihf-gcc -print-sysroot)
$ ls -l $SYSROOT/lib/libm.so.6
lrwxrwxrwx 1 ravi ravi 12 Aug 13 09:00 libm.so.6 -> libm-2.38.so
$ cd ~/rootfs
$ cp -a $SYSROOT/lib/libm.so.6 lib/
$ cp -a $SYSROOT/lib/libm-2.38.so lib/
$ cp -a $SYSROOT/lib/libc.so.6 lib/
$ cp -a $SYSROOT/lib/libc-2.38.so lib/
$ cp -a $SYSROOT/lib/ld-linux-armhf.so.3 lib/
$ cp -a $SYSROOT/lib/ld-2.38.so lib/
$ ls -l rootfs/lib
lrwxrwxrwx 1 ravi ravi 12 Aug 13 09:05 libm.so.6 -> libm-2.38.so
-rwxr-xr-x 1 ravi ravi 812345 Aug 13 09:05 libm-2.38.so
lrwxrwxrwx 1 ravi ravi 12 Aug 13 09:05 libc.so.6 -> libc-2.38.so
-rwxr-xr-x 1 ravi ravi 1904212 Aug 13 09:05 libc-2.38.so

Repeat this readelf-then-copy cycle for every dynamically linked program you place on the root filesystem. Once you have done it a handful of times, it is straightforward to script as part of your image build.

The dlopen() Trap

readelf only reports libraries recorded in the binary’s dynamic section at link time. Some libraries — most commonly NSS (Name Service Switch) modules used for DNS and user/group lookups, and various plugin architectures — are instead loaded at runtime through dlopen(3) based on configuration files, not static link records. These will not show up in your NEEDED list at all, and their absence on the target often only surfaces as a confusing runtime failure, such as hostname lookups silently failing. When your target needs networking, name resolution, or any plugin-based library feature, budget time to identify and copy these separately; a quick way to catch them is to actually exercise that code path on the target and watch for library-not-found errors, rather than trusting readelf alone.

Reducing Size by Stripping

Libraries and executables built with debug information, especially with -g, carry a symbol table and debug sections that are entirely unnecessary on a production target. Stripping removes this information after the fact, without needing to recompile.

$ file rootfs/lib/libc-2.38.so
rootfs/lib/libc-2.38.so: ELF 32-bit LSB shared object, ARM, ...
not stripped
$ ls -l rootfs/lib/libc-2.38.so
-rwxr-xr-x 1 ravi ravi 1904212 Aug 13 09:05 rootfs/lib/libc-2.38.so
$ arm-linux-gnueabihf-strip rootfs/lib/libc-2.38.so
$ file rootfs/lib/libc-2.38.so
rootfs/lib/libc-2.38.so: ELF 32-bit LSB shared object, ARM, ...
stripped
$ ls -l rootfs/lib/libc-2.38.so
-rwxr-xr-x 1 ravi ravi 1523980 Aug 13 09:06 rootfs/lib/libc-2.38.so

In this example stripping saved roughly 380 KB on a single library, around 20 percent — savings that repeat across every library and binary on the root filesystem. Kernel modules can be stripped too, but need a slightly different flag set to preserve the sections the module loader relies on:

$ arm-linux-gnueabihf-strip --strip-debug my_module.ko

Using --strip-debug rather than a full strip on kernel modules keeps section information the module loader needs while still discarding the bulky debug data.

Real-World Use Cases

  • Shrinking a root filesystem image to fit a fixed NOR/NAND flash partition size
  • Speeding up OTA update transfer time by minimizing image delta size
  • Meeting a hard boot-partition size budget set by a bootloader configuration
  • Reducing the attack surface by shipping only the libraries actually exercised on target

Common Mistakes and Troubleshooting

  • Plain cp instead of cp -a: silently converts symlinked libraries into duplicated real files, wasting space and breaking version updates.
  • Stripping before debugging is finished: once a binary is stripped, backtraces and gdb symbol resolution stop working; keep an unstripped copy on your build host for debugging.
  • Missing the dynamic linker itself: forgetting to copy ld-linux*.so means nothing dynamically linked will execute at all, producing a cryptic “No such file or directory” even though the file clearly exists.
  • Assuming readelf output is exhaustive: dlopen()-loaded plugins like NSS modules will not appear in the NEEDED list; test the actual code paths on target.

Best Practices

  • Script the readelf-discover-and-copy workflow so it runs consistently for every build, not manually per release
  • Keep a build-host copy of every binary and library unstripped for offline debugging
  • Strip on the target-bound copy only, never your working build tree
  • Track total root filesystem size over time so unexpected growth is caught early

Performance Considerations

Smaller shared libraries mean less data to read from flash at process startup and less RAM consumed by unused debug sections when the library is mapped into memory, which can measurably improve cold-boot and process-launch latency on slower embedded storage.

Security Considerations

Stripped binaries also give an attacker slightly less information to work with during reverse engineering, though this should never be relied on as a real security control on its own. More importantly, shipping fewer libraries overall — by carefully auditing real dependencies instead of copying “everything that might be needed” — genuinely reduces your patchable attack surface.

Summary and Key Takeaways

  • Use readelf to discover exact NEEDED shared library dependencies per binary
  • Always copy with cp -a to preserve versioned symlink chains correctly
  • Watch for dlopen()-loaded plugins that readelf will not report
  • Strip debug symbols from target-bound copies to reclaim meaningful flash space

Conclusion

Trimming a root filesystem down to exactly the libraries it needs, and stripping the debug fat out of what remains, is one of those unglamorous steps that separates a genuinely production-ready embedded Linux image from a bloated development one. Combined with the multi-call binary approach from the previous lecture, you now have both halves of the size-reduction picture for this free linux kernel development course track: fewer executables through applet sharing, and leaner libraries through careful dependency discovery and stripping.

FAQ

How do I find which shared libraries a binary needs?

Run readelf -a <binary> | grep "Shared library" to list every NEEDED entry, or use ldd for a more readable summary.

Why use cp -a instead of cp when copying libraries?

Because cp -a preserves symbolic links, so versioned library symlinks stay intact instead of being duplicated into separate real files.

Will readelf show me every library my program actually loads?

No — libraries loaded at runtime via dlopen(3), such as NSS modules, will not appear in the static NEEDED list and must be identified separately.

Is it safe to strip every library on my root filesystem?

Generally yes for production images, as long as you keep an unstripped copy on your build host for debugging with gdb.

What is the difference between strip and strip –strip-debug on kernel modules?

A full strip can remove sections the kernel module loader needs; --strip-debug removes only debug information while preserving those required sections.

Should I statically link instead of managing shared libraries?

Static linking can be simpler for a single-binary target, but dynamic linking is usually smaller overall once more than one program shares common libraries like libc.

What happens if I forget to copy the dynamic linker itself?

No dynamically linked program will execute; you will typically see a confusing “No such file or directory” error even though the binary file is present.

Continue Your Free Embedded Linux Course

You have now covered multi-call binaries and library trimming for a lean root filesystem.

 

PREV_LEC NEXT_LEC

Leave a Reply

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