What is BusyBox?
{:.gc-basic}
Basic
BusyBox is the “Swiss Army knife of embedded Linux”. It bundles roughly 400 Unix utilities — ls, mount, sh, init, vi, wget, and many more — into a single statically-linkable binary, typically under 1 MB. Every utility is implemented as an applet: a small function registered in a table. At runtime, BusyBox reads argv[0] to decide which applet to run.
The deployment pattern uses symlinks: the installer creates /bin/ls → /bin/busybox, /bin/sh → /bin/busybox, and so on. Calling any symlink causes BusyBox to dispatch to the matching applet.
Size comparison — full GNU utilities vs BusyBox:
| Component | Full GNU toolchain | BusyBox equivalent |
|---|---|---|
coreutils (ls, cp, mv …) |
~8 MB | included in busybox binary |
util-linux (mount, fdisk …) |
~4 MB | included |
procps (ps, top …) |
~500 KB | included |
| bash | ~1.3 MB | sh applet (~150 KB equivalent) |
| Total | ~14 MB | ~500 KB – 2 MB |
A typical embedded rootfs with BusyBox, a cross-compiled libc, and a kernel is 2–5 MB — small enough to live in NOR flash or a 16 MB QSPI chip.
BusyBox’s init is also a fully functional PID 1, reading /etc/inittab and spawning getty/daemons, removing the need for any separate init system.
Building BusyBox
{:.gc-basic}
Basic
Getting the Source
wget https://busybox.net/downloads/busybox-1.36.1.tar.bz2
tar -xjf busybox-1.36.1.tar.bz2
cd busybox-1.36.1
Configure
# Start from a sane default — enables most applets
make defconfig
# Interactive TUI configurator (similar to kernel menuconfig)
make menuconfig
Key menuconfig paths:
- Settings → Cross compiler prefix — set your toolchain prefix
- Settings → Build static binary — enable for fully self-contained binary
- Init Utilities — enable
init,getty,halt,poweroff,reboot - Linux System Utilities —
mount,umount,fdisk,blkid - Networking Utilities —
ifconfig,udhcpc,wget,telnetd
Cross-Compilation for ARM
# Set the cross-compiler prefix in the environment
export ARCH=arm
export CROSS_COMPILE=arm-linux-gnueabihf-
# Or put it in .config directly:
# CONFIG_CROSS_COMPILER_PREFIX="arm-linux-gnueabihf-"
make -j$(nproc)
# Install into _install/ subdirectory
make install
# Result: _install/{bin,sbin,usr/bin,usr/sbin,linuxrc}
Relevant .config Snippet
# CONFIG file fragment — BusyBox 1.36
CONFIG_CROSS_COMPILER_PREFIX="arm-linux-gnueabihf-"
CONFIG_SYSROOT=""
CONFIG_EXTRA_CFLAGS="-march=armv7-a -mfpu=neon -mfloat-abi=hard"
# Static linking: embed libc inside the binary (no .so needed at runtime)
CONFIG_STATIC=y
# Init system
CONFIG_INIT=y
CONFIG_FEATURE_USE_INITTAB=y
CONFIG_GETTY=y
CONFIG_LOGIN=y
# Core shell
CONFIG_ASH=y
CONFIG_ASH_JOB_CONTROL=y
CONFIG_FEATURE_SH_STANDALONE=y
# Key utilities
CONFIG_MOUNT=y
CONFIG_UMOUNT=y
CONFIG_SYSLOGD=y
CONFIG_KLOGD=y
CONFIG_IFCONFIG=y
CONFIG_UDHCPC=y
Static vs Dynamic Linking
| Criterion | Static (CONFIG_STATIC=y) |
Dynamic (shared libc) |
|---|---|---|
| Binary size | Larger (~1–2 MB) | Smaller binary, but needs /lib |
| Runtime dependencies | None | Requires libc.so, ld-linux.so |
| Deployment simplicity | Drop single binary anywhere | Must copy libraries to rootfs |
| Security patching | Rebuild to update libc | Replace .so only |
| Recommended for | Rescue images, initramfs | Full production rootfs |
For production, dynamic linking is preferred so security patches to libc propagate without rebuilding BusyBox.
Minimal RootFS Directory Structure
{:.gc-mid}
Intermediate
Create the Directory Skeleton
ROOTFS=$HOME/rootfs
mkdir -p $ROOTFS
# FHS-required directories
mkdir -p $ROOTFS/{bin,sbin,usr/bin,usr/sbin}
mkdir -p $ROOTFS/{etc,lib,lib64}
mkdir -p $ROOTFS/{dev,proc,sys,tmp,run}
mkdir -p $ROOTFS/{var/log,var/run,var/tmp}
mkdir -p $ROOTFS/home/root
mkdir -p $ROOTFS/mnt
# Correct permissions
chmod 1777 $ROOTFS/tmp
chmod 1777 $ROOTFS/var/tmp
chmod 700 $ROOTFS/home/root
Copy BusyBox
# Copy the binary and the applet symlinks from _install/
cp -a busybox-1.36.1/_install/* $ROOTFS/
Create Essential Device Nodes
These nodes must exist before the kernel hands off to init, because init opens /dev/console on its very first line.
# These need to be created as root (or with fakeroot for images)
sudo mknod -m 600 $ROOTFS/dev/console c 5 1
sudo mknod -m 666 $ROOTFS/dev/null c 1 3
sudo mknod -m 666 $ROOTFS/dev/zero c 1 5
sudo mknod -m 666 $ROOTFS/dev/random c 1 8
sudo mknod -m 666 $ROOTFS/dev/urandom c 1 9
sudo mknod -m 666 $ROOTFS/dev/tty c 5 0
sudo mknod -m 660 $ROOTFS/dev/tty1 c 4 1
# Serial console for embedded targets
sudo mknod -m 660 $ROOTFS/dev/ttyS0 c 4 64
sudo mknod -m 660 $ROOTFS/dev/ttyAMA0 c 204 64 # ARM PL011 UART
devtmpfs vs static /dev: On modern kernels, mounting devtmpfs on /dev at boot lets the kernel automatically populate device nodes. This is the preferred approach — add mount -t devtmpfs devtmpfs /dev to your rcS script. Static nodes are a fallback for kernels without CONFIG_DEVTMPFS.
Copy Cross-Compiled Libraries (Dynamic Linking)
SYSROOT=$(arm-linux-gnueabihf-gcc --print-sysroot)
# Dynamic linker / loader — absolute requirement
cp $SYSROOT/lib/ld-linux-armhf.so.3 $ROOTFS/lib/
# C library
cp $SYSROOT/lib/libc.so.6 $ROOTFS/lib/
cp $SYSROOT/lib/libm.so.6 $ROOTFS/lib/
cp $SYSROOT/lib/libpthread.so.0 $ROOTFS/lib/
# Resolve any additional .so dependencies
arm-linux-gnueabihf-readelf -d busybox-1.36.1/busybox | grep NEEDED
# NEEDED libm.so.6
# NEEDED libc.so.6
/etc/inittab and Init Scripts
{:.gc-mid}
Intermediate
BusyBox’s init is not systemd. It reads /etc/inittab at startup and processes each entry. There are no units, no D-Bus, no cgroups — just a simple table of “when to run what”.
inittab Field Format
id:runlevel:action:process
- id — unique identifier (1–4 characters); for serial-console entries this is the tty device name
- runlevel — ignored by BusyBox init (kept for compatibility); leave blank
- action — one of the keywords below
- process — full command to execute
| Action | Meaning |
|---|---|
sysinit |
Run once at boot, before anything else; init waits for it to finish |
wait |
Run once, init waits for completion |
once |
Run once, init does NOT wait |
respawn |
Restart the process whenever it exits (e.g., getty) |
askfirst |
Like respawn, but prints “Please press Enter to activate” first |
ctrlaltdel |
Run when Ctrl-Alt-Del is pressed |
shutdown |
Run on shutdown/reboot |
restart |
Run when init receives SIGHUP |
Real /etc/inittab Example
# /etc/inittab — BusyBox init
# System initialization
::sysinit:/etc/init.d/rcS
# Start a shell on the serial console (ARM UART)
ttyAMA0::respawn:/sbin/getty -L ttyAMA0 115200 vt100
# Also start a shell on tty1 (framebuffer/VGA)
tty1::respawn:/sbin/getty -L tty1 0 vt100
# If running under QEMU with -nographic, use this instead:
# ::askfirst:/bin/sh
# Handle Ctrl-Alt-Del
::ctrlaltdel:/sbin/reboot
# Cleanup before reboot/halt
::shutdown:/bin/umount -a -r
::shutdown:/sbin/swapoff -a
The /etc/init.d/rcS System Init Script
This script runs as sysinit — BusyBox init blocks until it completes.
#!/bin/sh
# /etc/init.d/rcS — system initialization script
echo "--- Starting rcS ---"
# Mount the kernel virtual filesystems first — many tools need these
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs devtmpfs /dev
# /dev/pts for pseudo-terminals (needed by SSH, screen, etc.)
mkdir -p /dev/pts
mount -t devpts devpts /dev/pts -o gid=5,mode=620
# Shared memory
mkdir -p /dev/shm
mount -t tmpfs tmpfs /dev/shm -o size=32m
# Writable tmpfs for runtime data
mount -t tmpfs tmpfs /run -o size=16m,mode=755
mount -t tmpfs tmpfs /tmp -o size=64m,mode=1777
# Set hostname
hostname -F /etc/hostname
# Bring up loopback interface
ip link set lo up
# Seed the kernel RNG from a saved random seed (optional but good practice)
if [ -f /var/lib/urandom/random-seed ]; then
cat /var/lib/urandom/random-seed > /dev/urandom
fi
dd if=/dev/urandom of=/var/lib/urandom/random-seed bs=512 count=1 2>/dev/null
# Start syslog
/sbin/syslogd -C512 # circular buffer, 512 KB
/sbin/klogd
# Network (static example — replace with udhcpc for DHCP)
# ip addr add 192.168.1.100/24 dev eth0
# ip link set eth0 up
# ip route add default via 192.168.1.1
echo "--- rcS complete ---"
Make it executable:
chmod +x $ROOTFS/etc/init.d/rcS
Additional Required Files
# /etc/hostname
echo "embeddex" > $ROOTFS/etc/hostname
# /etc/passwd — root with no password for development
echo "root::0:0:root:/root:/bin/sh" > $ROOTFS/etc/passwd
# /etc/group
echo "root:x:0:" > $ROOTFS/etc/group
echo "tty:x:5:" >> $ROOTFS/etc/group
# /etc/profile — shell environment
cat > $ROOTFS/etc/profile << 'EOF'
export PATH=/bin:/sbin:/usr/bin:/usr/sbin
export PS1='\u@\h:\w\$ '
export HOME=/root
umask 022
EOF
Packaging into a Filesystem Image
{:.gc-adv}
Advanced
ext4 Block Image
ROOTFS=$HOME/rootfs
IMAGE=rootfs.ext4
# Allocate a 64 MB image file
dd if=/dev/zero of=$IMAGE bs=1M count=64
# Format as ext4 — small block size and inode ratio for embedded
mkfs.ext4 -b 1024 -i 4096 -L "rootfs" $IMAGE
# Mount loop and copy in our rootfs
sudo mount -o loop $IMAGE /mnt/target
sudo cp -a $ROOTFS/* /mnt/target/
sudo umount /mnt/target
# Verify and minimize
e2fsck -f $IMAGE
resize2fs -M $IMAGE # shrink to minimum size
cpio Initramfs (in-memory rootfs)
cd $ROOTFS
# Build compressed cpio archive
find . | cpio -H newc -o | gzip -9 > ../initramfs.cpio.gz
# Check size
ls -lh ../initramfs.cpio.gz
# Should be around 1–3 MB for a BusyBox rootfs
SquashFS Read-Only Image
mksquashfs $ROOTFS rootfs.squashfs \
-comp lz4 \
-b 131072 \
-noappend \
-no-progress
Format Selection Guide
| Format | Writable | Compression | Use Case |
|---|---|---|---|
| ext4 | Yes | No | SD card, eMMC, development |
| cpio/initramfs | RAM only | Yes (gzip/xz) | Initramfs, tiny targets |
| SquashFS | No (ro) | Yes (lz4/zstd) | Production read-only rootfs |
| UBIFS | Yes | Yes (LZO/zlib) | Raw NAND flash |
Testing with QEMU
{:.gc-adv}
Advanced
Boot with initramfs (cpio)
qemu-system-arm \
-M vexpress-a9 \
-cpu cortex-a9 \
-m 256M \
-kernel zImage \
-initrd initramfs.cpio.gz \
-append "console=ttyAMA0,115200 rdinit=/sbin/init" \
-nographic
Boot with ext4 on Virtual SD Card
qemu-system-arm \
-M vexpress-a9 \
-cpu cortex-a9 \
-m 256M \
-kernel zImage \
-drive file=rootfs.ext4,format=raw,if=sd \
-append "console=ttyAMA0,115200 root=/dev/mmcblk0 rootfstype=ext4 rw init=/sbin/init" \
-nographic
Common Kernel Panic: “No working init found”
This panic means the kernel mounted the rootfs but could not execute PID 1.
Kernel panic - not syncing: No working init found.
Try passing init= option to kernel. See Linux Documentation/admin-guide/init.rst
Debugging checklist:
| Symptom | Likely Cause | Fix |
|---|---|---|
No working init |
/sbin/init missing or wrong arch |
Verify BusyBox is at /sbin/init and compiled for correct arch |
No working init |
Missing shared libraries | Use CONFIG_STATIC=y or copy all .so files |
No working init |
/dev/console missing |
mknod /dev/console c 5 1 |
VFS: Cannot open root device |
Wrong root= parameter |
Check device name: /dev/mmcblk0p2, /dev/sda1, etc. |
| Mounts ro, writes fail | Kernel mounted rootfs read-only | Add rw to kernel cmdline |
# Verify binary architecture matches QEMU target
file $ROOTFS/bin/busybox
# bin/busybox: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV),
# statically linked, stripped
Interview Q&A
{:.gc-iq}
Interview Q&A
Q1 — Why would you prefer static linking for a BusyBox-based initramfs?
An initramfs must work before the real rootfs is mounted — there is nowhere to load shared libraries from. Statically linking BusyBox embeds libc into the binary so it runs with zero external dependencies. For the final rootfs (where
/libexists), dynamic linking is preferable for security patch agility, but for the early-boot environment, static is the safe choice.
Q2 — What device nodes must exist in /dev for the system to boot to a BusyBox shell?
At minimum:
/dev/console(c 5 1) so that init can open its controlling terminal, and/dev/null(c 1 3) so that redirections work. Without/dev/console, BusyBox init aborts withcan't open /dev/console. The getty also needs the serial device node (e.g.,/dev/ttyAMA0). Usingdevtmpfsautomounts these from the kernel, removing the manualmknodrequirement.
Q3 — What is the difference between BusyBox init and systemd?
BusyBox init is a small, sequential init system driven by a single
/etc/inittabfile. It has no dependency graph, no parallelism, no socket activation, no cgroups integration, and no journal. It is appropriate for systems booting in under a second where simplicity and small footprint matter. systemd is a full service manager with declarative unit files, parallel startup, service supervision, cgroup accounting, and journald logging — valuable on complex embedded or server systems but adds 3–10 MB to the rootfs and significant RAM overhead.
Q4 — In /etc/inittab, what is the difference between respawn and once?
respawninstructs BusyBox init to restart the process every time it exits — used for getty on a serial console so a new login prompt appears after a user logs out.onceruns the process a single time and does not restart it if it exits. Useoncefor one-shot configuration tasks that run at boot and are expected to complete and exit.
Q5 — Why must /proc be mounted before many other init tasks?
/procis required by tools likemount(reads/proc/mounts),ps,top,ifconfig, and udev/mdev. Themount -acommand reads/etc/fstaband updates/proc/mounts. Attempting to start daemons or configure networking before/procis mounted causes mysterious failures. Similarly,/sysmust be mounted before any device enumeration.
Q6 — How would you add a custom applet to BusyBox?
Create
mycmd.cin the BusyBox source tree, implementingint mycmd_main(int argc, char **argv). Register it ininclude/applets.hwith theAPPLET()macro, and add the correspondingKconfigentry and Makefile rule. Runmake menuconfig, enable the new applet under its menu category, rebuild, and the applet is available. The BusyBox wiki documents the full process under “Applets Development”.
Q7 — What kernel command-line parameters does BusyBox init respect?
BusyBox init reads
init=(path to the init binary, default/sbin/init),rdinit=(path to init inside an initramfs), and per-applet parameters. It also respectssingleor1to enter single-user mode (runlevel 1), which spawns a single root shell without runningrcS. Thequietparameter suppresses kernel messages but does not affect BusyBox init output.
References
{:.gc-ref}
References
| Resource | Link |
|---|---|
| BusyBox Official Site | busybox.net |
| BusyBox FAQ | busybox.net/FAQ.html |
| Buildroot Manual | buildroot.org/downloads/manual/manual.html |
| Linux From Scratch — Creating a Boot Script | linuxfromscratch.org/lfs |
| Bootlin Embedded Linux Training | bootlin.com/training/embedded-linux |
| QEMU ARM vexpress-a9 | qemu.org/docs/master/system/arm/vexpress.html |
| Linux kernel init documentation | kernel.org/doc/html/latest/admin-guide/init.html |
man 8 init (BusyBox) |
Run busybox init --help or man 8 init |