Before you start#
Read Kconfig and modules, CPU, memory and I/O, and the disposable kernel lab. This chapter follows a possible upstream v6.12 x86-64 boot, with QEMU's direct kernel loading as the practical setting. Firmware, loader, CPU, configuration and boot parameters change the path. A source-only worked log is provided; no boot is required to reason about it.
Learning goals#
Separate firmware and bootloader responsibilities from the kernel's boot protocol; follow compressed and uncompressed entry code into start_kernel; explain early memory, interrupt and scheduling dependencies; and map observable log messages to bounded source claims without treating a log as a complete trace.
The contract before kernel C#
A machine starts with firmware-defined behavior, not with a C call to main. Depending on the platform, legacy firmware or UEFI initializes enough hardware and transfers control to a boot manager or loader. A conventional loader selects an image, supplies a command line and possibly an initramfs, and constructs the boot parameters required by the selected x86 boot protocol. QEMU's -kernel direct-loading mechanism supplies a kernel and parameters through its own virtual-machine implementation; it should not be described as a trace through a particular installed GRUB or UEFI setup. A real boot can involve secure-boot policy, a firmware stub, loaders and platform-specific memory maps absent from the simple guest. See the x86 boot protocol.
The x86 bzImage contains setup and compressed-kernel components; it is not merely the uncompressed vmlinux file renamed. Boot code needs the loader's memory map, command line, ramdisk location and other fields to be valid for the chosen entry protocol. The v6.12 setup header defines protocol-facing fields. Firmware tables such as ACPI can later describe system resources; their presence and correctness are platform-specific. If the loader supplies a malformed image, invalid parameters or inaccessible memory, failure can occur before the normal kernel console is initialized. A blank screen does not by itself prove the kernel reached or failed within start_kernel.
Source walkthrough#
- Loader handoff
Pass the image, boot parameters and optional initramfs.
- Compressed startup
Prepare mappings and extract the kernel to a suitable location.
- Early kernel
Architecture entry and setup lead into start_kernel.
- Tasks and userspace
rest_init coordinates kernel_init and kthreadd before userspace execution.
The compressed x86-64 code in v6.12 arch/x86/boot/compressed/head_64.S documents a startup_64 entry that can be reached from the 32-bit startup path or directly by a suitable 64-bit loader. The code depends on the documented identity mappings when entered through that 64-bit route. Later, after relocation of the decompressor and preparation, .Lrelocated contains (SPDX GPL-2.0; surrounding instructions and comments omitted):
movq %r15, %rdi
movq %rbp, %rsi
call extract_kernel
The surrounding comments identify %r15 as the boot-parameter pointer and %rbp as the output target address. Under this calling convention the first two arguments are placed in %rdi and %rsi; extract_kernel returns an entry point. The subsequent source restores the boot-parameter pointer in %rsi and jumps to that returned address. This is a source trace through one implementation, not a claim that every loader entered at the same instruction or used the same physical address. Decompression failure, an invalid image or insufficient suitable memory can stop progress before the uncompressed kernel starts.
CONFIG_RELOCATABLE affects the compressed code's calculation of a suitable load address; KASLR can further alter placement when configured and permitted by boot conditions. Relocation support and randomization are related but distinct: the presence of relocatable code does not prove randomization occurred, and one boot log is not a proof of entropy quality. Early identity mappings translate selected virtual addresses to corresponding physical addresses so instructions and data remain accessible during transition. They are temporary boot machinery, not the final userspace address-space model.
The uncompressed kernel's architecture-specific entry code and early C setup eventually reach v6.12 arch/x86/kernel/head64.c. In x86_64_start_reservations, the following excerpt ends the function (SPDX GPL-2.0; platform-specific branches omitted):
start_kernel();
Before this point the source handles early boot data and platform-specific setup. The call enters common initialization in v6.12 init/main.c. start_kernel calls setup_arch(&command_line), initializes core memory and scheduling facilities, and sets up interrupt and timekeeping infrastructure. The exact calls and their order are visible in that revision; optional subsystems and actual hardware availability remain configuration dependent. The x86 implementation of setup_arch in v6.12 arch/x86/kernel/setup.c processes the boot environment, reserves memory and coordinates architecture-specific initialization.
Early memory and execution context#
Before the normal page allocator is ready, the kernel needs a description of available and reserved physical memory. On x86, firmware-provided maps and boot parameters contribute to this view, with platform-specific adjustments. Memblock is an early allocator and region tracker used while the full memory-management system is being initialized. A reserved range should not be handed out as ordinary free memory. A supplied initramfs also occupies memory and must be kept valid until it is consumed or moved under the relevant boot path. setup.c includes explicit initrd reservation and relocation logic; if the required allocation cannot be satisfied, that code can panic rather than silently overwrite another region. This is one failure branch, not an exhaustive model of every boot memory problem.
At the beginning of common initialization there is a boot CPU and highly constrained context. Interrupts are not assumed available simply because C is running. The v6.12 start_kernel source initializes scheduler structures before setting up IRQ and timer machinery, then enables local interrupts later after required early setup. A source call to sched_init does not mean every CPU is already online or that ordinary userspace processes are running. Secondary CPU bring-up and full SMP scheduling come later and depend on the architecture and configuration. Code annotated __init is intended for initialization and can later be discarded; persistent code must not retain unprotected pointers into freed init-only memory.
The interrupt distinction matters when interpreting a hang. A machine can print a kernel banner and then stop because of a timer, interrupt-controller or later init problem. Conversely, missing serial output can conceal progress. Establish the last visible marker, console parameters and build configuration; a console observation is not a complete chronological trace of all internal events. Early boot debugging can itself alter timing or require configuration, so record instrumentation as part of the experiment.
A reservation is not ordinary free memory
Suppose a loader places an initramfs in a RAM range also reported in the machine's general memory map. The kernel needs to know that this particular range holds boot data while it builds its early memory bookkeeping. Treating the entire reported RAM region as freely allocatable could let an early allocation overwrite the archive before it is unpacked. The x86 setup source reserves the initrd range and, when required and possible, can relocate it into a suitable mapped region. The failure to allocate a relocation target is an explicit panic path in that code. This scenario illustrates an invariant and an error branch; exact addresses and whether relocation is needed depend on the loader, memory map and build.
In v6.12 arch/x86/kernel/setup.c, SPDX GPL-2.0-only, the early reservation helper is (shown in full):
static void __init early_reserve_initrd(void)
{
/* Assume only end is not page aligned */
u64 ramdisk_image = get_ramdisk_image();
u64 ramdisk_size = get_ramdisk_size();
u64 ramdisk_end = PAGE_ALIGN(ramdisk_image + ramdisk_size);
if (!boot_params.hdr.type_of_loader ||
!ramdisk_image || !ramdisk_size)
return; /* No initrd provided by bootloader */
memblock_reserve(ramdisk_image, ramdisk_end - ramdisk_image);
}
The helper returns early when the loader supplied no initrd, and otherwise reserves the page-aligned range with memblock_reserve. That reservation is what keeps the archive out of early allocations. The excerpt does not show the relocation branch in reserve_initrd, the panic when no relocation target exists, or the addresses used by any particular boot.
From the first task to userspace#
Near the end of start_kernel, rest_init starts the next phase. The v6.12 source in init/main.c contains these statements (SPDX GPL-2.0-only; intervening setup omitted):
pid = user_mode_thread(kernel_init, NULL, CLONE_FS);
pid = kernel_thread(kthreadd, NULL, NULL, CLONE_FS | CLONE_FILES);
The first creates the task that will run kernel_init and obtain PID 1 in the initial PID namespace. The second creates kthreadd, which supports kernel-thread creation. The comments explain why init must be created first yet must not proceed into work requiring kernel threads before kthreadd is ready. rest_init signals the kthreadd_done completion; kernel_init waits for it before continuing. This is a synchronization and lifetime relationship, not proof that PID 1 immediately executes /init at task creation.
In v6.12 init/main.c, SPDX GPL-2.0-only, the start of kernel_init waits on the completion:
static int __ref kernel_init(void *unused)
{
int ret;
/*
* Wait until kthreadd is all set-up.
*/
wait_for_completion(&kthreadd_done);
The wait is the first blocking step in kernel_init, and the comment states its purpose. The excerpt does not show the complete(&kthreadd_done) call in rest_init, which runs after kthreadd is created, or the later /init attempt. The order in the diagram below is a source-order model, not a scheduling trace.
- rest_initkernel_init (PID 1)
user_mode_thread(kernel_init) creates the init task
- kernel_init (PID 1)kernel_init (PID 1)
wait_for_completion(&kthreadd_done)
- rest_initkthreadd
kernel_thread(kthreadd) creates the helper
- rest_initkernel_init (PID 1)
complete(&kthreadd_done)
- kernel_init (PID 1)kernel_init_freeable
call kernel_init_freeable (subsystems, initcalls)
- kernel_init_freeablekernel_init (PID 1)
Return. return to kernel_init
Time runs downward. Dashed arrows show returns.
Read the text version
- rest_init → kernel_init (PID 1): user_mode_thread(kernel_init) creates the init task
- kernel_init (PID 1) → kernel_init (PID 1): wait_for_completion(&kthreadd_done)
- rest_init → kthreadd: kernel_thread(kthreadd) creates the helper
- rest_init → kernel_init (PID 1): complete(&kthreadd_done)
- kernel_init (PID 1) → kernel_init_freeable: call kernel_init_freeable (subsystems, initcalls)
- kernel_init_freeable → kernel_init (PID 1) (return): return to kernel_init
The later kernel_init_freeable work includes subsystem initialization and initcalls under the selected configuration. Initcalls register and invoke initialization functions at defined phases; their existence does not guarantee all optional drivers are built or that a particular device probes successfully. After required asynchronous initialization completes, init memory can be freed. The kernel then attempts early userspace and configured fallback programs. A successful exec replaces the task's image; a missing interpreter or invalid executable can cause failure even if /init exists. If no working init can be executed, the kernel can panic. The init failure guide helps distinguish these cases.
A worked boot-log interpretation#
Consider this synthetic sequence, not captured output: Linux version 6.12..., then Unpacking initramfs..., then Guest init: shell will be PID 1., then guest shell PID=1. The first line supports that a kernel banner was printed, but not which exact source commit or configuration produced it. The unpacking line supports that the external archive path was reached in that run, not that every archive member was usable. The marker is printed by the chapter 08 userspace script after its mount commands; because the script does not abort on mount failure, the marker alone does not prove /proc mounted. The last line, if printed by that guest shell after the script's exec, supports that the shell sees PID 1 in its namespace. Check /proc/1/status and /proc/cmdline as independent observations.
Now alter the scenario: the archive contains /init with execute permission but no /bin/sh. The script's interpreter cannot be found, so a later init failure is compatible with the archive successfully unpacking. Alternatively, the system might boot but print nothing on your terminal if the console is directed elsewhere. These hypotheses need different checks. File existence, archive integrity, interpreter architecture, configuration, QEMU arguments and captured logs all belong in the evidence, not in an assumed narrative.
Interpreting initcalls and errors#
An initcall is a registered initialization function invoked as the boot progresses through ordered levels. That ordering helps establish subsystem prerequisites, but it does not turn every initialization into a synchronous guarantee that hardware is usable. A driver may not be selected by Kconfig, a device may be absent, or a probe can fail or be deferred. Initcall timing or diagnostic output, when enabled, can help locate a slow or failing phase; its availability depends on configuration and command-line options. Capturing a message from one initcall does not show that all later initcalls ran successfully.
Consider two failures after a visible banner. In the first, memory reservations are inconsistent and an early allocation needed for the initramfs cannot be made; the kernel may stop before userspace. In the second, the archive is unpacked but its /init interpreter is missing; kernel initialization can have progressed much further before the userspace exec fails. These are different fault domains even if the user reports both as “the VM did not start.” Preserve the entire serial log and image/configuration pairing, identify the first concrete error, and form hypotheses tied to that phase. A reboot without saving the log can erase the best evidence.
- Before start_kernel
- Decompression, loader handoff or boot parameters fail; the normal console may not exist yet.
- Early memory
- An initrd that cannot be reserved or relocated can panic in x86 setup code.
- Archive unpacked
- The initramfs is present, but /init, its interpreter or its mode can still fail.
- Init handoff
- kernel_init reaches run_init_process; a failure is logged and the fallbacks are tried.
Practice#
Source-only route: annotate the four synthetic messages above with (a) what each supports, (b) one fact it cannot prove and (c) the next artifact to inspect. Add the explicit kernel_init/kthreadd_done ordering to your notes. This route is complete without a guest and must be labeled a prediction.
Optional guest observation: use only the disposable chapter 08 environment and the exact QEMU command from that chapter. If you have already built its kernel and archive, preserve the configuration and boot output. Run QEMU in a host terminal and, at its guest shell, execute:
printf 'guest shell PID=%s\n' "$$"
cat /proc/cmdline
cat /proc/1/status
The prompt should belong to the guest, not the host; check that the kernel boot messages and Guest init marker preceded it. Expect PID 1 for the exec-replaced shell and the console=ttyS0 rdinit=/init arguments in the guest command line. A missing /proc/1/status may mean proc was not mounted; it does not establish that PID 1 never existed. If there is no console, inspect QEMU arguments and CONFIG_SERIAL_8250_CONSOLE; if the kernel reports init failure, inspect the archive and interpreter as described in chapter 08. Stop the guest using its poweroff -f command or terminate the identified QEMU process from another host terminal. Do not run guest shutdown commands in your host shell.
Save a two-column table of actual observations and justified source interpretations, including version, .config, QEMU invocation and one unexplained gap. If you did not boot, save the synthetic table instead and clearly label it source-only. Do not alter host firmware, bootloader or disks. The practice does not require recording a guest trace or enabling debugging instrumentation.
Knowledge check#
- What information does a loader provide to an x86 kernel, and why is QEMU direct boot not proof of a particular firmware path?
- What is the role of the call to
extract_kernel, and why must the compressed and uncompressed entry paths be distinguished? - Why does
kernel_initwait forkthreadd_doneeven though its task is created first? - What does an initramfs unpacking message establish, and why can
/initstill fail? - Why is a kernel banner insufficient to establish the exact commit and configuration?
Answers#
- A loader supplies image placement, boot parameters, command line and optional ramdisk according to the applicable protocol. QEMU's direct loader is a distinct environment; the same guest image need not traverse a separately installed firmware and bootloader stack.
- The compressed path prepares an output location and invokes extraction before jumping to the uncompressed kernel entry. The two stages use different code and constraints, so
startup_64names and addresses must be qualified by their source file. - Init must obtain PID 1, but its later work may require kernel threads. The completion coordinates progress until
kthreaddis established; task creation is not the same as successful userspace exec. - It indicates the unpacking path was reached in that observed run. The archive may lack a usable script, interpreter or required files, and mount or execution can fail afterward.
- A banner can include a release string but not all source changes, configuration or compiler inputs. Record the resolved commit, dirty status,
.configand build provenance alongside the log.
Checkpoint#
Deliver the source-only or actual boot evidence table with the compressed entry, common initialization, kthreadd synchronization and init execution separated. Pass when you can explain one failure before start_kernel, one after unpacking and a safe recovery route. Retry using the source links and chapter 08 diagnostics if the sequence or assumptions are unclear. This is not certification.