Linux Kernel Command Line Explained-Free Embedded Linux Course

Linux Kernel Command Line Explained
How bootargs shape every boot of a custom embedded Linux kernel
Free Embedded Linux Course
Chapter 4 · Porting & Configuring the Kernel
Hands-on QEMU demo included
kernel command line
bootargs
free linux kernel development course
free embedded linux course
free linux device drivers course
console loglevel

If you’ve ever built your own embedded Linux image, you already know that the
kernel command line is one of the first things you need to get right, or your board
simply won’t boot to a shell. This lecture is part of our free linux kernel development
course
, and it explains exactly what the kernel command line is, how the bootloader hands
it to the kernel, and how you can use it to control everything from boot speed to which root
filesystem gets mounted. This is the kind of practical, board-bring-up knowledge you need whether
you’re following a free embedded linux course or debugging a production board at 2 AM.

What You Will Learn

  • What the kernel command line actually is, and the three ways it reaches the kernel
  • How to inspect and modify bootargs on a running or QEMU-emulated board
  • The most useful kernel command line parameters for embedded bring-up and debugging
  • How console log levels work, and how to control kernel boot verbosity
  • How to shave real milliseconds off boot time using a calibrated lpj value
  • Common mistakes engineers make with bootargs, and how to avoid them

Prerequisites

  • A cross-compiled ARM kernel and root filesystem (see earlier lectures in this course)
  • Basic familiarity with U-Boot environment variables, or a device tree chosen node
  • QEMU installed on your host if you want to follow the demo without real hardware

What Is the Kernel Command Line?

The kernel command line is a single string of space-separated options that is handed to the
Linux kernel at the very start of boot, before any driver has probed, before init has run,
before anything userspace-related exists. It tells the kernel things it cannot discover on its
own: which device holds the root filesystem, how noisy the console should be, what the very first
user-space program should be called, and dozens of other low-level policy decisions.

There are three places this string can come from, and the kernel merges them in a defined
order:

Where the Kernel Command Line Comes From
+—————————–+
| 1. Bootloader (highest |
| priority in practice) |
| U-Boot “bootargs” env |
| var, passed via ATAGs |
| or the DT /chosen node |
+—————+————-+
|
v
+—————————–+
| 2. Device tree |
| chosen { bootargs = …} |
| baked into the .dtb |
+—————+————-+
|
v
+—————————–+
| 3. Kernel .config |
| CONFIG_CMDLINE=”…” |
| built into the kernel |
| image itself |
+—————+————-+
|
v
Final merged string
seen by the kernel
at “Kernel command
line: …” in dmesg

In the U-Boot case, the flow looks like this: U-Boot reads the bootargs environment
variable, patches it into either the legacy ATAG list or the device tree’s /chosen/bootargs
property, and jumps into the kernel. The kernel then parses that string during
start_kernel(), long before any driver subsystem is initialised.

Inspecting the Command Line on a Running Kernel

Every running Linux system exposes the exact string it booted with. This is the fastest way to
confirm what actually reached the kernel, as opposed to what you think you set in U-Boot:

$ cat /proc/cmdline
console=ttyAMA0,115200 root=/dev/mmcblk0p2 rootwait rw

If this doesn’t match what you expected, the bug is almost always in the bootloader stage
(wrong environment variable, wrong device tree overlay applied) rather than in the kernel
itself.

Setting bootargs From U-Boot

On a U-Boot-based board, you set and save the command line like this:

=> setenv bootargs 'console=ttyAMA0,115200 root=/dev/mmcblk0p2 rootwait rw'
=> saveenv
=> boot

Setting bootargs From the Device Tree

If your board has no interactive bootloader prompt, or you want the command line to travel
with the device tree itself, set it in the chosen node of your board’s .dts file:

/ {
    chosen {
        bootargs = "console=ttyAMA0,115200 root=/dev/mmcblk0p2 rootwait rw";
    };
};

Rebuild the device tree blob and it will be baked in on the next boot:

$ make ARCH=arm dtbs

Reference Table: Useful Kernel Command Line Parameters

Parameter Effect
console=<dev>,<baud> Selects which serial device carries kernel and early userspace output
root=<device> Device node holding the root filesystem
rootwait Waits indefinitely for the root device to appear — required for MMC/SD boot
rootdelay=<N> Fixed N-second wait before mounting root — a crude alternative to rootwait
rootfstype=<fs> Forces the root filesystem type; required for filesystems the kernel can’t auto-detect, such as JFFS2
ro / rw Mounts root read-only or read-write at boot (has no effect on an initramfs, which is always read-write)
init=<path> First userspace program to run from the root filesystem; defaults to /sbin/init
rdinit=<path> First program to run from an initramfs; defaults to /init
quiet Raises the console log level so only emergency messages print — the biggest single lever for reducing boot-time serial I/O
debug Lowers the console log level so every kernel message, including KERN_DEBUG, prints
loglevel=<N> Sets an explicit console log level, 0 (emergency only) through 8 (everything)
panic=<N> Seconds to wait before auto-rebooting after a kernel panic; 0 (default) waits forever, negative reboots immediately
lpj=<N> Skips the ~250ms delay-loop calibration by supplying a pre-measured loops_per_jiffy constant

Console Log Levels: Where Messages Actually Go

Every kernel log message carries a priority, from KERN_EMERG (0) to KERN_DEBUG (7). All
of them are always written into an internal ring buffer called __log_buf, whose size is
2^CONFIG_LOG_BUF_SHIFT bytes — with CONFIG_LOG_BUF_SHIFT=16, that’s a 64 KiB buffer. You
can dump the full buffer at any time with dmesg, regardless of what actually hit the
console.

Whether a message also prints live to the serial console is a separate decision, controlled by
the console log level. Only messages with a priority numerically lower than the console log
level appear on the console. The default console log level is 7, so priority-7 (KERN_DEBUG)
messages are logged to the buffer but suppressed from the live console.

# View current console log level, default level, min level, boot-time default
$ cat /proc/sys/kernel/printk
7  4  1  7

# Change it live without rebooting
$ dmesg -n 5

# Or dump everything captured so far
$ dmesg | tail -20

Hands-On Demo: Measuring the Effect of lpj

During early boot, the kernel spends roughly 250 ms spinning a busy-loop to calibrate a
delay constant called loops_per_jiffy. On real, fixed hardware this value never changes
between boots, so you can precompute it once and skip the calibration entirely on every
subsequent boot.

Boot once normally and capture the calibrated value from the kernel log:

$ dmesg | grep -i bogomips
[    0.098234] Calibrating delay loop... 996.14 BogoMIPS (lpj=4980736)

Now feed that value back in as a boot parameter on every future boot, either via U-Boot or the
device tree chosen node from earlier:

=> setenv bootargs 'console=ttyAMA0,115200 root=/dev/mmcblk0p2 rootwait rw lpj=4980736'
=> saveenv

Reboot and confirm the calibration step is skipped:

$ dmesg | grep -i bogomips
[    0.000041] Calibrating delay loop (skipped) already calibrated this CPU

That’s roughly 250 ms saved on every single boot — meaningful on a battery-powered device
that needs to wake, do something quick, and go back to sleep.

Real-World Use Cases

  • Fast-boot embedded appliances — combine quiet with a fixed lpj to cut boot time on
    devices where every millisecond of startup matters
  • Field debugging — temporarily add debug loglevel=8 to see every driver probe message
    when diagnosing a board that hangs during boot
  • Read-only production images — ro root=/dev/mmcblk0p2 mounts the root filesystem
    read-only at boot, protecting flash from unclean power loss, with an application-level overlay
    mounted read-write afterwards
  • Initramfs-based recovery images — rdinit=/bin/recovery-shell boots straight into a
    recovery tool instead of the normal init system

Common Mistakes and Troubleshooting

Symptom Likely Cause Fix
Kernel panics with “VFS: Unable to mount root fs” Missing rootwait on an MMC/SD root device that isn’t ready the instant the kernel probes it Add rootwait, or a generous rootdelay= as a fallback
No console output at all Wrong console= device name or baud rate for the actual UART wired to your debug header Double-check against the board schematic or SoC UART numbering, not the Linux device name from a different board
bootargs edits in U-Boot have no effect The device tree’s chosen/bootargs is silently overriding what U-Boot passes, depending on the DT source setup used Check /proc/cmdline on the running system to see which string actually won, and edit that source
Applied an lpj value and boot got slower or CPU-bound code misbehaves Reused an lpj value measured on a different board revision or clock configuration Recalibrate on the exact hardware and firmware you’re shipping; never copy an lpj value between boards

Best Practices

  • Keep one authoritative bootargs source per board — don’t split the string across both U-Boot
    and the device tree, or you’ll spend hours debugging which one won
  • Always confirm the effective command line via /proc/cmdline after any bootloader or
    device tree change — never trust what you think you set
  • Use rootwait rather than a fixed rootdelay= for removable/MMC media — it’s both faster
    on a good boot and more robust on a slow one
  • Only fix an lpj= value on hardware whose clock configuration is truly locked down for
    production; recalibrate whenever you change bootloader clock setup

Security Considerations

Treat the kernel command line as part of your board’s trusted boot chain, not just a
convenience string. An attacker with physical access to a U-Boot prompt can append arbitrary
kernel parameters — including init=/bin/sh, which drops straight to a root shell bypassing
your normal init and any userspace access control. If physical security matters for your product,
lock the U-Boot console with a password or disable interactive boot entirely, and consider a
verified/measured boot chain that authenticates the command line along with the kernel image.

Summary and Key Takeaways

  • The kernel command line is a single string, sourced from the bootloader, the device tree, or
    CONFIG_CMDLINE, that configures the kernel before any driver runs
  • /proc/cmdline always shows you the truth about what the kernel actually received
  • Console log level (default 7) controls what prints live; the ring buffer __log_buf always
    captures everything regardless, and dmesg reads it back
  • A precomputed lpj= value removes a real, measurable chunk of boot time on fixed hardware
  • rootwait, root=, and rootfstype= are the three parameters that make or break booting
    from removable/MMC storage

Conclusion

The kernel command line looks like a trivial detail the first time you copy one from a tutorial,
but understanding exactly where it comes from and what each parameter does is what separates
“my board eventually boots” from “I can debug why it doesn’t.” Once you’re comfortable reading
/proc/cmdline, tuning console verbosity, and shaving boot time with lpj, you have real
control over how your embedded Linux system starts up — a core skill in this free linux
device drivers course
and any serious embedded Linux bring-up work.

Frequently Asked Questions

What’s the difference between rootwait and rootdelay?

rootwait waits indefinitely, polling until the root device actually appears — it’s the
correct choice for MMC/SD, which can take a variable amount of time to enumerate. rootdelay=N
just sleeps a fixed N seconds regardless of whether the device is ready yet, which wastes time on
a fast boot and can still fail on a slow one.

Does changing the console log level with dmesg -n affect what’s stored in the log buffer?

No. dmesg -n only changes what gets printed live to the console going forward. Every
message at every priority is still written into __log_buf regardless of the console log
level, and you can always retrieve the full history with dmesg.

Can I set the kernel command line without touching the bootloader or device tree?

Yes — CONFIG_CMDLINE in your kernel .config bakes a default string directly into the
kernel image. Depending on CONFIG_CMDLINE_OVERRIDE/CONFIG_CMDLINE_EXTEND settings, this
either gets replaced by or appended to whatever the bootloader supplies.

Is quiet safe to use in production?

Generally yes, and it’s recommended for boot-time-sensitive products — it suppresses routine
console spam, not the underlying log buffer. Just make sure you still have a way to retrieve
dmesg output in the field (over SSH, a debug UART, or a log-collection agent) for when you
actually need to diagnose a problem.

Why does my lpj value change after a bootloader update?

Because loops_per_jiffy calibration depends on the CPU clock speed at the moment the
kernel measures it. If your bootloader update changes PLL/clock-tree configuration before jumping
to the kernel, the effective CPU frequency changes too, and any previously fixed lpj= value
becomes stale. Recalibrate after any change to early clock setup.

What happens if I set an invalid or nonsensical kernel parameter?

Unrecognised parameters that don’t match any registered kernel option are simply passed through
to init as environment-style arguments and otherwise ignored by the kernel itself — they
won’t normally crash the boot, but a typo in a parameter the kernel does recognise (like a bad
root= device name) will cause a boot failure such as an unmountable root filesystem.

 

Want to keep going?

Continue this free embedded Linux course with the next lecture on porting Linux to a new board.

 

 

Leave a Reply

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