Before an operating system can run applications, the machine must pass through a chain of carefully defined stages.
Firmware finds bootable code. A bootloader places the kernel in memory. The kernel initializes the machine and starts the first user-space process.
Later, whenever Linux launches a compiled program, another chain of components must agree on the program's binary format, processor instructions, register usage, memory layout, and library interface.
These two chains depend on the same broad principle:
One component can hand control to the next only when both agree on a precise contract.
This chapter follows the Linux boot path, examines the ELF executable format, and explains the application binary interface that makes separately compiled code work together.
A typical Linux system starts through the following stages:
Each stage prepares an environment for the next one. If any stage cannot locate, understand, or trust the next component, the boot process stops.
The exact path differs across machines. An embedded board, virtual machine, cloud server, and laptop may use different firmware and loaders. The sequence above is the useful general model.
When a CPU resets, it begins execution in an architecture-defined state at a predefined location.
The first software it runs is firmware, stored in nonvolatile memory on the machine. Firmware performs enough hardware initialization to locate and start the next stage.
On modern PCs, the firmware is usually UEFI, the Unified Extensible Firmware Interface. Older systems commonly use a traditional BIOS.
Firmware responsibilities can include:
Traditional BIOS firmware typically reads a small initial block from a boot device and executes it. UEFI provides a richer environment that can read files from an EFI System Partition and launch UEFI applications.
The operating system is not fully active at this point. There is no Linux process scheduler, normal root filesystem, or PID 1 yet.
UEFI systems may also verify signed boot components when Secure Boot is enabled. That security chain is important in production systems, but its detailed policy is outside this chapter.
The bootloader prepares the kernel to run.
GRUB and systemd-boot are common bootloaders on Linux systems. Other environments use platform-specific loaders.
A bootloader commonly:
The initial RAM filesystem, usually called an initramfs, contains a small early user-space environment.
It is useful when the kernel needs additional drivers or setup before it can access the real root filesystem. For example, early user space may need to unlock an encrypted volume, assemble a storage array, or load a driver required to reach the root device.
Some UEFI systems can load a suitably prepared Linux kernel directly without a separate general-purpose bootloader. This changes the number of visible stages, not the kernel's need to receive a valid boot environment.
The bootloader transfers control to the kernel while the machine is still in a limited startup state.
Early kernel code establishes the environment needed for normal kernel execution. Depending on the architecture and kernel image, it may decompress the main kernel before continuing.
The kernel then initializes core facilities such as:
| Area | Initialization purpose |
|---|---|
| Memory management | Discover usable memory and establish kernel mappings |
| Interrupts and timers | Allow the kernel to respond to hardware events and time |
| CPU management | Bring processor cores into a controlled operating state |
| Scheduling | Prepare to run independent execution contexts |
| Devices and drivers | Discover or initialize hardware needed during boot |
| Filesystems | Make an initial and then permanent root filesystem available |
These tasks have dependencies. The kernel cannot start ordinary processes before basic memory management and scheduling exist. It cannot mount a root filesystem until the required storage path and filesystem support are ready.
Boot messages often reflect this dependency order:
The full Linux initialization sequence contains many architecture-specific and subsystem-specific steps. The essential transition is from a single early boot path into a working kernel capable of running protected user-space processes.
If an initramfs is present, the kernel exposes its contents as an early root filesystem and starts its initialization program.
That program performs whatever setup is needed to reach the real root filesystem. It may load modules, discover devices, unlock storage, or assemble volumes.
Once the real root is ready, early user space switches to it and continues the boot process.
Minimal systems may not need a separate initramfs. If every required driver is built into the kernel and the root device is directly accessible, the kernel can mount the real root filesystem without this intermediate environment.
The kernel eventually starts the first user-space process.
This process receives process identifier 1, so it is known as PID 1. On many current Linux distributions, PID 1 runs systemd. Other systems may use an init implementation such as OpenRC, runit, or BusyBox init.
PID 1 brings the machine from “the kernel can run programs” to “the operating-system environment is available.”
It typically starts and supervises system services, establishes remaining system configuration, and enables login or application workloads.
On an initramfs-based system, the early initialization program may already be PID 1 and later replace itself with the final init system. The process identifier remains 1 across that replacement.
PID 1 also has special process-management responsibilities that will be covered in the processes module.
You can identify PID 1 on a running Linux system with:
You can inspect the kernel command line supplied during boot with:
These commands expose the endpoints of the boot path: the arguments passed into the kernel and the first user-space program running after kernel initialization.
Loading simulation...
Booting the kernel and launching an ordinary program are different operations.
Firmware and a bootloader place the kernel into a specially prepared machine state. Once Linux is running, the kernel itself loads user programs into protected process address spaces.
On Linux, most compiled user-space programs use the ELF format.
| Startup kind | Path |
|---|---|
| Machine startup | Firmware, bootloader, kernel |
| Program startup | Running process, kernel loads the ELF, program entry point |
The kernel image used during boot may have a platform-specific boot format and setup code. It should not be confused with an ordinary user-space ELF executable, even though ELF is also used for many kernel-development artifacts.
ELF stands for Executable and Linkable Format.
It is the standard binary format for executables, shared libraries, object files, and core dumps on Linux and many Unix-like systems.
An ELF file begins with a header that identifies what kind of file it is and how the rest of the file is organized.
Not every ELF file uses every part in the same way. Executable loading depends primarily on program headers, while compilers, linkers, debuggers, and analysis tools also use sections.
The ELF header describes the file as a whole.
Important fields include:
| Field | Purpose |
|---|---|
| Magic bytes | Identify the file as ELF |
| Class | Indicate a 32-bit or 64-bit format |
| Data encoding | Indicate byte order |
| Type | Identify an executable, shared object, object file, or core dump |
| Machine | Identify the required processor architecture |
| Entry point | Give the virtual address where execution begins |
| Program-header location | Locate the runtime segment descriptions |
| Section-header location | Locate the link-time section descriptions |
The first four bytes of an ELF file are:
The final three bytes are the ASCII letters ELF.
Common ELF types include:
| ELF type | Typical use |
|---|---|
ET_REL | Relocatable object file such as module.o |
ET_EXEC | Traditional executable |
ET_DYN | Shared library or position-independent executable |
ET_CORE | Core dump |
Many modern Linux toolchains build position-independent executables by default. Such a program may appear as ET_DYN even though it is launched as an executable. This allows its memory location to vary between runs.
Sections organize an ELF file for compilation, linking, relocation, and debugging.
Common sections include:
| Section | Contents |
|---|---|
.text | Compiled machine instructions |
.rodata | Read-only constants |
.data | Initialized writable global and static data |
.bss | Space for zero-initialized global and static data |
.symtab | A full symbol table, when retained |
.dynsym | Symbols used for dynamic linking |
.debug_* | Optional debugging information |
The .bss section is a useful example of why file layout and memory layout differ.
A program may need one megabyte of zero-initialized memory, but the executable does not need to store one megabyte of zeros. ELF metadata records the required size, and the loader provides zero-filled memory at runtime.
Sections are especially important before and during linking:
The compiler turns source files into object files containing sections, and the linker combines those object files into an executable or shared library.
A stripped production executable may omit much of the symbol and debugging information while remaining runnable.
Program headers describe segments that the operating-system loader maps into memory.
A segment can contain several sections with compatible runtime properties.
For example:
Several sections collapse into one segment because the loader cares about permissions, not names. Anything that needs the same access rights can share a mapping.
The actual mapping may separate read-only data from executable code for stronger protection. The diagram illustrates the general relationship rather than a required layout.
Important program-header types include:
| Program-header type | Purpose |
|---|---|
PT_LOAD | Describe bytes and memory that must be loaded |
PT_INTERP | Name the dynamic linker for a dynamically linked program |
PT_DYNAMIC | Describe information needed for dynamic linking |
PT_GNU_STACK | Describe expected stack permissions |
For each loadable segment, ELF records the file location, virtual address, file size, memory size, alignment, and access permissions.
The distinction between sections and segments is one of the most important ELF ideas:
Sections organize a binary for tools; segments organize it for execution.
The kernel's ELF loader primarily follows the program headers. It does not create one memory mapping for every section name.
Loading simulation...
A process asks Linux to execute a program using an operation such as execve.
execve does not create a second process. It replaces the calling process's current program image with a new one. Process creation and exec semantics will be covered in the processes module.
For an ELF executable, the loading path is conceptually:
The initial user stack contains more than command-line strings. It also contains environment entries and an auxiliary vector through which the kernel supplies useful process-startup information.
The kernel does not normally begin execution at the C function main.
The executable's ELF header identifies a lower-level entry point, commonly named _start in linked C programs. For a static executable, the kernel can transfer control there directly. For a dynamically linked executable, the kernel initially enters the dynamic linker, which later transfers control to the executable's entry point.
Startup code prepares the language runtime and eventually calls main.
_start runs.main(argc, argv) is called.When main returns, runtime code translates that result into process termination.
Most general-purpose Linux programs use shared libraries.
If an ELF file contains a PT_INTERP entry, it names a dynamic linker, also called a runtime linker or ELF interpreter.
A common x86-64 glibc path is:
The exact path differs across architectures and Linux distributions.
The kernel loads the main executable and its named interpreter. The dynamic linker then loads required shared libraries, applies relocations, resolves symbols, and transfers control into the program's startup path.
A statically linked executable contains the code it needs directly and does not require this shared-library loading path. It is usually larger and still depends on the kernel's system-call ABI and the processor's instruction set.
Dynamic linking works only because the executable, linker, and libraries follow the same binary conventions.
Create a small C program named hello.c:
Compile it on Linux:
Identify the file:
The result may describe a 64-bit ELF position-independent executable for x86-64 or AArch64.
Display the ELF header:
Look for the class, type, machine, entry-point address, and program-header count.
Display program headers and their memory permissions:
This output also reveals the requested program interpreter and shows how sections are grouped into segments.
Display section headers:
You should see entries such as .text, .rodata, .data, and .bss, though the exact set depends on the compiler and linker.
These tools inspect file metadata without launching the program. Their output provides a concrete view of the contract that the kernel loader and dynamic linker consume.
An application binary interface, or ABI, is a contract between compiled components.
An API describes how source code refers to functionality. An ABI describes how the compiled machine code represents and uses that functionality.
| Level | What it covers |
|---|---|
| API | Source-level names, types, and function declarations |
| ABI | Registers, stack layout, symbols, binary formats, data layout |
The compiler is what turns the first into the second.
An ABI can define:
ELF is therefore part of a larger ABI. It tells a loader how a binary is organized, while calling conventions and data-layout rules tell separately compiled functions how to exchange values.
Two systems using ELF are not automatically binary compatible. An x86-64 Linux ELF executable contains different instructions and follows different platform conventions from an AArch64 Linux ELF executable.
A calling convention defines how one compiled function calls another.
It answers questions such as:
On 64-bit Linux running on x86-64, ordinary C functions generally follow the System V AMD64 ABI.
The first six integer or pointer arguments use:
Additional arguments are passed on the stack, and an integer or pointer return value normally uses rax.
For example:
The first six arguments fit in registers. The seventh is passed through the stack according to the ABI.
The convention also divides registers into two broad groups.
Caller-saved registers may be overwritten by the called function, so the caller preserves any values it still needs.
Callee-saved registers must be restored by the called function before it returns.
This agreement allows a function compiled today to call a library function compiled years earlier without either compiler seeing the other's source code.
An ordinary function call and a Linux system call use related but different conventions.
On x86-64 Linux:
| Purpose | Ordinary System V function call | Linux system call |
|---|---|---|
| Operation selection | Call target address | Number in rax |
| Argument 1 | rdi | rdi |
| Argument 2 | rsi | rsi |
| Argument 3 | rdx | rdx |
| Argument 4 | rcx | r10 |
| Argument 5 | r8 | r8 |
| Argument 6 | r9 | r9 |
| Return value | rax | rax |
The C library wrapper performs this translation before entering the kernel.
Using r10 rather than rcx for the fourth system-call argument is not an arbitrary library preference. The x86-64 syscall instruction itself uses rcx to preserve return information, so the kernel-entry ABI assigns the argument elsewhere.
This is a concrete example of an ABI doing its job: independently written library, kernel, and application code agree on the same register-level contract.
API compatibility asks whether source code can still be compiled against a new version of an interface.
ABI compatibility asks whether an already compiled binary can continue working with a new component without being recompiled.
| Change | API compatible? | ABI compatible? |
|---|---|---|
| Change only a function's internal implementation | Usually yes | Usually yes |
| Add a new function without removing old ones | Usually yes | Usually yes |
| Remove a declaration but keep the old binary symbol | No for new builds using it | Yes for existing binaries |
| Change a function's parameters and replace its symbol | No | No |
| Change public structure field offsets | Source may still compile | No for binaries using the old layout |
| Change a required calling convention | Source can be recompiled | No for existing binaries |
The distinction matters because source code and machine code encounter changes at different times.
An API break is commonly discovered during compilation:
An ABI break affects code that has already passed compilation:
There may be no compiler present to catch the problem.
Suppose a shared library originally exports:
A new version changes the implementation to expect:
New source code sees the changed declaration and can be updated.
An old executable, however, still places only one argument according to the original ABI. If the loader connects that old call to the incompatible new implementation, the function reads whatever happens to be in the second argument location.
The failure may be an immediate crash, or it may silently use an incorrect length and corrupt data.
Structure layout changes can be even less visible. Consider:
If a library changes field order, type sizes, packing, or alignment, old machine code still reads fields at the byte offsets it was compiled to use. Field names do not exist in those machine instructions.
ABI breaks are especially disruptive because:
Shared libraries commonly use versioned names so incompatible releases can coexist. A new incompatible major version might use a new SONAME, allowing old binaries to continue requesting the old ABI.
Some platforms also use symbol versioning or compatibility shims to preserve older binary contracts while adding newer interfaces.
The Linux kernel treats its user-space ABI as a long-term compatibility boundary. If a kernel update arbitrarily changed syscall numbers, argument layouts, or result conventions, existing programs could stop working even though their source code had not changed.
Backend deployments encounter ABI constraints whenever they ship native code.
A precompiled native library must match the container or host's processor architecture and user-space ABI. An x86-64 binary does not natively execute AArch64 instructions. A binary built around glibc conventions may not be interchangeable with one built for a musl-based environment.
Native extensions for runtimes such as Python or Node.js also depend on binary contracts. A package can import correctly at the source-language level yet fail to load because its compiled extension targets the wrong architecture, C library, or runtime ABI.
Containers do not remove these requirements. A container packages user space but shares the host kernel, so its executables still need a compatible processor and kernel syscall ABI.
Understanding the ABI turns errors such as “wrong ELF class,” “exec format error,” unresolved symbols, and missing dynamic interpreters into concrete compatibility problems rather than mysterious deployment failures.
Booting and program loading can now be viewed as one chain of agreements:
ELF gives binaries structure. Calling conventions define how compiled code cooperates. The system-call ABI connects user programs to the kernel.
None of these contracts is visible in ordinary source code, but every compiled program depends on them.
Loading simulation...
A Linux system typically boots from firmware to a bootloader, then to kernel initialization, early user space, and PID 1. PID 1 starts the services and workloads that form the usable system.
Linux commonly loads user programs in ELF format. ELF headers identify the binary, sections organize link-time content, and program-header segments describe runtime memory mappings. Dynamically linked programs also rely on an ELF interpreter to load shared libraries before program startup reaches main.
An ABI defines the machine-level contract for binary format, register usage, calling conventions, data layout, symbols, and system calls. An API break can often be found and repaired during recompilation; an ABI break can invalidate already deployed binaries and cause runtime failures or silent corruption.
The essential mental model is:
Boot and program execution are chains of binary contracts, and every stage must understand the format and conventions established by the stage before it.
5 quizzes