From Power-On to main()

September 17, 2026 • 5 min read

From Power-On to main()
Table of Contents

Part of the series:Operating Systems

Every post in this series assumed a running kernel: processes scheduled, pages translated, files mounted, devices humming. The I/O post named the missing piece explicitly: RAM is empty at power-on and no kernel exists anywhere. This capstone closes that gap and the two series with it. We will follow one press of the power button through firmware, bootloader, kernel startup, the first user process, the program loader, and the handoff to main, meeting every earlier post’s machinery as it comes online.

Firmware: the first instructions

At power-on the CPU resets to a known state: caches cold, RAM empty and untested, devices uninitialized. It fetches from a hardwired address pointing at firmware1 in non-volatile memory (BIOS2 on old machines, UEFI3 on modern ones). Firmware runs before anything else exists: it tests RAM, enumerates devices, initializes DRAM controllers and clocks, and finds something bootable.

power ──▶ CPU reset vector ──▶ firmware (ROM/flash)
                                   │ POST, init RAM, enumerate

                              find boot device

Firmware’s last job is loading the next stage and jumping to it. The chain has begun: each stage just smart enough to load a smarter one.

Bootloader: the kernel’s delivery service

The bootloader4 (GRUB, systemd-boot, phone bootloaders) bridges firmware and kernel. Firmware loads it; it understands filesystems (unlike firmware, which reads raw sectors or simple partitions), finds the kernel image and initial RAM disk, sets up the boot parameters and memory map, and transfers control. The two-stage trick exists because firmware stays small and dumb while the bootloader, living on disk it can update, carries the complexity.

On UEFI systems the path is standardized: firmware reads the EFI partition, executes the bootloader application, which calls back into UEFI services for disk and display until the kernel takes over and those services are abandoned (ExitBootServices). The machine is still single-tasked; no processes, no virtual memory, just code running on bare metal toward one goal.

Kernel startup: building the world

The kernel entry point runs the most delicate code in the system: no stack worth speaking of, no heap, no scheduler, no memory management yet. Startup builds every subsystem this series described, in dependency order:

entry ──▶ early page tables ──▶ memory allocator ──▶ interrupts & timer
              │                       │                      │
              ▼                       ▼                      ▼
        virtual memory on      kmalloc works          preemption possible
              │                       │                      │
              └───────────┬───────────┘                      │
                          ▼                                  ▼
                    scheduler + drivers + VFS ──▶ mount root ──▶ spawn init

Early page tables turn on virtual memory (the paging machinery now active for the kernel itself); the page allocator and slab allocator follow; interrupt descriptors and the timer IRQ enable preemption (the scheduler can now exist); drivers probe buses; the virtual filesystem layer mounts the root filesystem (the filesystems world now reachable). Each layer assumes only what came before, which is why the order is fixed.

Init: the first process

With the kernel alive, it becomes a process factory and manufactures process number one. On Unix the kernel spawns init5 (pid 1, systemd on most Linux systems); orphaned processes get adopted by it, which is why the processes post’s reaping promise holds. Init mounts filesystems, starts services, and brings up login: the machine graduates from one kernel thread to a managed multi-process system. Everything you think of as “the OS running” is init’s descendants, scheduled, isolated, and page-tabled exactly as earlier posts described.

Loader: from file to process

Starting your program replays process birth at full detail. The shell forks; the child calls exec on your binary; the kernel’s loader takes over: parse the ELF6 headers, map code and data segments into a fresh address space, map shared libraries (or the dynamic linker that maps them), zero the BSS, allocate the stack with arguments and environment, set the entry point, and mark the process ready.

fork ──▶ exec("./app") ──▶ parse ELF ──▶ map segments + libraries


                              stack (argv, env) ──▶ jump to _start

_start is not your main. It is the C runtime’s entry: set up the standard library, run global constructors, collect the exit code afterward. Its last act is calling your code:

_start: ──▶ __libc_start_main ──▶ main(argc, argv, envp) ──▶ exit(status)

Eight posts of machinery narrow to one function call. main returns, the runtime calls exit, the kernel reaps, the parent is notified. Birth to death, syscalls at both ends.

The big picture

Two series, one arc. The Computers series built the machine: representation, logic, CPU, memory, storage, I/O, interconnects, and the stored-program idea that makes one machine every machine. The Operating Systems series taught that machine to share: kernel and syscalls, processes, threads, scheduling, virtual memory, filesystems, I/O paths, and now the boot that starts it all. Press power and firmware wakes, the bootloader delivers, the kernel builds its world layer by layer, init fathers every process, the loader maps your bytes, and _start calls main.

Whatever you build on top, from Fibonacci to distributed systems, runs on this stack. The details will keep evolving: new firmware, new schedulers, new filesystems. The shape, stages loading stages until a program believes the machine is its own, will keep rhyming.

Footnotes

  1. Firmware - Wikipedia

  2. BIOS - Wikipedia

  3. UEFI - Wikipedia

  4. Bootloader - Wikipedia

  5. Init - Wikipedia

  6. Executable and Linkable Format - Wikipedia