Have you ever wondered what happens the moment you press the power button on your computer? Behind that brief pause, before your screen lights up, a complex series of processes is taking place. This article will dive into the fascinating world of firmware, exploring how different components interact during the boot process.
By understanding these connections, you will get a clearer picture of foundational elements that bring your system to life. Our primary focus will be on the Intel x86 architecture, but many principles apply across other architectures as well.
If you missed the first part of our series, click here to catch up. Now, let's uncover the mysteries behind the firmware.
To understand how the firmware components interact, we will explore the entire architecture with all its connected parts. The execution flow, shown in the diagram below, starts from the reset vector, which is part of the First-Stage Bootloader. From there, it progresses through various firmware stages:
Firmware or BIOS can generally be divided into two main parts, with a typically minimal interface between them:
The design of the platform firmware can be either monolithic, combining hardware initialization and boot functionality, or it can follow a modular and staged boot flow. The choice of design depends on the system requirements and may be preferable for certain devices.
The following diagram illustrates how different firmware components interact and can be used together to support the boot process (arrows indicate the sequence of execution):
If these diagrams seem complex now, don't worry. Review them again after reading this article, and they will be clearer.
This piece of firmware is designed to initialize computers and embedded systems with a focus on minimal hardware initialization: to do only what is absolutely needed, then pass control to Second-Stage Bootloader in order to boot the operating system. The FSBL doesn't load operating systems from storage media other than the flash chip. Since it only initializes the underlying hardware and doesn't handle boot media like hard drives, SSDs, or USB flash drives, another piece of software is required to actually boot an operating system.
Key Responsibilities of FSBL:
In the early days of computing, open-source software was not widely popular, and most BIOS implementations were proprietary. There are only a few available open solutions providing BIOS POST source code, such as Super PC/Turbo XT BIOS and GLaBIOS. These projects were designed to work on IBM 5150/5155/5160 systems and most XT clones.
However, the more well-known open-source BIOS implementations, like OpenBIOS and SeaBIOS, do not perform hardware initialization because they are not intended to run on bare hardware. But they are widely used as Second-Stage Bootloaders and run natively in virtual environments like QEMU and Bochs.
In any case, there's little chance that you will need to work directly with these early BIOSes or delve deeply into their specifics. But if you're interested in exploring, the mentioned repositories are a good starting point.
As far as current development trends go, there appears to be no ongoing development of proprietary BIOS solutions, and such projects have become obsolete in the face of modern alternatives.
The boot process follows a staged flow, starting from the left and moving to the right in the next figure. The timeline of the platform boot process is divided into the following phrases as indicated by yellow boxes:
ExitBootServices()
call.
This process and its execution phases are covered by the UEFI Platform Initialization (PI) Specification. However, there is also the UEFI Interface (indicated by the bold blue line in the picture), which is not part of the previous document and is described in the UEFI Specification. Although the names and frequent use of UEFI can be confusing, these two documents have different focuses:
Essentially, both specifications are about interfaces, but at different levels. For detailed information, you can access both specifications on the UEFI Forum website.
UEFI PI was initially designed as a unified firmware solution, not considering the distinction between first-stage and second-stage bootloaders. However, when we refer to UEFI as a First-Stage Bootloader, it includes the SEC, PEI, and early DXE phases. The reason we divide DXE into early and late stages is due to their different roles in the initialization process.
In the early DXE phase, drivers typically perform essential CPU/PCH/board initialization and also produce DXE Architectural Protocols (APs), which help isolate the DXE phase from the platform-specific hardware. APs encapsulate the details specific to the platform, allowing the late DXE phase to operate independently of the hardware specifics.
Detailed articles on how Coreboot works are coming soon. Follow my social media – they will be published very soon!
After the initial hardware setup is completed, the second stage comes into play. Its primary role is to set up a software interface between the operating system and platform firmware, ensuring that the OS can manage system resources and interact with hardware components.
The SSBL aims to hide hardware variations as much as possible, simplifying OS and application development by handling most of the hardware-level interfaces. This abstraction allows developers to focus on higher-level functionalities without worrying about the underlying hardware differences.
Key Responsibilities of SSBL:
Platform Information Retrieval: Obtains platform-specific information from the First-Stage Bootloader, including memory mapping, SMBIOS, ACPI tables, SPI flash, etc.
Run Platform Independent Drivers: Includes drivers for SMM, SPI, PCI, SCSI/ATA/IDE/DISK, USB, ACPI, network interfaces, and so on.
Services Implementation (aka Interface): Provides a set of services that facilitate communication between the operating system and hardware components.
Setup Menu: Offers a setup menu for system configuration, allowing users to adjust settings related to boot order, hardware preferences, and other system parameters.
Boot Logic: Mechanism to locate and load the payload (probably operating system) from available boot media.
The interface in the BIOS is known as BIOS services/functions/interrupt calls. These functions provide a set of routines for hardware access, but the specific details of how they are executed on the particular hardware of the system are hidden from the user.
In 16-bit Real Mode, they can be easily accessed by invoking a software interrupt via INT x86 assembly language instruction. In 32-bit Protected mode, almost all BIOS services are unavailable because of the different way segment values are handled.
Let's take for example Disk Services (INT 13h
), which provides sector-based hard disk and floppy disk read and write services using Cylinder-Head-Sector (CHS) addressing, as an example of how this interface can be used. Let's say we want to read 2 sectors (1024 bytes) and load them at memory address 0x9020, then the following code could be executed:
mov $0x02, %ah # Set BIOS read sector routine
mov $0x00, %ch # Select cylinder 0
mov $0x00, %dh # Select head 0 [has a base of 0]
mov $0x02, %cl # Select sector 2 (next after the
# boot sector) [has a base of 1]
mov $0x02, %al # Read 2 sectors
mov $0x00, %bx # Set BX general register to 0
mov %bx, %es # Set ES segment register to 0
mov $0x9020, %bx # Load sectors to ES:BX (0:0x9020)
int $0x13 # Start reading from drive
jmp $0x9020 # Jump to loaded code
If you're interested in how this service is written in SeaBios, have a look at src/disk.c.
Starts searching for a bootable device (it can be a hard drive, CD-ROM, floppy disk, etc.) by reading the first 512-byte sector (sector zero) from the devices.
The bootstrap sequence in the BIOS loads the first valid Master Boot Record (MBR) that it finds into the computer's physical memory at the physical address 0x7C00 (hint: 0x0000:0x7c00 and 0x7c0:0x0000 refer to the same physical address).
BIOS transfers control to the first 512 bytes of the payload. This loaded sector, which is too small to contain the entire payload code, serves the purpose of loading the rest of the payload from the bootable device. At this point, the payload can use the interface exposed by the BIOS.
It's noteworthy that BIOS specifications didn't exist in the early days. BIOS is a de facto standard - it works the way it worked on actual IBM PCs, in the 1980s. The rest of the manufacturers just reverse-engineered and made IBM-compatible BIOSes. As a result, there was no regulation to prevent BIOS manufacturers from inventing new BIOS functions or having overlapping functionalities.
As mentioned before, UEFI itself is just a specification and has many implementations. The most widely used one is TianoCore EDK II, an open-source reference implementation of the UEFI and PI specifications. While EDKII alone is not enough to create a fully functional boot firmware, it provides a solid foundation for most commercial solutions.
To support different First-Stage Bootloaders and provide a UEFI interface, the UEFI Payload project is used. It relies on the initial setup done and platform information provided by boot firmware to prepare the system for the UEFI environment.
The UEFI Payload uses the DXE and BDS phases, which are designed to be platform-independent. It offers a generic payload that can adapt to different platforms. In most cases, it doesn’t require any customization or platform-specific adjustments and can be used as-is by consuming platform information from the First-Stage Bootloader.
Variants of UEFI Payload:
Legacy UEFI Payload: Requires a parse library to extract necessary implementation-specific platform information. If the bootloader updates its API, the payload must also be updated as well.
Universal UEFI Payload: Follows the Universal Scalable Firmware (USF) Specification, using Executable and Linkable Format (ELF) or Flat Image Tree (FIT) as a common image format. Instead of parsing them itself, it expects to receive the Hand Off Blocks (HOBs) at the payload entry.
While the Legacy UEFI Payload works fine, the EDK2 community is trying to shift the industry towards the Universal UEFI Payload. The choice between payloads depends on your firmware components. For example, it's not possible to run the Legacy Payload with SMM support on Slim Bootloader without my patch. On the other hand, using the Universal Payload with coreboot requires a shim layer to translate coreboot tables into HOBs, a feature only available in the StarLabs EDK2 fork.
Every UEFI-compliant system provides a System Table that is passed to every code running in the UEFI environment (drivers, applications, OS loaders). This data structure allows a UEFI executable to access system configuration tables such as ACPI, SMBIOS, and a collection of UEFI services.
The table structure is described in MdePkg/Include/Uefi/UefiSpec.h:
typedef struct {
EFI_TABLE_HEADER Hdr;
CHAR16 *FirmwareVendor;
UINT32 FirmwareRevision;
EFI_HANDLE ConsoleInHandle;
EFI_SIMPLE_TEXT_INPUT_PROTOCOL *ConIn;
EFI_HANDLE ConsoleOutHandle;
EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *ConOut;
EFI_HANDLE StandardErrorHandle;
EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *StdErr;
//
// A pointer to the EFI Runtime Services Table.
//
EFI_RUNTIME_SERVICES *RuntimeServices;
//
// A pointer to the EFI Boot Services Table.
//
EFI_BOOT_SERVICES *BootServices;
UINTN NumberOfTableEntries;
EFI_CONFIGURATION_TABLE *ConfigurationTable;
} EFI_SYSTEM_TABLE;
Services include the following types: Boot Services, Runtime Services, and Services provided by protocols.
UEFI abstracts access to the device by setting up UEFI Protocols. These protocols are data structures containing function pointers and are identified by a Globally Unique IDentifier (GUID) that allows other modules to locate and use them. They can be discovered through Boot Services.
A UEFI driver produces these protocols, and the actual functions (not pointers!) are contained within the driver itself. This mechanism allows different components within the UEFI environment to communicate with each other and ensures that the OS can interact with devices before loading its own drivers.
While some protocols are predefined and described in the UEFI specification, firmware vendors can also create their own custom protocols to extend the functionality of a platform.
Boot Services
Provide functions that can be used only during boot time. These services remain available until the EFI_BOOT_SERVICES.ExitBootServices()
function is called (MdeModulePkg/Core/Dxe/DxeMain/DxeMain.c).
Pointers to all boot services are stored in the Boot Services Table (MdePkg/Include/Uefi/UefiSpec.h):
typedef struct {
EFI_TABLE_HEADER Hdr;
...
EFI_GET_MEMORY_MAP GetMemoryMap;
EFI_ALLOCATE_POOL AllocatePool;
EFI_FREE_POOL FreePool;
...
EFI_HANDLE_PROTOCOL HandleProtocol;
...
EFI_EXIT_BOOT_SERVICES ExitBootServices;
...
} EFI_BOOT_SERVICES;
Runtime Services
A minimal set of services are still accessible while the Operating System is running. Unlike Boot Services, these services are still valid after any payload (for example, OS bootloader) has taken control of the platform via a call to EFI_BOOT_SERVICES.ExitBootServices()
.
Pointers to all runtime services are stored in Runtime Services Table (MdePkg/Include/Uefi/UefiSpec.h):
typedef struct {
EFI_TABLE_HEADER Hdr;
...
EFI_GET_TIME GetTime;
EFI_SET_TIME SetTime;
...
EFI_GET_VARIABLE GetVariable;
EFI_GET_NEXT_VARIABLE_NAME GetNextVariableName;
EFI_SET_VARIABLE SetVariable;
...
EFI_GET_NEXT_HIGH_MONO_COUNT GetNextHighMonotonicCount;
EFI_RESET_SYSTEM ResetSystem;
...
} EFI_RUNTIME_SERVICES;
The picture below shows the timeline for boot and runtime services, so you can see exactly when each one is active.
The UEFI spec defines a boot policy engine called the UEFI boot manager. It will attempt to load UEFI applications in a specific order. This order and other settings can be configured by modifying global NVRAM (nonvolatile random-access memory) Variables. Let's discuss the most important of them:
Boot####
(####
is replaced by a unique hex value) — a boot/load option.BootCurrent
— the boot option used to start the currently running system.BootNext
— the boot option for the next boot only. This replaces BootOrder
for one boot only and is deleted by the boot manager after first use. This allows you to change the next boot behavior without changing BootOrder
.BootOrder
— the ordered boot option load list. The boot manager tries to boot the first active option in this list. If unsuccessful, it tries the next option, and so on.BootOptionSupport
— the types of boot options supported by the boot manager.Timeout
— the firmware's boot managers timeout, in seconds, before automatically choosing the startup value from BootNext
or BootOrder
.
These variables can be easily obtained from Linux by using efibootmgr(8):
[root@localhost ~]# efibootmgr
BootCurrent: 0000
Timeout: 5 seconds
BootOrder: 0000,0001,2001,2002,2003
Boot0000* ARCHLINUX HD(5,GPT,d03ca3cf-1511-d94e-8400-c7a125866442,0x40164000,0x100000)/File(\EFI\ARCHLINUX\grubx64.efi)
Boot0001* Windows Boot Manager HD(1,GPT,6f185443-09fc-4f15-afdf-01c523565e52,0x800,0x32000)/File(\EFI\Microsoft\Boot\bootmgfw.efi)57a94e544f5753000100000088900100780000004200430044039f0a42004a004500430054003d007b00390064006500610038003600320063002d1139006300640064002d0034006500370030102d0061006300630031002d006600330032006200330034003400640034003700390035007d00000033000300000710000000040000007fff0400
Boot0002* ARCHLINUX HD(5,GPT,d03ca3cf-1511-d94e-8400-c7a125866442,0x40164000,0x100000)
Boot2001* EFI USB Device RC
Boot2002* EFI DVD/CDROM RC
Boot2003* EFI Network RC
Let's take a look at the booting by relying on the code snippet above. UEFI will start iterating the BootOrder
list. For each entry in the list, it looks for a corresponding Boot####
variable — Boot0000
for 0000, Boot2003
for 2003, and so on. If the variable does not exist, it continues to the next entry. If the variable exists, it reads the contents of the variable. Each boot option variable contains an EFI_LOAD_OPTION
descriptor that is a byte-packed buffer of variable length fields (it's just the data structure).
The data structure is described in [MdePkg/Include/Uefi/UefiSpec.h][ https://github.com/tianocore/edk2/blob/edk2-stable202405/MdePkg/Include/Uefi/UefiSpec.h#L2122)
typedef struct _EFI_LOAD_OPTION {
/// The attributes for this load option entry.
UINT32 Attributes;
/// Length in bytes of the FilePathList.
UINT16 FilePathListLength;
/// The user readable description for the load option.
/// Example: 'ARCHLINUX' / 'Windows Boot Manager' / `EFI USB Device`
// CHAR16 Description[];
/// A packed array of UEFI device paths.
/// Example: 'HD(5,GPT,d03ca3cf-1511-d94e-8400-c7a125866442,0x40164000,0x100000)/File(\EFI\ARCHLINUX\grubx64.efi)'
// EFI_DEVICE_PATH_PROTOCOL FilePathList[];
/// The remaining bytes in the load option descriptor are a binary data buffer that is passed to the loaded image.
/// Example: '57a9...0400' in Boot0001 variable
// UINT8 OptionalData[];
} EFI_LOAD_OPTION;
At this point, the firmware will examine a Device Path (EFI_DEVICE_PATH_PROTOCOL). In most cases, our computer is booted up from a storage device (Hard Drive/SSD/NVMe/etc). So, the Device path would contain HD(Partition Number, Type, Signature, Start sector, Size in sectors)
node.
Note: If you're interested in how to translate other paths, read UEFI Specification v2.10, 10.6.1.6 Text Device Node Reference.
UEFI will look into the disk and see if it has a partition matching the node. If it exists, it should be labeled with a specific Globally Unique IDentifier (GUID) that marks it as the EFI System Partition (ESP). This one is formatted with a file system whose specification is based on the specific version of the FAT file system and is named EFI File System; actually, it's just a regular FAT12/16/32.
File(\Path\To\The\File.efi)
, then UEFI will look for that specific file. For example, the Boot0000
option contains File(\EFI\ARCHLINUX\grubx64.efi)
.\EFI\BOOT\BOOT{arch}.EFI
(BOOTx64.EFI
for amd64 or BOOTia32.EFI
for i386/IA32). This mechanism allows the bootable removable media (for example, a USB drive) to work in UEFI; they just use a fallback boot path. For example, the Boot0002
option will use this mechanism.
Note: All Boot####
options mentioned above refer to the boot options displayed in the example output of efibootmgr.
In both cases, the UEFI Boot Manager will load the UEFI Application (it might be OS bootloader, UEFI Shell, utility software, System setup, and whatever) into memory. At this moment, control is then transferred to the UEFI application’s entry point. Unlike BIOS, the UEFI application can return control to the firmware (besides the situation, when the application takes over control of the system). If it happens or anything goes wrong, the Boot Manager moves on to the next Boot####
entry, and follow exactly the same process.
The specification mentions that the boot manager can automatically maintain the database variables. This includes removing load option variables that are not referenced or cannot be parsed. Additionally, it can rewrite any ordered list to remove any load options without corresponding load option variables.
The above text describes the UEFI booting. Also, UEFI firmware can run in Compatibility Support Module (CSM) mode that emulates a BIOS.
A piece of software started by the firmware (usually Second-Stage Bootloader) and using its interface to load the OS kernel. It can be as complex as an OS, offering features such as:
The common designs of these programs are beyond the scope of this article. For a detailed comparison of popular OS bootloaders, you can refer to the ArchLinux wiki and the Wikipedia article.
Windows system uses its proprietary OS bootloader known as the Windows Boot Manager (BOOTMGR).
Firmware is no longer a small, complex piece of code. It has become a huge amount of complex code, and current trends only contribute to this. We can run Doom, Twitter, and many other interesting applications on it.
Understanding the overall architecture helps to organize these components in your mind. By examining the design of existing firmware, you gain insight into the fascinating process that unfolds each time a computer is powered on. This top-down perspective not only clarifies the role of each part but also highlights the sophisticated and evolving nature of modern firmware systems.