When an ARM-based system powers on, there are several key software components that run to initialize the hardware and prepare the system for operation. Understanding the differences between these components – the bootloader, startup code, and bootstrap loader – is important for developers working on low-level software.
Bootloader
The bootloader is the first piece of software that runs when the system powers on. Its main responsibilities are:
- Initializing critical hardware components like RAM, clocks, and the MMU
- Locating, loading, and passing control to the kernel image
- Providing a user interface for selecting alternate boot configurations or performing low-level tasks like flashing a new firmware image
The bootloader runs in a special execution environment before the OS has been loaded. It has limited access to hardware resources but enough to accomplish its initialization tasks. On ARM systems, common bootloaders include U-Boot, Barebox, and proprietary vendor bootloaders.
Key properties of the bootloader:
- Executes first after reset
- Limited scope and capabilities
- Only runs during the boot phase
- Responsible for loading the kernel image
Startup Code
The startup code, also known as bootstrapping code, runs immediately after the kernel image is loaded into memory by the bootloader. Its main responsibilities are:
- Setting up the C runtime environment
- Initializing CPUs and core components like the GIC
- Transferring control to the main() function
The startup code provides a bridge between the bootloader environment and the kernel C environment. It is responsible for completing system initialization so that the kernel can begin executing.
Key properties of the startup code:
- Executes immediately after the kernel is loaded
- Written in C/assembly language
- Runs only once during boot
- Starts main kernel execution
Bootstrap Loader
The bootstrap loader is a simple piece of code that initializes just enough of the system to load a more fully featured bootloader. Its main responsibilities are:
- Initializing the RAM interface
- Configuring static memory controllers
- Loading the bootloader image from storage into RAM and transferring control to it
The bootstrap loader provides essential early initialization before handing off to more sophisticated boot firmware. It may have hard-coded configurations for the bootloader location, memory setup, and peripherals.
Key properties of the bootstrap loader:
- Executes first out of reset
- Minimal initialization and error handling
- Only task is loading the bootloader
- May be ROM-based or hard-coded flash
Comparison of Features
While the bootloader, startup code, and bootstrap loader work together to boot the system, they have some key differences:
Feature | Bootloader | Startup Code | Bootstrap Loader |
---|---|---|---|
Execution time | After bootstrap loader | After bootloader | First code executed |
Functional scope | Broad | Limited | Minimal |
Code size | Large | Medium | Small |
Execution duration | Longer | Brief | Very brief |
Handoff target | Kernel image | Kernel entry point | Bootloader image |
Boot Sequence Overview
Here is a high-level overview of the boot sequence on a typical ARM system using these components:
- After reset, the hard-coded bootstrap loader runs first. It initializes RAM and loads the bootloader image into RAM.
- The bootloader runs next. It does more complete system initialization and includes features like a user interface. It finds the kernel image and loads it into memory.
- The bootloader transfers control to the kernel startup code. The startup code sets up the C environment and kernel entry point.
- The startup code hands off to the main() kernel function. At this point, full kernel execution begins.
This sequence ensures the processor initializes in a progressive, modular way until the point where the kernel takes over normal system operation.
Bootloader and Kernel Relationship
The bootloader and kernel have distinct roles in the boot process:
- The bootloader’s job is to load the kernel image into memory and start execution at the kernel’s entry point.
- The kernel handles all aspects of system management once it takes control from the bootloader.
Ideally, a cleanly delineated handoff occurs between the bootloader and kernel. The bootloader does not depend on any kernel functions, and the kernel does not rely on any bootloader artifacts being present.
Some key ways the bootloader and kernel differ:
- The bootloader has limited scope focused on initialization tasks. The kernel has broad system capabilities.
- The bootloader runs in a restricted execution environment separate from the kernel space.
- The kernel is designed for long-term robustness. The bootloader only needs to run once.
Keeping the bootloader and kernel modular with well-defined interfaces promotes separation of concerns during system startup.
Startup Code and Main Kernel Relationship
The startup code serves as a bridge between the bootloader and kernel environments:
- It is invoked by the bootloader and handles any machine-dependent setup the kernel needs before main().
- It calls main() and transfers control to the kernel’s C entry point.
The startup code is part of the kernel package and provides a consistent, portable way to initialize the kernel C environment across platforms. This helps decouple the kernel from machine-specific details.
Some key aspects of the startup code → main() handoff:
- The startup code sets up stack pointers, CPU modes, and initialization data structures.
- main() inherits the execution environment created by the startup code.
- main() does not call back into the startup code during normal operation.
This clean handoff from bootstrap to bootloader to startup code to kernel is key for portability across ARM-based designs.
Conclusion
The bootloader, startup code, and bootstrap loader each perform specific roles in an ordered sequence to boot an ARM system:
- The bootstrap loader handles the earliest initialization before loading the bootloader.
- The bootloader initializes hardware and loads the kernel image.
- The startup code initializes the kernel’s C environment and transfers control to main().
Understanding the responsibilities and interactions between these software components is important for low-level development, debugging, and optimizing performance on ARM platforms.