AlgoMaster Logo

Boot, ELF, and the ABI

23 min readUpdated August 7, 2026
Listen to this chapter
Unlock Audio

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.

From Power-On to User Space

A typical Linux system starts through the following stages:

  1. Power on or reset.
  2. Firmware runs.
  3. The bootloader takes over.
  4. Linux kernel initialization begins.
  5. Early user space and the root filesystem are prepared.
  6. PID 1 starts.
  7. System services and user applications run.

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.

Stage 1: Firmware

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:

  • Initializing essential processor, memory, and chipset state
  • Discovering bootable devices
  • Selecting a configured boot entry
  • Loading and starting boot code
  • Providing hardware and configuration information to the next stage

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.

Stage 2: The Bootloader

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:

  1. Selects a kernel, sometimes through a boot menu.
  2. Loads the kernel image into memory.
  3. Loads an initial RAM filesystem if one is configured.
  4. Supplies a kernel command line and platform information.
  5. Transfers control to the kernel's entry point.

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.

Stage 3: Kernel Initialization

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:

AreaInitialization purpose
Memory managementDiscover usable memory and establish kernel mappings
Interrupts and timersAllow the kernel to respond to hardware events and time
CPU managementBring processor cores into a controlled operating state
SchedulingPrepare to run independent execution contexts
Devices and driversDiscover or initialize hardware needed during boot
FilesystemsMake 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:

  1. A minimal CPU environment is established.
  2. Memory and interrupt handling are set up.
  3. Core kernel subsystems start.
  4. Drivers and storage become available.
  5. The root filesystem is mounted.
  6. The first user-space program is started.

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.

Stage 4: Early User Space and the Real Root

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.

  1. The kernel starts the early environment.
  2. initramfs tools prepare storage.
  3. The real root filesystem becomes available.
  4. The system switches to the real root.
  5. The final init system starts, or replaces the early init.

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.

Stage 5: PID 1

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 vs. Loading a Program

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 kindPath
Machine startupFirmware, bootloader, kernel
Program startupRunning 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.

What Is ELF?

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

The ELF header describes the file as a whole.

Important fields include:

FieldPurpose
Magic bytesIdentify the file as ELF
ClassIndicate a 32-bit or 64-bit format
Data encodingIndicate byte order
TypeIdentify an executable, shared object, object file, or core dump
MachineIdentify the required processor architecture
Entry pointGive the virtual address where execution begins
Program-header locationLocate the runtime segment descriptions
Section-header locationLocate 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 typeTypical use
ET_RELRelocatable object file such as module.o
ET_EXECTraditional executable
ET_DYNShared library or position-independent executable
ET_CORECore 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: The Linker's View

Sections organize an ELF file for compilation, linking, relocation, and debugging.

Common sections include:

SectionContents
.textCompiled machine instructions
.rodataRead-only constants
.dataInitialized writable global and static data
.bssSpace for zero-initialized global and static data
.symtabA full symbol table, when retained
.dynsymSymbols 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.

Segments: The Loader's View

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 typePurpose
PT_LOADDescribe bytes and memory that must be loaded
PT_INTERPName the dynamic linker for a dynamically linked program
PT_DYNAMICDescribe information needed for dynamic linking
PT_GNU_STACKDescribe 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...

How Linux Starts an ELF Program

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:

  1. The kernel opens the executable.
  2. It verifies the ELF identity, architecture, and layout.
  3. It reads the program headers.
  4. It creates process memory mappings for loadable segments.
  5. It builds the initial user stack.
  6. It loads a dynamic interpreter if one is required.
  7. It enters the interpreter or the executable.

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.

  1. The ELF entry point is reached.
  2. _start runs.
  3. C runtime initialization happens.
  4. main(argc, argv) is called.

When main returns, runtime code translates that result into process termination.

Dynamically Linked Programs

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.

  1. The kernel ELF loader runs.
  2. It maps the main executable and the dynamic linker.
  3. The dynamic linker loads the shared libraries.
  4. Symbols and addresses are resolved.
  5. Program startup code runs.

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.

Inspecting an ELF File

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.

What Is an ABI?

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.

LevelWhat it covers
APISource-level names, types, and function declarations
ABIRegisters, stack layout, symbols, binary formats, data layout

The compiler is what turns the first into the second.

An ABI can define:

  • Instruction-set and processor requirements
  • Sizes and alignment of fundamental data types
  • Structure and object layout
  • Function calling conventions
  • Which registers a function must preserve
  • Symbol naming and dynamic-linking rules
  • Executable and object-file formats
  • The system-call interface to the kernel

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.

Calling Conventions

A calling convention defines how one compiled function calls another.

It answers questions such as:

  • Where are function arguments placed?
  • Where is the return value placed?
  • How must the stack be aligned?
  • Which registers may the called function overwrite?
  • Which registers must it restore before returning?

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.

Function-Call ABI vs. System-Call ABI

An ordinary function call and a Linux system call use related but different conventions.

On x86-64 Linux:

PurposeOrdinary System V function callLinux system call
Operation selectionCall target addressNumber in rax
Argument 1rdirdi
Argument 2rsirsi
Argument 3rdxrdx
Argument 4rcxr10
Argument 5r8r8
Argument 6r9r9
Return valueraxrax

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 vs. ABI Compatibility

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.

ChangeAPI compatible?ABI compatible?
Change only a function's internal implementationUsually yesUsually yes
Add a new function without removing old onesUsually yesUsually yes
Remove a declaration but keep the old binary symbolNo for new builds using itYes for existing binaries
Change a function's parameters and replace its symbolNoNo
Change public structure field offsetsSource may still compileNo for binaries using the old layout
Change a required calling conventionSource can be recompiledNo 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:

  1. Source code uses the old declaration.
  2. The compiler sees the new header.
  3. A build error identifies the mismatch.
  4. The developer updates the code and recompiles.

An ABI break affects code that has already passed compilation:

  1. An existing executable runs.
  2. It loads a changed shared library.
  3. Arguments, symbols, or layouts no longer match.
  4. The result is a load failure, a crash, wrong data, or memory corruption.

There may be no compiler present to catch the problem.

Why an ABI Break Is Worse

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:

  • Users may not possess the source code needed for recompilation.
  • Many applications and plugins may depend on the same shared library.
  • The failure may appear only at runtime.
  • A mismatch can produce silent memory corruption rather than a clear error.
  • Every dependent binary may need to be rebuilt and redeployed together.

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.

ABI Compatibility in Backend Systems

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.

The Chain of Binary Contracts

Booting and program loading can now be viewed as one chain of agreements:

  1. The firmware contract is honored.
  2. The bootloader understands the platform and the kernel boot format.
  3. The kernel understands the hardware and its boot parameters.
  4. The kernel ELF loader understands program headers.
  5. The dynamic linker understands shared objects and symbols.
  6. Functions agree on calling and data-layout conventions.
  7. The program uses the kernel's stable system-call ABI.

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...

Summary

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.

Quiz

Boot, ELF, and the ABI Quiz

5 quizzes