© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
S. Banik, V. ZimmerSystem Firmwarehttps://doi.org/10.1007/978-1-4842-7939-7_3

3. Understanding the BIOS and Minimalistic Design

Subrata Banik1   and Vincent Zimmer2
(1)
Bangalore, Karnataka, India
(2)
Tacoma, WA, USA
 

“Simplicity is the ultimate sophistication.”

—Leonardo da Vinci

In the modern computing era, we are surrounded by many consumer electronics devices. These devices have extended their boundary from being computing “only” devices to “companion” devices, namely devices that are used for daily needs including smart home aids, smart appliances, and smart gadgets. From an end user’s standpoint, the device is operational only after it is booted to its GUI and/or connected with the network to operate in faceless mode (example: soft robotic devices) using a remote or applications. The entire device bring-up process, starting with the user pressing the device’s power button to the device getting booted to the UI, remains unnoticed by its user. This process is responsible for bringing the device to life after executing a series of complex CPU initialization sequences, performing the physical memory (also known as RAM) initialization to let applications use the main memory for improved efficiency, increasing the capabilities, and finally performing the chipset initialization to boot the OS (Windows, Linux or one of its flavors, or macOS) from the boot media.

The tiny piece of programming sequence that is responsible for such operations is known as the BIOS. The BIOS resides in the target hardware itself and is fetched by the processor upon coming out from the reset as part of a power-on sequence.

There are many different types of firmware that exist based on the underlying hardware requirements like system boot firmware, device firmware, platform security firmware, and manageability firmware. Each firmware has its minimum requirements to complete its assigned task to initialize the underlying hardware and/or abstract the hardware to higher level applications. This chapter will focus on the system boot firmware and provide details about what a BIOS is, what basic characteristics the firmware has to perform to qualify as a BIOS, and the minimum requirements to design a BIOS.

This information will be helpful for you when you go to create your own system firmware for a target embedded system. It won’t be possible to do unless you understand the basic expectation from the BIOS. The upcoming chapters will guide you in choosing the appropriate boot firmware for the target hardware.

What Is the BIOS?

The BIOS, as the acronym suggests, is responsible for the basic input/output system. The term BIOS is relevant with the word “boot.” Typically, in computer terminology, the boot is referred to as bootstrapping, which is the startup process that takes place automatically after a computer power-on without any manual input. Thus, a BIOS is known as a boot program or boot sequence specially designed for such usage, where it starts its execution by performing a bunch of instructions at the early boot phase in the absence of physical memory and later performs a series of operations to initialize the main memory where it can load dynamic libraries, install several services for efficient boot usages, and finally initialize the boot media (example: eMMC, SATA, SSD, NVMe, UFS, or USB) or boot from a network (example: PXE boot or netboot) to load the OS and associated drivers and applications.

The term bootloader can be used in many ways in computer programming; in many cases, the bootloader is also referred to as a special operating system software block (e.g., boot.efi as the EFI bootloader to boot an EFI-aware OS; ARM Core Bootloader is typically used by smartphone OSes) that is responsible for loading the operating system from the block device into system memory. This chapter and Chapter 4 will use bootloader as semantics of booting, as in booting up an embedded system. Hence in the context of this book, bootloader is used as a synonym for the BIOS when it comes to initializing the CPU and hardware and still relying on the OS loader to load and run the OS.

Working Principle of BIOS

As mentioned, the BIOS is the first executable piece of code run by the processor after the user presses the power button. Thus, the boot firmware needs to adhere to certain characteristics to qualify as a BIOS:
  • Self-supporting: A bootloader is a boot program for bringing up the device without any user input, hence the design principle needs to be self-supportive. Self-supportive means that the bootloader is capable of starting its execution from its residence. Typically, it’s the reset vector (refer to later sections for details) which is an address, where upon a reset, the processor goes to find the first executable instruction. This address is dependent on the target hardware architecture, and the bootloader needs to get patched into this address. The entire bootloader boot sequence is designed such that it doesn’t depend on any user interaction to perform the basic hardware initialization. All the boot phases are part of the bootloader hardware initialization process, which is written in such a way that it’s aware of the prerequisites prior to starting execution of the next phase. For an example, if the boot program is dependent on the main memory, then the stage prior to that will first ensure that the main memory is being initialized and readily available.

  • Simple: The whole purpose of the bootloader is to perform initialization of the underlying hardware. It collects the most diverse hardware data and prepares the device list while booting to higher-level software like the OS. Post booting to the OS, the drivers and applications can make use of the hardware without knowing the exact programming details to initialize the hardware interfaces. The expectation from the boot program is a simple interface without any high-level programming language due to the fact that the bootloader starts its execution from a state where RAM is not even available, and it needs a simple robust mechanism during the boot phase to access the hardware registers directly.

  • Boot: When the hardware initialization process is complete, the bootloader will have a boot device list prepared. Depending on the type of bootloader, it can either decide to transfer the call to another firmware called a payload or continue the same to find the special boot signature (or boot record) from the available boot devices to boot to the kernel. If it fails to find the boot signature, an unsuccessful attempt to boot to the OS would result in a recovery boot where the device is dependent on booting from a removable recovery media. If the bootloader and payload are independent firmware binaries, it needs a standard communication method to pass the hardware and platform information to know the possible boot devices.

The BIOS design aspect that this chapter is going to highlight is with an assumption that the bootloader is only responsible for performing the hardware initialization, preparing the boot device lists, and passing that information to the payload. The payload is in charge of booting to an operating system or equivalent to make use of the embedded system.

Where Does the BIOS Reside?

Based on the platform reset architecture, the BIOS can reside in two different places:
  1. 1.
    The BIOS is stored in the flash memory (SPI NOR) on the target motherboard hardware. This is the widely used platform design where the SPI NOR is used as firmware boot media. SPI NOR designates the serial peripheral interconnect (SPI) bus upon which the NOR flash is attached. NOR flash is distinct from NAND in that it is memory mapped, byte-addressable, bit and byte writable but only bulk erasable, with the erase operation spanning either an 8KiB or 64KiB section of the device. Typically, on x86 platforms, the default reset architecture supports the boot from SPI NOR. Figure 3-1 shows the high-level block diagram of an IA-based platform to highlight where the BIOS is located in the hardware.
    Figure 3-1

    Hardware block diagram to locate the BIOS

     
  2. 2.

    Alternatively, on smaller computing devices the BIOS is located as part of the block device. This is a cost-effective approach where the BIOS is part of the block device boot partition (BP) and the OS is located in the general purpose partition (GPP). Most handheld devices follow this reset mechanism.

     

All discussions in this chapter are based on the assumption that the BIOS resides in the SPI flash (as shown in Figure 3-1), which is a slave device attached to the SPI controller.

BIOS Work Model

The BIOS works as an intermediate layer between the underlying hardware and the target operating system, as shown in Figure 3-2. As soon as the CPU comes out from the reset, the bootloader needs to get the boot process going. The first major task that the BIOS supports is the initialization of the main memory to load the operations into the main memory, which is essential for the next set of tasks.
Figure 3-2

High-level function of a bootloader

In the second step, the BIOS performs the chipset and CPU initialization and loads the kernel or the high-level system software that controls the devices through drivers. Apart from this, there is still some work that the bootloader performs that remains unnoticed in the boot process. Figure 3-3 provides the system simplified boot flow so you can understand the bootloader work model. This boot flow includes
  • Pre-CPU reset: By definition, a bootloader is designed to be self-supporting. Hence the firmware layout as part of the SPI flash has to be explicit to point to the required binaries (ROM patch) for associated microcontrollers that take part in the prior CPU reset activities. The final BIOS binary generation (described in more detail as part of the “Stitching Tools” section in Chapter 2 of Firmware Development: A Guide to Specialized Systemic Knowledge) process needs to ensure that upon the CPU release from reset it is able to jump into an address where the bootloader is located.

Note

In the most simplistic platform design, where no other microcontrollers are involved prior to the CPU reset process, you certainly can ignore the pre-CPU reset sections from the BIOS work model.

  • POST (Power-on self-test): The boot program responsible for performing the required hardware initialization. Due to the size restriction where only a limited portion of the SPI flash is memory mapped and physical memory is not available, the bootloader tasks can be divided into two major phases as Stage 1 (a.k.a. pre-memory phase) and Stage 2 (a.k.a. post-memory phase).
    • Stage 1: Typically, this is executed as a part of the SPI mapped memory or part of the temporary memory in the absence of the real main memory. All modules that are part of Stage 1 are mapped as eXecute-in-Place (XIP) where modules are executed in a host address space where the SPI flash is being mapped.

    • Stage 2: Main memory is initialized as part of Stage 1, and it loads Stage 2 into the memory. Stage 2 is executed from main memory. It is responsible for performing the hardware initialization, creating the device lists, and passing the platform-centric information to the OS loader/payload stage.

  • OS loader/payload: Depending on the type of bootloader, the OS loader may or may not be part of the bootloader program. Regardless of where the payload is located, the bootloader passes a set of information that is required for the OS loader to initialize an input, an output device (optional to display some informative text or take some special key sequence), and most importantly the boot devices list to boot to the OS. Chapter 6 describes the payload in more detail.

  • After Life (AL): Typically, in normal eyes the bootloader is only responsible for performing the most essential hardware initialization to boot to the OS and its job ends once the OS loader hands over the control to the boot manager. But that’s not the case anymore with modern operating systems. As highlighted before in the “BIOS Work Model” section, that bootloader works as a mediator between hardware and OS. Hence, certain pieces of code stay in the system memory even after the bootloader has transited. These types of services are referred to as bootloader’s AL in this book.
    Figure 3-3

    Bootloader simplified boot flow

Types of BIOS

The BIOS in modern consumer electronics devices is responsible for initializing and testing the system hardware components and loading an operating system from a mass memory device. Although BIOS is a generic terminology used to initialize the underlying hardware as mentioned in the “BIOS Work Model” section (performing POST operations), other features like the OS loader and AL characteristics make it a product differentiator for each platform. The platform owner goes with their own specification of BIOS firmware that suits the target hardware and operating system needs the most. For an example, coreboot is the official boot firmware solution used on the Chrome OS platform, whereas other Windows-based platforms rely on UEFI to provide the required boot and AL (runtime) services.

Based on the wide usage model across client, servers, IoT, and automotive platforms, there are different types of bootloaders:
  • coreboot: An extended firmware for embedded platforms that is designed with the principle of being open, secure, simple, and offering an instant boot experience for its users. coreboot is an Open Source Firmware (OSF) project that aims to provide lightweight and lightning fast performance with only the bare minimum hardware initialization to load and run operating systems. In fact, coreboot doesn’t by default come with any OS loader so it’s just meant to perform the hardware initialization. It needs a dedicated payload to load and run the target operating system.

  • UEFI (Unified Extensible Firmware Interface): An interface created to communicate between the OS and platform firmware. It is a specification that is used to write the firmware program that performs basic hardware initialization (most similar to what’s done by the traditional BIOS) to hand over the platform to its operating system. There are other specifications as part of UEFI that provide details on internal interfaces used to communicate between different parts of the platform firmware. UEFI has the OS visible interfaces in the main UEFI specification and the underlying interfaces in the platform initialization (PI) specification. There are several reference implementations of UEFI and PI, including the EFI Developer Kit II hosted on tianocore. Tiano was the codename of the initiative that created the precursor to PI, namely the Intel Platform Innovation Framework for the Extensible Firmware Interface, or “Framework” specifications, and the original implementation known as the EFI Developer Kit (EDK), which was the precursor to EDKII.

  • SBL (Slim Bootloader): An open-source boot firmware build with the principle of being small, fast, secure, extensible, and configurable running on Intel x86 architecture. SBL is designed with the modular approach by providing hardware initialization and then launching a payload to boot to the OS. SBL leverages the build system and library infrastructure of EDKII.

  • U-Boot (Universal Boot Loader): U-Boot is an open source bootloader consisting of a first stage (where U-Boot is responsible for configuring the memory controller and performing the hardware configuration) and a second stage (where it performs the required operations to load an OS from the available device lists) of a bootloader. Typically, it is used as a bare-bone bootloader for embedded systems or evaluation hardware. It is available almost for all possible CPU architectures (ARM, RISC-V, x86, etc.). U-boot also has an implementation layer supporting UEFI that is independent of the PI specification and EDKII reference code.

  • Trusted Firmware: Provides an open source reference implementation of secure world software for ARM architecture. This specification allows SoC developers and OEMs to implement their required bootloader code by complying with the implementation specification. The purpose of Trusted Firmware is to create a foundation for trusted execution environments (TEEs) on CPUs or secure processing environments (SPEs) on microcontrollers where it reduces duplicate efforts by adhering to standardizing on a single implementation framework.

Chapter 4 will provide the detailed architecture overview of each boot firmware type.

Some of these boot firmwares don’t have an OS loader integrated by default so they need a dedicated payload to meet the OS loading characteristics of a BIOS firmware. A few popular ones include
  • Tianocore: An open source implementation of the UEFI payload that was developed using the EDKII and PI specifications. coreboot only performs the hardware initialization and then relies on the payload to handle the boot logic. The Tianocore payload can be integrated into the coreboot boot image; residing in the SPI flash would load the UEFI-aware operating system. Figure 3-4 shows the different usage models of Tianocore as a payload.
    Figure 3-4

    Different usages of the Tianocore payload

  • Depthcharge: The bootloader for the Chrome OS platform. The main goal of Depthcharge is to provide a small, simple, secure (using vboot), and efficient payload, integrated with coreboot to boot to the Chrome OS.

  • LinuxBoot: An open source firmware initiative to replace any proprietary firmware modules meant to boot Linux or an equivalent OS with a Linux kernel. The Linux kernel works here as a payload located in the SPI flash as part of the BIOS binary. LinuxBoot is intended to remove unnecessary code from the boot firmware (UEFI DXE phase, coreboot ramstage, SBL stage 1B, U-Boot SPL) and relies on the Linux kernel and runtime services to perform the boot logic to boot to the OS.

In modern computing devices, usage of the legacy BIOS has faded at a rapid speed. The legacy BIOS had its own complexity in programming the native hardware using assembly language or relying too much on device-specific firmware like Option ROM (OpROM), which could increase the attacking surface. Based on functionality, adaptability, and operating speed, a platform owner decides which firmware to choose from the available list of BIOS (boot firmware and OS loader) options to replace the legacy BIOS concept like OpROM with a modular and driver-based approach while initializing the hardware and booting to the OS.

Designing a Minimalistic Bootloader

It’s almost difficult for end users to differentiate the presence of the BIOS from higher-level system software like operating systems, drivers, and applications on a production system. It’s expected that end users and the platform owners of the device only bother knowing about the details about the system hardware and the capabilities of the operating system running on the device. But it’s also important to understand the underlying bootloader design as well for several reasons:
  • The underlying boot firmware (i.e., the BIOS) provides consistent behavior across different architectures (e.g., ARM, x86, RISC V, etc.).

  • Firmware, being closest to the hardware, is meant to provide better control of the platform.

  • It abstracts the underlying hardware by providing a flexible interface to the OS.

  • Dealing with bugs: Working on a real hardware platform increases the chances of working around hardware or SoC defects. Firmware is close to hardware and provides the lowest turnaround time to get the bug fixed without altering the hardware or updating the OS. Updating the BIOS is thus comparatively easier.

The above reasons should be enough to motivate you to understand the underlying bootloader. Also, this book is committed to helping you create your own system firmware and be able to boot the target embedded systems. Prior to that, it’s important to understand the bootloader’s minimum requirements and design schema.

The different types of BIOSes provide several different frameworks or interfaces to ease the OS-to-hardware communication. But the underlying working model is almost the same across different bootloaders. Hence this section provides the minimalistic bootloader design with the required sequence in which a typical BIOS performs all recommended hardware initialization even on a cross-architecture platform.

Minimalistic Bootloader Design on x86 Platform

With growing customer demand for creating more performance-oriented systems with a limited power envelope, it forces the SoC vendors to bring more efficient microcontrollers into the SoC design along with the native CPU. This makes the reset architecture even more complicated to ensure that firmware running in these microcontrollers also gets periodic updates, since they aren’t exempt from defects either. Hence the firmware image as part of the SPI flash doesn’t just hold the BIOS region alone; rather it needs to have ROM patch binaries for all possible microcontrollers that take part into the SoC reset flow. Due to the fact that the bootloader design expects it to be self-supporting, here is the list of items that you need to consider when designing a minimalistic bootloader on x86 platform.

SPI Flash Layout

All boot-critical firmware components are stitched together, and the integrated firmware image is called the IFWI (integrated firmware image). The IFWI is stored in the SPI flash. The integration process for firmware ingredients is important on the x86 platform to ensure that all microcontrollers prior to the CPU reset are able to apply the ROM patch and let the CPU come out from reset. It’s important to ensure that upon reset the CPU is able to fetch code from the BIOS region without any manual intervention.

Figure 3-5 depicts the firmware integration on a typical x86 platform.
Figure 3-5

Firmware integration process

Refer to Figure 3-6 for the detailed flash layout.
Figure 3-6

IFWI flash regions are mapped from flash descriptors

Region

Name

Description

0

Descriptor

In the SPI controller, a size of 4KB flash descriptor, located at the base of the SPI flash, splits the flash layout into regions and provides access control to each region.

Here are the flash descriptor region register details:

Flash Region 0 (flash descriptor) register

Bit range

Default

Access

Description

30:16

0h

RO/V

Region limit (RL)

This specifies address bits 26:12 for the Region 0 limit. The value in this register is loaded from the contents in the flash Descriptor.FLREG0 region limit.

14:0

0h

RO/V

Region base (RB)

This specifies address bits 26:12 for the Region 0 base. The value in this register is loaded from the contents in the flash Descriptor.FLREG0 region base

Similarly, there are other flash region registers to describe the base and limit for other flash regions as part of the IFWI image as below. The flash controller must have a default region base of 0x7FFF and region limit to 0x0 within the flash controller in case the number of regions specified are not in use.

Region

Name

Register

Calculate base and limit

1

BIOS

Flash Region 1 (BIOS) register

Region base (RB) = Flash (Descriptor.FLREGx & 0x00000FFF ) << 12

Region limit  (RL) = Flash (Descriptor.FLREGx & 0x0FFF0000 ) >> 4 | 0xFFF

x = Flash region index

2

Manageability ingredients

Flash Region 2 (IFWI) register

3

GigE firmware

Flash Region 3 (GbE) register

4

Platform data

Flash Region 4 (platform data) register

Here are details about the other flash region sections as part of the IFWI:

Region

Name

Description

1

BIOS

The BIOS region is at the top of the SPI flash so that this region can be mapped into system memory. By default, all x86 platforms map a maximum of 16MB of SPI flash at the top of 4G in the host address space.

This map is controlled by the BIOS Decode Enable register either as part of LPC or SPI based on where SPI flash is attached.

This register allows PCH to simply decode these ranges as memory access when enabled for the SPI flash.

Bit range

Default value

Description

Bit 0

1h

Enables decoding of 1MB of the following BIOS range: 0xFF00_0000 : 0xFF0F_FFFF

...

….

….

Bit 15

1h

Enables decoding of 1MB of the following BIOS range: 0xFFF8_0000 : 0xFFFF_FFFF

2

Manageability

ingredients

This region is divided into two parts: initialization section (code) and data region. The initialization section starts from the offset 4KB, covering the ROM patch. If the ROM patch doesn’t exist, then the region size is 0. The data region is followed by the code region.

3

GbE firmware

The GbE region and any optional flash regions will be added as required into the SPI flash layout after the manageability region and prior to BIOS region.

4

Platform data

The above sections provided detailed descriptions so you can understand how to create a SPI flash layout and map the BIOS region and several other boot-critical regions while creating an IFWI image, as applicable based on the platform reset architecture. On x86 platforms, the CPU reset is not guaranteed and the bootloader won’t be able to perform its operation if you haven’t followed the proper recommendations when creating the SPI flash layout. If you want to create your own system firmware for x86-based embedded systems, the proper understanding of these sections is important.

Take a look at the sample SPI flash layout (64MB) from a Xeon processor-based server platform under the Cedar Island codename. The BIOS region is placed at the top of the SPI flash as shown here:
FLASH@0xfc000000 64M {
 SI_ALL@0x0 0x2fe8000 {
     SI_DESC@0x0 0x1000
     SI_GBE@0x1000 0x2000
     SI_ME@0x3000 0x2fd5000
     SI_PT@0x2fd8000 0x10000
 }
    FMAP@0x03000000 0x800
    RW_MRC_CACHE@0x3000800  0x10000
    COREBOOT(CBFS)@0x3010800
}

Pre-Reset Flow

When the user presses the power button, the expectation is that the CPU will immediately come out from reset and start fetching code from the SPI flash. But in reality, there are more underlying steps involved prior to the CPU being ready to execute the first instruction. The duration between the user pressing the power button to the CPU fetching the first BIOS code is called the pre-reset phase. The boot flow in this phase typically involves hardware sequencing, so the hardware waits for the power supply to get settled to its nominal state and the microcontrollers that are part of SoC boot architecture come out from reset and start running from their ROM. This pre-reset flow is complex and highly dependent on the SoC architecture, so the flow is not generic enough to apply on all possible x86 platforms. The pre-reset flow that we will discuss here is based on the IA-architecture.

Prior to that, let’s understand the associated controllers and microcontrollers that are part of this reset flow:

CSE (Converged Security Engine):
  • Intel Quark x86-based 32-bit CPU, 384KB SRAM, ROM present.

  • First microcontroller active after power-on reset

  • Responsible for retrieving all firmware from the SPI flash and verifying it prior to allowing other microcontrollers to load their firmware.

  • CSE firmware has four different firmwares in it.
    • ROM: First piece of firmware code that runs after reset, allows fetching and validating other CSE bring up firmware from SPI flash.

    • RBE (ROM boot extension)

    • BUP (bring-up process): Responsible for configuring the shared SRAM and loading the other boot firmware into SRAM

    • Runtime: Micro kernel and runtime applications

PMC (Power Management Controller):
  • 32-bit ARC controller, 64KB local instruction memory, 16KB local data memory, ROM present.

  • Responsible for platform-wide power management during active and idle device states

  • PMC is also responsible for enhanced low power states like S0ix

PUNIT:
  • 8051 based microcontroller

  • Performs similar functionality as the PMC for the North Complex IP block.

  • Determines the package C-states based on the system idleness and latency tolerance reporting by the various IP blocks on the North Complex

  • Communicates periodically with PMC to apply comprehensive platform power management policies.

SPI Flash (BIOS region)
  • The BIOS on IA-based platforms is divided into three blocks:
    • IBBL (Initial Boot Block Loader): This stage runs from the shared static RAM (SRAM), which is shared by the host CPU and the security controller (i.e., CSE) and is mapped to the top of the 4GB memory region. For example, in coreboot, the bootblock acts as IBBL. For UEFI, it’s the SEC phase.

    • IBB (Initial Boot Block): This stage is executed from temporary memory known as cache as RAM (CAR). For example, in coreboot, the romstage acts as IBB. For UEFI, it’s the PEI phase.

    • OBB (OEM Boot Block): This stage is executed from system DRAM. For example, in coreboot, the ramstage and rest phases act as OBB. For UEFI, it’s the DXE and rest phases.

Here is the pre-reset boot flow that involves these controllers, the microcontrollers described above, and of course the host CPU:

  1. 1.

    Upon pressing the power button, the CSE comes out from reset and the ROM starts its execution.

     
  2. 2.

    The CSE ROM sets up the CSE SRAM.

     
  3. 3.

    The CSE ROM authenticates and loads the RBE to the SRAM.

     
  4. 4.

    The CSE RBE loads the PMC firmware to the SRAM and the PMC consumes it.

     
  5. 5.

    The CSE RBE authenticates and loads the BUP to the SRAM.

     
  6. 6.

    The CSE BUP configures the shared SRAM and maps portions of the SRAM into the host CPU address space.

     
  7. 7.

    The CSE BUP loads the uCode (microcode) patch to shared SRAM.

     
[If Boot Guard is enabled, then the CSE can perform additional steps to load the Key Manifest (KM) and Boot Policy Manifest (BPM) into the SRAM as well. In this flow, we shall ignore this step.]
  1. 8.
    The CSE BUP creates the FIT (firmware interface table) with
    1. a.

      A pointer to uCode

       
    2. b.

      A pointer to IBBL hash

       
     
  2. 9.

    The CSE BUP verifies and loads the IBBL.

     
  3. 10.

    It notifies the PMC and PUNIT to power up the North Complex and CPU.

     
  4. 11.

    The CPU/PUNIT applies the uCode patch from the shared SRAM according to the FIT, where the uCode is loaded into the CPU and the pCode is loaded by the PUNIT.

     
  5. 12.

    The CPU starts executing the IBBL from the reset vector.

     
Figure 3-7 provides the pictorial representation of the entire pre-reset boot flow where the CPU is out from reset and ready to run the BIOS region.
Figure 3-7

IA-based platform pre-reset boot flow

Minimal Bootloader Flow (Post Reset)

The prior sections provided details of the prerequisites that allow the CPU to start running the boot program upon coming out from the reset. This section provides the minimum bootloader design to allow an IA-based platform to boot an OS. A few steps that are part of this minimalistic bootloader design are subject to change (addition, deletion, or refactoring) based on the target hardware and/or specific OS requirements.

Host CPU at Reset Vector
After the bootstrap processor (BSP) comes out from reset, the location where the processor will go and find the first instruction to execute is known as the reset vector. The reset vector typically contains branch instruction that point to the start of the boot firmware. Hence a reset vector can also be defined as a pointer where the processor should always start its execution. The reset vector address is specific to the CPU architecture. For example,
  • On the x86-based platform, the reset vector is patched at address 0xFFFF_FFF0.

  • For the ARM family of processors, the reset vector is at the 0x0000_0000 address. This is the case for Motorola 68000 and PIC18 processors.

As we are discussing the x86-based bootloader design, let’s understand how this reset vector is mapped. The whole chip is mapped to the system memory (as mentioned above using the BIOS decode range) but due to protection of the flash device, not all ranges are readable. The shared SRAM is mapped into the top of the CPU address space (Step 6 in the pre-reset flow). This access is located at the top of the SPI flash - 16 bytes (i.e., the top of 4G (0xFFFF_FFFF) - 0x10 (16 bytes) = 0xFFFF_FFF0). The IBBL is part of a shared SRAM executed from the reset vector. The code is written in assembly as at this point no stack or cache as RAM is being set. Figure 3-8 shows the typical IA system memory map at power-on.
Figure 3-8

Typical IA-based platform power-on memory map

Processor Operational Modes and Mode Switching
On the x86 processor, from the boot firmware and general purpose operations standpoint, let’s discuss two widely used operating modes:
  • Real mode: Also known as real address mode. Upon the CPU coming out from reset or with any hardware reset, the processor starts operating in real mode. This mode provides a 16-bit code execution mode and a 20-bit segmented memory address space that can help to access a total of 1MB of addressable memory.

In real mode, interrupts are handled using the interrupt vector table (IVT).
  • Protected mode: Since the i386 processor, x86 architecture has added 32-bit native operating mode support for processors to overcome the limitation of maximum accessible memory up to 1MB in real mode. This mode provides flexibility for system firmware to access higher address memory and still maintain backward compatibility based on the operations needed.

In protected mode, the interrupt descriptor table (IDT) is used to handle the interrupt and the global descriptor table (GDT) is used to determine the segment-based addressing.

After power-on, the BSP always starts in real mode. All processor resources are reset. The memory management unit (MMU) and cache are not yet available. The top 12-bit address line being asserted as high allows access to the bootloader code directly from the SPI flash mapped into below 1MB of memory (20-bit address as 0xFFFxx or physical address 0xFFFx_xxxx) in real mode. The BIOS will continue in this mode unless it executes the first long jump; at that time, the processor will enter into protected mode by following the recommended programming.

Switching to protected mode

    1. Disable the maskable hardware interrupt using the cli instruction.

    2. [Optional] Load an IDT with a NULL limit to prevent the 16-bit IDT being used in the protected mode before a 32-bit IDT is set.

    3. Construct two temporary GDTs.

    • GDT[0]: NULL

    • GDT[1]: Typically, one data and one code GDT entry with a base = 0 and size = 4GB. (Refer to Figure 3-9 for the GDT entry.)

    4. Execute the LGDT instruction to load the GDTR to point to the temporary GDT.

    5. Load a MOV CR0 instruction that clears the PG, AM, WP, NE, TS, EM, and MP flags and sets the CD, NW, and PE flags.

    6. Immediately after that, perform a far JMP or far CALL that clear the real mode instruction queue and also reset the code segment (CS) register. Reload segment registers DS and ES with a GDT[1] descriptor, so both point to the entire physical memory space.

    7. After entering into protected mode, the original CS base address of 0xFFFF_0000 is retained (used as a macro to restore the CS register) and execution continues from the current offset in the EIP register.

    8. Execute the STI to enable the maskable hardware interrupt and continue the necessary hardware operation.

Figure 3-9 shows an example of GDT in gcc syntax that contains the NULL descriptor, a 32-bit code selector (0x8), and a 32-bit data selector (0x10). This is required because in protected mode all memory mappings are 1:1, meaning all logical addresses are equivalent to a physical address.
Figure 3-9

GDT for protected mode in coreboot (source: src/arch/x86/gdt_init.S)

The bootloader may need to switch to real mode from protected mode based on certain use cases like executing legacy Option ROM, using a legacy OS loader, resuming from S3, and so on.

Pre-Memory Initialization
The IBBL runs from the shared SRAM in the absence of physical memory, so the following code is responsible for setting up the cache as RAM (CAR). Figure 3-10 describes the cache hierarchy on x86 platforms, where L3 cache is shared between all the cores. It is called before accessing memory and is usually referred to as the last level cache (LLC).
Figure 3-10

Cache hierarchy on the x86 platform

NEM (Non-Evict Mode)

The boot code requires temporary memory in the absence of main memory for setting up the stack that is required for a C program execution. The BIOS implements a temporary memory by configuring the BSP cache in non-evict mode. For many generations, NEM mode has run from the LLC. Memory doesn’t exist, so all read/writes must be confined to the cache; otherwise they’re lost. NEM has a strict requirement of maintaining a 0.5MB buffer between the code and data sections. This makes the bootloader’s job difficult where IBB doesn’t fit in the available LLC size. To overcome this limitation, many bootloader designs use Cache Allocation Technology (CAT), which provides a mechanism that ensures data is always in the cache (minimum one way locked for data) while code lines are replaced. This mechanism is also known as enhanced NEM (eNEM), which allows bigger code blocks to run from the LLC.

Early Chipset Initialization

A few controllers are required to program during the early boot phases like UART for enabling the serial console. This process requires programming the PCI configuration space’s base address registers (BARs), enabling the I/O and MMIO space. Depending on the chipset, there are prefetchers that can be enabled at this point to speed up the data transfer from the flashed mapped device. There may be other controllers that can be accessed either over the PCI bus or memory mapped devices prior to memory initialization.

Memory Initialization

Initialization of the memory controller is the key responsibility of the boot firmware. This initialization process depends on the DRAM technology and the capabilities of the memory controller. The memory controller integrated inside the SoC provides an opportunity for the SoC vendors to supply the memory reference code (MRC) and required documentation to know the configurations allowed on a platform. At a high level, MRC is developed based on the DRAM technology by following the JEDEC initialization sequence. This involves running several training algorithms in 32-bit protected mode by considering a few board configuration parameters. PC-based memory configurations are based on dual inline memory modules (DIMM). There is a very wide range of DIMM configurations on embedded systems based on the module configuration as soldered down/memory-down solutions or socketed memory solutions. These configurations vary between the number of ranks, memory controllers, runtime control of resistive compression (RCOMP), and delay locked loop (DLL) capabilities. (Refer to Chapter 7’s “Adopting Hybrid Firmware Development Model” section for details) These capabilities allow the memory controller to change components such as the drive strength to ensure flawless operation with temperature variations and with aged hardware.

The majority of embedded systems are populated with soldered down DIMMs on the motherboard so the BIOS doesn’t need to perform the dynamic configuration; rather, the BIOS is specifically built for the target configuration using the vendor-supplied serial presence detect (SPD) data hard-coded as part of the BIOS. If the platform supports a socketed memory module, then the DIMM configurations are read through a special, tiny serial EEPROM chip. These chips contain specification-defined information about the capabilities of the DRAM configuration such as SPD data, which is readable through a SMBUS or an I2C interface. It is also possible to provide hard-coded SPD data for EPROM-less devices in case of socketed DIMM.

All this configuration data needs to be gathered as part of the memory initialization process to feed into the MRC code in order to make main memory available.

Post Memory Initialization

Today the firmware boundary is prolonged due to the fact that lots of IP initializations and application processors initializations are dependent on the main memory being initialized. For example, the basic foundation block for security enforcement has to wait until the DRAM is available. Once the main memory has been initialized, here is the minimum list of operations being performed.

Memory Test

The idea here is to ensure the integrity of the main memory by writing and reading back the test patterns (the most common ones are Zero-One and Walking 1/0). This operation is done as part of MRC prior to transferring the call into DRAM-based resources. For systems with fast boot requirements, it doesn’t make sense to run this operation at all boot paths (cold or warm boot).

Shadowing

In the absence of physical memory, the BIOS modules (IBBL and IBB) are expected to run as part of XIP. This makes the boot process slower. Once main memory is available, the BIOS performs a shadowing operation of further modules from the SPI flash into DRAM-based memory. This process requires the bootloader stages being built with relocation enabled so that the program starts to execute from RAM.

Tear Down the CAR
While the BIOS is operating using the CAR as temporary memory, the data and code stacks are part of the BSP cache. Now with main memory being initialized, the need for the CAR is over, hence the need to tear down the CAR. The stack must be set up before jumping into the main memory. The stack top needs to get identified based on the main memory availability after reserving the required memory range (refer to Figure 3-11). The stack counts down, so the top of the stack must be placed with enough memory to be allocated for the maximum stack.
Figure 3-11

System memory map on an IA-based platform

Since the processor cache using temporary memory is already being torn down, now the need is to enable caching ranges based on DRAM resources. The bootloader makes use of the variable memory MTRRs to specify the different caching ranges. The first entry in each pair is MTRR_PHYSBASEn and it defines the base address and memory type for the ranges. The second entry, MTRR_PHYSMASKn, contains a mask to specify the range limit. The n indicates variable MTTR pairs between 0 to 7.

MP (Multi-Processor) Initialization

In a typical CPU architecture, there is more than one logical processor in it. As a result, the CPU initialization process is not only limited to the BSP. The other logical processors available in a CPU architecture in case of a multi-CPU core environment are known as application processors (APs). It’s the responsibility of the BSP to bring the APs from the dormant state and perform the initialization with identical features enabled for the BSP. Before going to the AP initialization process, let’s understand the underlying command used in this initialization process.

Startup Inter-Processor Interrupt (SIPI)

To wake up the other logical processors apart from the BSP, the BSP sends a SIPI to each application processor, indicating the physical address from which the APs should start executing. All APs start in real mode so this address where APs will jump after reset must be below 1MB of memory and be aligned on a 4-KB boundary.

The MP initialization can be divided into two major phases:
  • BSP initialization sequence, to let the APs out from the reset

  • AP initialization sequence, to let the APs perform the feature programming

BSP Initialization Sequence

The boot-strap code typically performs the following operations:

  1. 1.

    Initializes memory

     
  2. 2.

    Loads the microcode update into the processor

     
  3. 3.

    Initializes the MTRRs

     
  4. 4.

    Enables the caches

     
  5. 5.

    Loads start-up code for the AP to execute into a 4-KByte page in the lower 1MB of memory

     
  6. 6.

    Switches to protected mode and ensures that the APIC address space is mapped to the strong uncacheable (UC) memory type. Determines the BSP’s APIC ID from the local APIC ID register (default is 0).

     
  7. 7.

    Save the BSP used MSRs and MTRRs.

     
  8. 8.
    Performs the following operation to set up the BSP to detect the presence of APs in the system and the number of processors (within a finite duration, minimally 10 milliseconds):
    • Sets the value of the COUNT variable to 1

    • In the AP BIOS initialization code, the AP increments the COUNT variable to indicate its presence. The finite duration while waiting for the COUNT to be updated can be accomplished with a timer. When the timer expires, the BSP checks the value of the COUNT variable. If the timer expires and the COUNT variable has not been incremented, no APs are present or some error has occurred.

     
  9. 9.

    Broadcasts an INIT-SIPI-SIPI sequence to the APs to wake them up (newer AMD and Intel CPUs don’t even need second SIPI and a delay between INIT and the first SIPI) and initialize them. If the software knows how many logical processors it expects to wake up, it may choose to poll the COUNT variable. If the expected processors show up before the 10 millisecond timer expires, the timer can be canceled and skip to step 10.

     
  10. 10.

    Reads and evaluates the COUNT variable and establishes a processor counts

     
  11. 11.

    If necessary, reconfigures the APIC and continues with the remaining system diagnostics as appropriate

     
AP Initialization Sequence

When an AP receives the INIT SIPI, it begins executing the BIOS AP initialization code at the vector encoded in the SIPI.

The AP initialization code typically performs the following operations:
  1. 1.

    Waits on the BIOS initialization lock semaphore. When control of the semaphore is attained, initialization continues.

     
  2. 2.

    Loads the microcode update into the processor

     
  3. 3.

    Syncs the BSP MSRs and MTRRs

     
  4. 4.

    Enables the cache

     
  5. 5.

    Determines the AP’s APIC ID from the local APIC ID register and adds it to the MP and ACPI tables and optionally to the system configuration space in RAM

     
  6. 6.

    Initializes and configures the local APIC. Configures the AP’s SMI execution environment. (Each AP and the BSP must have a different SMBASE address.)

     
  7. 7.

    Increments the COUNT variable by 1

     
  8. 8.

    Releases the semaphore

     
  9. 9.
    Executes one of the following:
    • The CLI and HLT instructions (if MONITOR/MWAIT is not supported), or

    • The CLI, MONITOR and MWAIT sequence to enter a deep C-state

     
  10. 10.

    Waits for an INIT SIPI

     
Figure 3-12 provides a snapshot of the MP CPU feature programming as part of IA-based platform using coreboot bootloader.
Figure 3-12

MP feature-enabling sequence (using coreboot on an IA-based platform)

Late Chipset Initialization

The early chipset initialization is already being done in the pre-memory phase. This stage performs the initialization of the remaining chipset devices that take part in the payload or early kernel initialization process. This list may widely vary between the underlying hardware and target OS features. At a high level, the minimal operations in this phase are

  • General purpose I/O (GPIO) programming

  • Interrupt configuration

  • PCI enumeration

  • Graphics initialization

  • Boot media initialization (USB, SATA)

GPIO Programming

The GPIO controller is an elemental part of the SoC PCH. The GPIO controller part of SoC may have one or more GPIO communities. Each community consists of one or more GPIO groups, which consist of a number of GPIO PINs. Several functionalities of each IP/device inside SoC can be multiplexed to a particular I/O PIN. The configuration of the PINs must be set prior to the use of the IP/device. This PIN can either be configured to be a special function (typically known as a native function) or a general purpose I/O PIN. If a PIN is configured as GPIO, then it’s direction can be configured as input or output with respect to the SoC IP/device. For an example, UART Rx and Tx PINs are programmed as native functions. The GPIO state may be configured for PINs that are configured as outputs. It’s recommended that developers refer to the board schematics to know the exact GPIO configuration based on the target hardware.

Interrupt Configuration

Interrupts are like events and are used by endpoint devices to communicate something to the CPU. For example, for user input on the HID, in absence of an interrupt, the CPU would have had to poll all the platform devices, resulting in wasting CPU time.

There are three types of interrupts:

  • Hardware interrupts: Interrupts coming from hardware devices like a keyboard or timer

  • Software interrupts: Generated by the software int instruction

  • Exceptions: Triggered by the CPU itself in response to some erroneous conditions like “divide by zero” and such

On the x86 platform, a combination of the following is used to handle interrupts:
  • PIC (programmable interrupt controller): The simplest way to handle interrupts on x86 platforms where PIC receives interrupt requests from the endpoint device and sends them to the CPU. The PIC contains two cascaded 8259s with 15 IRQs (IRQ2 is not available since it is used to connect the 8259s). The BIOS programs the IRQs as per the board configuration. Figure 3-13 shows a reference block diagram of the BIOS configuring the IRQ.

Figure 3-13

BIOS configuring the interrupt in PIC mode

  • LAPIC (local advanced programmable interrupt controller): APIC is a more advanced interrupt controller than PIC. The APIC design is split into two components: local components (LAPIC) integrated into the processor and I/O APIC on a system bus. There is one LAPIC in each processor in the system. LAPICs may support up to 224 usable interrupt vectors. Vector numbers 0 to 31, out of 0 to 255, are reserved for exception handling by x86 processors.

  • IOxAPIC (input/output advanced programmable interrupt controller): The IOxAPIC is present inside the Integrated Controller HUB (ICH) or Platform Controller Hub (PCH). The IOxAPIC has support for 24 interrupt lines. Each IRQ has an associated redirection table entry that can be enabled/disabled and selects the vector for the associated IRQ. Typically, the BIOS is expected to program interrupts in IOxAPIC mode as it provides the improved latency.

Refer to Figure 3-14 for the interrupt handling mechanism on x86-based platforms. There are eight PIRQ pins named PIRQ[A#: H#] that routed individual IRQs to the PIC and the PIC forwards the single interrupt to the CPU based on the BIOS programming (GPIO IRQ Select; the valid value is 14 or 15). Alternatively, ICH/PCH also connects those PIRQs to eight individual IOxAPIC input pins.
Figure 3-14

BIOS programming PIRQx# to IRQx in PIC or IOAPIC

PCI Enumeration

The Peripheral Connect Interface is a standard bus used in an x86-based SoC design to connect various controllers to it. The bootloader is responsible for enumerating the PCI buses to detect the PCI devices present and create the complete PCI device list. Later, it allocates the DRAM-based resources to those PCI devices. The PCI enumeration is a generic process that might take several milliseconds based on the number of devices attached to the PCI bus and the speed at which the CPU is operating. Most embedded systems have SoC internal devices attached to Bus 0, hence a static PCI device list (similar to src/mainboard/google/zork/variants/baseboard/devicetree_dalboz.cb) where the device location (i.e., the bus) and function are known. Of course, there are external devices sitting behind PCIe root ports which need an additional child device enumeration. After discovering the entire PCI device list, the BIOS needs to allocate and enable the resources, which includes

  • MMIO space

  • IRQ assignments as described in an earlier section

  • Locating the Option ROM (also known as expansion ROM) and executing if available based on platform need

A few of the PCI devices that are typically used in embedded systems are the on-board graphics controller, USB controller, Serial ATA (SATA), and SPI. The BIOS must ensure that these devices have resource allocations for future use.

Graphics Initialization

Graphics initialization is considered as the sign of life for embedded devices with displays attached. The BIOS detects the on-board graphics controller or the off-board discrete graphics card on the PCIe root port. Let’s limit this discussion up to the internal graphics controller. On legacy platforms, the BIOS locates the expansion ROM inside the graphics controller, also known as video BIOS (vBIOS) and executes it for display initialization. Modern systems with UEFI rely on Graphics Output Protocol (GOP), and open source firmware uses libgfxinit for graphics initialization on embedded systems.

Boot Media Initialization

Typically boot device initialization is the responsibility of a payload program. Boot firmware is responsible for detecting the boot devices over the standard PCI bus and performing the resource allocation. The payload has several device drivers for the required boot device: USB or SATA. With USB, the payload needs to have the standard XHCI controller drivers and USB bus drivers, followed by the USB device drivers. As USB is a generic specification, the payload needs to have a mass storage driver to support booting from the USB media.

Similarly, the boot firmware is expected to perform the SATA controller mode setup prior to payload. Also, it implements the Port Mapping register and Port X Enable (PxE) bit(s) of the Port Control and Status register to enable the unused SATA ports based on runtime device detection. Typically, on client segments, SATA controllers are expected to program in AHCI (Advanced Host Controller Interface) mode while server platforms configure the SATA controller in RAID mode.

Booting to the OS

The OS loader is responsible for managing the required communication while booting to an OS. The boot firmware might need to provide different sets of information based on the target OS (between a UEFI-aware OS and a legacy OS). This section will limit the discussion to booting to a legacy OS. While booting to a legacy OS, the OS bootloader is loaded into a memory location below 1MB , so it switches into processor real mode and jumps to the location. The following section specifies the expectation from the boot firmware while booting to a legacy OS.

OS Handoff Lists

Here are the minimum requirements from a boot firmware while booting to the OS and boot firmware to create several tables that the OS needs as part of the post memory initialization process:

  • E820 table

  • Programmable interrupt routing (PIRQ) table

  • Multi-processor specification (MP) table

  • System management BIOS (SMBIOS) tables

  • Advanced configuration and power interface (ACPI) tables

e820 Table

As discussed (Figure 3-11 as part of the post memory initialization), the BIOS is responsible for defining the cacheable range and creating system memory maps. It is also responsible for providing that system memory map to the OS so that the OS is aware of what regions are actually available to use and what all ranges are reserved. The ACPI specification describes the entries in this table.

There are different ways to provide this information to the OS. For example, an UEFI-aware OS relies on the EfiMemoryMap call from the kernel to a real UEFI memory map. In a legacy OS, the most widely used mechanism is to use real mode interrupt service 0x15, function 0xe8, sub-function 0x20 (hence its INT15, 0xe820), which the BIOS must implement. The e820 table defines the following address range types:

e820 Type

Name

Description

1

RAM

This range is the available RAM usable by the operating system.

2

Reserved

This range of addresses is in use or reserved by the system and is not to be included in the allocatable memory pool of the operating system’s memory manager.

3

ACPI

ACPI reclaim memory. This range is available RAM usable by the OS after it reads the ACPI tables.

4

NVS

ACPI NVS memory. This range of addresses is in use or reserved by the system and must not be used by the operating system. This range is required to be saved and restored across an NVS sleep.

5

Unusable

This range of addresses contains memory in which errors have been detected. This range must not be used by OSPM.

7

Persistent

memory

OSPM must comprehend this memory as having non-volatile attributes and handling distinct from conventional volatile memory.

With some bootloaders like coreboot, the e820 table has an entry of type 16, which is unknown to the Linux kernel. coreboot marks the entire cbmem as type 16 rather than attempting to mark the RAM as reserved. Here is sample example of the e820 table from an x86-based platform:
BIOS-e820: [mem 0x0000000000000000-0x000000000009ffff] usable
BIOS-e820: [mem 0x00000000000a0000-0x00000000000fffff] reserved
BIOS-e820: [mem 0x0000000000100000-0x000000005bffffff] usable
BIOS-e820: [mem 0x000000005c000000-0x00000000707fffff] reserved
BIOS-e820: [mem 0x00000000c0000000-0x00000000cfffffff] reserved
BIOS-e820: [mem 0x00000000fc000000-0x00000000fc000fff] reserved
BIOS-e820: [mem 0x00000000fd000000-0x00000000fe00ffff] reserved
BIOS-e820: [mem 0x00000000fed10000-0x00000000fed17fff] reserved
BIOS-e820: [mem 0x00000000fed80000-0x00000000fed83fff] reserved
BIOS-e820: [mem 0x00000000feda0000-0x00000000feda1fff] reserved
BIOS-e820: [mem 0x0000000100000000-0x000000048f7fffff] usable
Programmable Interrupt Routing Table

The first PCI interrupt routing table provided by the x86 BIOS is the $PIR table. This table describes how the PCI interrupt signals are connected to input pins on a PIC. In addition, these details can be used by the operating system to program the interrupt router directly.

Multiprocessor Specification Table

The multiprocessor specification table is required if a system has more than one processor present. The signature of this table is _MP_. More details can be found in the multiprocessor specification (MP Spec).

System Management BIOS Table

On booting the system, the BIOS will create the SMBIOS table with the signature _SM_ and put it into the system memory. This table provides information about the underlying system hardware and firmware and is used by some OS-based applications. A popular consumer of SMBIOS tables is the dmidecode command, which provides hardware-related information such as processors, DIMM, BIOS, memory, and serial number.

Creation of ACPI Tables

ACPI defines an interface between the ACPI-compliant OS and platform HW via the system BIOS to control core power and system management.

To provide HW vendor flexibility in choosing the implementation, ACPI uses the concept of tables like system info, features, and other control methods.
  • Describe the interfaces to the hardware

  • The ACPI table is initiated with the root system description pointer (RSDP). During the OS initialization, the OSPM must have an RSDP pointer structure from the platform.

  • The platform BIOS should look for the RSDP pointer entry in the system memory. This approach differs between a legacy BIOS and a UEFI implementation.

  • The platform design should specify what ACPI table system the BIOS should populate.

  • Figure 3-15 shows the sample ACPI table structure, where all ACPI tables have standard header format (signature/length) apart from table-specific information or a pointer to another table structure.

Figure 3-15

Sample ACPI table structure

BIOS Runtime Services

Apart from the boot services, many bootloaders also provide runtime capabilities that coexist with the main operating system, also known as runtime (RT) services. Typically, it’s expected that a BIOS job is done once the system is able to find the boot media and load the kernel but there is certain access where the OS relies on the system BIOS. A widely used application of a RT service is BIOS ASL (ACPI Source Language) code, which is used by kernel devices and ACPI drivers. Apart from that, there may be other services as part of the bootloader runtime service table, such as get/set time, get/set wakeup time, access to the non-volatile memory like SPI flash using a get/set variable, resetting the system, and more.

Another hidden but available AL service is usage of SMM triggered by the System Management Interrupt (SMI). This allows accessing the SMI handlers part of SMM memory written by the bootloader from the OS layer with the highest privilege level.

BIOS AL Services
In the event that the main operating system crashes or issues a shut-down, there is a final set of capabilities known as After Life (AL) services. Typically, it’s expected that the OS will shut down gracefully. In the event of an OS panic or failure, a hardware watchdog timer (WDT) may reinvoke the firmware, say the TCO SMI source. In this case, the firmware can quiesce the hardware and create a firmware error log prior to issuing a hard reset, such as an 0xCF9 write. Figure 3-16 shows a x86-based embedded system boot flow with a minimalistic bootloader design approach.
Figure 3-16

System boot flow with a minimalistic BIOS design

Minimalistic Bootloader Design on the ARM Platform

A bootloader design on the ARM platform is way different than what we have discussed so far on the x86 platform. On the ARM platform, the minimalist bootloader design needs to implement the Trusted Board Boot (TBB) feature. The TBB feature allows the platform to be protected from malicious firmware attack by implementing a chain of trust (CoT) at each firmware level up to the normal world bootloader. Trusted Firmware (TF) implements a subset of the TBB requirements for ARM reference platforms. This section describes the minimalistic design of TF on the ARM platform.

Trusted Firmware

The TBB sequence starts when the user presses the power button until it transfers the control to firmware running as part of a normal world bootloader in DRAM. The TF is a reference implementation of secure world software for processors implementing both the A-Profile and M-Profile of the ARM architecture. The TF for A-Profile ARM processors is known as TF-A, and Trusted Firmware-M (TF-M) provides a reference implementation of the platform requirements for ARM Cortex-M processors. The scope of this section is limited to TF-A, an open source implementation focusing on trusted boot including Exception Level 3 (EL3) code.

The ARM architecture defines four exception levels (EL0, EL1, EL2, and EL3). These exception levels are associated with software execution privileges.
  • EL0: The lowest privileged execution level, also referred to as the unprivileged level of execution

  • EL3: The highest privileged execution level, which is used to control access to the secured world. Switching between a non-secure state to a secure state or vice-versa can be done only when the software executes at the EL3 privilege level.

This table shows the partitioning of software based on the processor exception level:

Exception level

Description and usage

Non-secure world EL0

Unprivileged applications downloaded and running from the app stores

Non-secure world EL1

Operating system kernels run at EL1 exception level

Non-secure world EL2

Bootloader running from DRAM. Virtualization applications from vendors run at this exception level.

Secure EL0

Trusted OS applications

Secure EL1

Trusted OS kernels from trusted OS vendors. Starting with AArch64 architecture, it allows firmware to run at Secure EL1.

Secure EL3

Secure firmware from SoC vendors, secure monitor, trusted ROM firmware runs at this exception level.

Trusted Firmware utilizes the processor exception levels and associate this with Trusted Execution Environment (TEE), so that the trusted firmware running part of Secure World and other firmware running in DRAM are from Non-Secure World. Figure 3-17 shows the software layers of TF on an ARM system.
Figure 3-17

Software/firmware layers of TF on an ARM system

In order to create a more scalable kernel image that can work on all platforms, the ARM platform firmware is looking for a standard that allows interaction with the hardware platform based on certain firmware interfaces. Trusted Firmware has created some of these standards to make this possible:
  • PSCI (Power State Coordination Interface)

  • SMC (Secure Monitor Call) Calling Convention

Power State Coordination Interface
This creates an API that can be used to perform device power management by OS vendors for supervisory software working at different privilege levels. The idea here is that when supervisory software running as part of the non-secure world requests to manage the device power (i.e., power off the system), the request context is transferred to the secure world platform firmware, which might need to communicate with the trusted OS. This interface is designed to provide a generic implementation that allows rich OS, non-trusted firmware, trusted OSes, and secure firmware to interoperate when power is being managed. This interface also allows it to work along with other firmware tables (examples: ACPI and FDT (Flattened Device Tree)). Figure 3-18 provides an example of a platform shutdown request being managed with PSCI.
Figure 3-18

SYSTEM_OFF PSCI command from non-secure world

SMC Calling Convention

This defines a standard calling mechanism for the Secure Monitor Call (SMC) instruction in the ARM architecture. In the ARM architecture, synchronous control is transferred between the non-secure world to the secure world through SMS exceptions. These calls may then be passed on to a trusted OS in Secure EL1. It eases out the integration of several software layers, such as a rich OS, virtualization application, trusted OS, secure monitor, and system firmware.

TF Architecture
Based on the underlying ARM architecture (between 32-bit ARM processor AArch32 and 64-bit ARM processor AArch64), the Trusted Firmware is divided into five stages (in the order of execution):

Stage name

Firmware name

Applicable for AArch32

Applicable for AArch64

Boot Loader Stage 1 (BL1)

Processor Trusted ROM

Yes

Yes

Boot Loader Stage 2 (BL2)

Trusted Boot Firmware

Yes

Yes

Boot Loader Stage 3-1 (BL31)

EL3 Runtime Firmware

No

Yes

Boot Loader Stage 3-2 (BL32)

EL3 Runtime Firmware (for AArch32)

Secure EL1 Payload (for AArch64)

Yes

Optional

Boot Loader Stage 3-3 (BL33)

Non-Trusted Firmware

Yes

Yes

As part of the initialization process, these boot firmwares use different memory regions for loading, verifying, and execution:
  • Regions are only accessible by the secure world elements like ROM and trusted SRAM. There may be some implementations that also use a region in the DRAM as trusted DRAM only for secure operations.

  • Regions are accessible by both the non-secure and secure world, such as non-trusted SRAM and DRAM.

Firmware Configuration
The Firmware Configuration Framework (FCONF) provides the flexibility at each bootloader stage to allow for configuring the platform dynamically. The bootloader stage can specify a firmware configuration file and/or hardware configuration file that was previously hardcoded into the firmware code. This framework uses the Flattened Device Tree (FDT) format that is passed to the firmware during the bootloader load process. These configuration files are
  • FW_CONFIG: The firmware configuration file that holds platform configuration data shared across all boot loader (BLx) images. The FW_CONFIG expects a dtb_registry node with the information field like the physical loading address of the configuration, maximum size, and image id.

  • HW_CONFIG: The hardware configuration file that can be shared by all bootloader stages and also by the unsecured world rich OS

  • TB_FW_CONFIG: The Trusted Boot firmware configuration file shared between BL1 and BL2 stages

  • SOC_FW_CONFIG: The SoC firmware configuration file used by BL31

  • TOS_FW_CONFIG: The trusted OS firmware configuration file used by the trusted OS (BL32)

  • NT_FW_CONFIG: The non-trusted firmware configuration file used by the non-trusted firmware (BL33)

Each bootloader stage can pass up to four arguments via registers to the next stage. When dynamic configuration files are available, the firmware configuration file is passed as the first argument and the generic hardware configuration is passed as the next available argument for the next bootloader stage. For example, FW_CONFIG is loaded by the BL1 and then its address is passed in arg0 to BL2.

Firmware Image Package (FIP)

The FIP allows packing all possible bootloader images along with configuration files and certificates for boot stage authentication into a single archive that can be loaded by the TF from the non-volatile platform storage (for FVP, it’s the SPI flash). This package also consists of a dedicated driver (drivers/io/io_fip.c) to read data from a file in the package. The FIP creation tool (tools/fiptool) can be used to pack specified images into a binary package that can be loaded by the TF from platform storage.

Firmware Authentication

Trusted Firmware has an authentication framework as part of the TCB requirement that allows verification of all bootloader images. TF does this by establishing a CoT using public-key cryptography standards (PKCS).

A CoT on the ARM platform relies on trusted components such as
  • A SHA-256 hash of the root of trusted public key (ROTPK), stored inside the trusted root-key storage register

  • The BL1 image running as part of the trusted ROM

The CoT certificates are categories as Key and Content. Key certificates are used to verify public keys that have been used to sign content certificates. Content certificates are used to store the hash of a bootloader image. An image can be authenticated by calculating its hash (using SHA-256 function) and matching it with the hash extracted from the content certificate.

Boot Loader Stages

Figure 3-19 shows a simple TF boot flow on the ARM FVP platform. Trusted ROM and trusted SRAM are used for trusted firmware binaries and non-secured DRAM is used for non-trusted bootloaders. BL1 originally sits in the trusted ROM at address 0x0 (RO block). Its RW data is relocated at the base of the trusted SRAM at runtime. BL1 loads the BL2 image into the top of trusted SRAM to run at the EL3 exception level.

BL2 loads BL31 images between BL1 and B2 in the trusted SRAM. BL31 then loads the first non-secure bootloader BL33 into the non-secure memory running as an EL2 exception level. BL33 then performs the remaining platform initialization and boots to the rich OS.
Figure 3-19

Minimalistic bootloader operations in an ARM TF

Here is the detailed description of each bootloader stage and its minimal architecture:

Bootloader stage 1: The first stage of TF is designed to execute upon a platform hitting the reset vector from the trusted ROM at EL3. The BL1 code starts at 0x0000_0000 in the FVP memory map. The Data section of BL1 is placed inside a trusted SRAM base at address 0x0400_0000. The operations performed by this stage are as follows:

  • Identify the boot path: Upon the CPU release from reset, BL1 needs to perform a unique identification to detect the boot path between the warm and cold boot. This is a platform-specific implementation. For example, the ARM FVP relies on a power controller to distinguish between a cold and warm boot.

  • Set up exception vectors: BL1 sets up simple exception vectors for both synchronous and asynchronous exceptions.

  • MMU setup: BL1 sets up an EL3 memory translation by creating page tables to cover the physical address space.

  • Control register setup: BL1 performs enabling of several control registers, which helps other bootloaders running at different exception levels (for example, instruction cache, intra-cluster coherency, use of the HVC instruction from EL1, FIQ exceptions are configured to be taken in EL3, etc.).

  • Platform initialization: BL1 performs the following initialization:
    • Initialize the UART controller.

    • Determine the amount of trusted SRAM required to load the next bootloader image (BL2).

    • Configure the platform storage to load the BL2 binary at a platform-specific base address.

    • Pass control to the BL2 image at Secure E1, starting from its load address.

    • If the dynamic configuration file is available (TB_FW_CONFIG), BL1 loads it to the platform-defined address and makes it available for BL2 via arg0.

Bootloader stage 2: BL2 performs the next level of Trusted Firmware initialization that loads both secure and non-secure bootloader stages. BL2 runs from the Secure EL1. The operations performed by this stage are as follows:

  • MMU setup: BL2 sets up a Secure EL1 memory transition by creating page tables to cover the physical address space in a similar way as BL1.

  • Platform initialization: BL2 performs the following initialization:
    • BL2 calculates the limits of DRAM (main memory) to determine whether there is enough space to load the BL33 image.

    • BL2 uses a platform-defined base address to load the BL31 image into trusted SRAM.

    • If the ARM architecture supports a BL32 image, then BL2 is also responsible for loading BL32 into trusted SRAM.

    • BL2 also initializes the UART controller.

    • Platform security is initialized to allow access to controlled components. BL2 loads and verifies the trusted key and other bootloader (BL3x) key certificate.

    • BL2 loads the non-secured bootloader from non-volatile storage into non-secured memory (DRAM). The entry point address of the BL33 image is passed to BL31 as BL2 relies on BL31 to pass control to BL33.

    • BL2 raises an SMC to pass control back to BL1 with a BL31 entry point. The exception is handled by the SMC exception handler as part of BL1.

Bootloader stage 3-1: This image is loaded by BL2 and BL1 passes control to BL31 at EL3. BL31 executes from trusted SRAM. The functionality perform by BL31 is as follows:

  • BL31 perform the similar architectural initialization being done by BL1 as BL1 code was running from the ROM, so BL31 allows an override of any previous initialization done by BL1.

  • BL31 creates page tables to address the physical address space and initializes the MMU.

  • BL31 overrides the exception vectors earlier populated by BL1 with its own exception vector table.

  • Platform initialization: BL31 performs the following initialization:
    • BL31 ensures that the required platform initialization is done to help the non-secure world bootloader and software run correctly.

    • BL31 is also responsible for executing the non-secure world bootloader BL33 based on the platform memory address populated by BL2.

    • BL31 also initializes the UART controller.

    • It initialize the generic interrupt controller.

    • It initialize the power controller device.

    • It initialize the runtime services of the software and firmware running in the non-secure and secure world at an exception level lower than EL3 will request runtime services using the SMC instruction. The EL3 runtime services framework enables different service providers to easily integrate their services into the final product firmware. For an example, for power management of the ARM system, PSCI is the interface used by non-secure or secure world software.

Bootloader stage 3-3: BL2 loads the BL33 image into the non-secure memory. BL33 is the first bootloader call outside the secure world, so platform owners can decide to use any BIOS solution as BL33. BL33 images can be any boot firmware like UEFI, coreboot, or U-Boot. BL31 initializes the EL2 or EL1 processor context for a non-secure world cold boot, ensuring that no secure state information finds its way into the non-secure execution state. BL31 uses the entry point information provided by BL2 to jump to the non-trusted firmware image (BL33) at the highest available exception level (mostly EL2; if not available, then EL1). The functionality perform by BL33 is as follows:

  • Platform initialization: BL33 performs the following initialization:
    • As memory is available and initialized, it performs the required set up for stack for running the C programming code.

    • Initializes the UART controller for the serial console

    • Has a non-volatile driver to access the system firmware

    • Sets up MMU for caching if BL33 is running from non-secure SRAM.

    • Performs GPIO programming and moving bootloader code from SRAM to DRAM (if not done already)

    • Creates firmware code based on standard specifications like ACPI and SMBIOS.

    • Performs boot device initialization and transfers control to the OS bootloader as part of the non-secure world

Figure 3-20 shows the Trusted Firmware boot flow which summarizes all these different bootloaders’ minimalistic operational designs to perform SoC and platform initialization in a secure manner and finally boot to an OS.
Figure 3-20

Trusted Firmware minimalistic bootloader flow

Summary

This chapter captured the roles and responsibilities that a system firmware needs to perform in order to be called a BIOS. We discussed in detail the types of BIOSes and their working principles. Additionally, this chapter revealed the minimalistic bootloader design across different CPU architectures. The chapter also resolved the mystery of the system initialization process prior to CPU reset (i.e., the pre-reset phase) and its prerequisites, and provided a detailed understanding of reset and various other boot and runtime operations made by the bootloader to ensure all required hardware and platform components are initialized as part of the bootloader execution. Having a minimal understanding of BIOS design in cross-architecture will help you create your own system firmware based on the target embedded system.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.116.62.45