ABI.md 21 KB

Writing Easyboot compatible kernels

Easyboot supports various kernels using plugins. But if no suitable plugin found, it fallbacks to ELF64 or PE32+ binaries with a simplified (no need to embed anything) variant of the Multiboot2 protocol.

This is the very same protocol that Simpleboot uses, all of the example kernels in that repo must work with Easyboot too.

You can use the original multiboot2.h header in GRUB's repo, or the easyboot.h C/C++ header file to get easier to use typedefs. The low-level binary format is the same, you can also use any existing Multiboot2 libraries, even with non-C languages, like this Rust library for example (note: I'm not affiliated with those devs in any way, I just searched for "Rust Multiboot2" and that was the first result).

[[TOC]]

Boot Sequence

Bootstrapping the loader

On BIOS machines, the very first sector of the disk is loaded to 0:0x7C00 by the firmware, and control passed to it. In this sector Easyboot has boot_x86.asm, which is smart enough to locate and load the 2nd stage loader, and also to set up long mode for it.

On UEFI machines the very same 2nd stage file, called EFI/BOOT/BOOTX64.EFI is loaded directly by the firmware. The source for this loader can be found in loader_x86.c. That's it, Easyboot isn't GRUB nor syslinux, both of which requiring dozens and dozens of system files on the disk. Here no more files needed, just this one (plugins are optional, none needed to provide Multiboot2 compatibility).

On Raspberry Pi the loader is called KERNEL8.IMG, compiled from loader_rpi.c.

The loader

This loader is very carefully written to work on multiple configurations. It loads the GUID Partitining Table from the disk, and looks for an "EFI System Partition". When found, it looks for the easyboot/menu.cfg configuration file on that boot partition. After the boot option selected and the kernel's filename is known, the loader locates and loads it.

It then autodetects the kernel's format, and it is smart enough to interpret the section and segment information about where to load what (it does on-demand memory mapping whenever necessary). Then it sets up a proper environment depending on the detected boot protocol (Multiboot2 / Linux / etc. protected or long mode, ABI arguments etc.). After the machine state is solid and well-defined, as a very last act, the loader jumps to your kernel's entry point.

Machine State

Everything what's written in the Multiboot2 specification about the machine state stands, except for the general purpose registers. Easyboot passes two arguments to your kernel's entry point according to the SysV ABI and Microsoft fastcall ABI as well. First parameter is the magic, the second one is a physical memory address, pointing to a Multiboot Information taglist (abbreviated as MBI hereafter, see below).

We also violate the Multiboot2 protocol a bit to handle higher-half kernels. Multiboot2 mandates that memory must be identity mapped. Well, under Easyboot this is just partially true: we only guarantee that all of the physical RAM is surely identity mapped as expected; however some regions above that (depending on the kernel's program headers) might be yet available. This does not break normal Multiboot2 compliant kernels, which are not supposed to access memory outside of the available physical RAM.

Your kernel is loaded exactly the same way on both BIOS and UEFI systems as well as on RPi, firmware differences are just "Somebody Else's Problem". The only thing your kernel will see is whether the MBI contains the EFI system table tag or not. To simplify your life, Easyboot does not generate EFI memory map (type 17) tag either, it provides only the Memory map (type 6) tag indiscriminatly on all platforms (on UEFI systems too, there the memory map is simply converted for you, so your kernel has to deal with only one kind of memory list tag). Old, obsolete tags are also ommited and never generated by this boot manager.

The kernel is running at supervisor level (ring 0 on x86, EL1 on ARM), possibly on all CPU cores in parallel.

GDT unspecified, but valid. Stack is set up in the first 640k, and growing downwards (but you should change this as soon as possible to whatever stack you deem worthy). When SMP is enabled, all cores have their own stacks, and the core id is on the top of the stack (but you can also get the core id the usual platform specific way, using cpuid / mpidr / etc.).

You should consider IDT as unspecified; IRQs, NMI and software interrupts disabled. Dummy exception handlers are set up to display some minimal dump and halt the machine. These should only be relied on to report if your kernel goes havoc before you were able to set up your own IDT and handlers, preferably as soon as possible. On ARM vbar_el1 is set up to call the same dummy exception handlers (although they dump different registers of course).

Framebuffer is also set by default. You can alter the resolution in config, but if not given, framebuffer is still configured.

It is important to never return from your kernel. You're free to overwrite any part of the loader in memory (as soon as you've finished with the MBI tags), so there's simply nowhere to return to. "Der Mohr hat seine Schuldigkeit getan, der Mohr kann gehen."

Boot information passed to your kernel (MBI)

It's not obvious at first, but the Multiboot2 specification actually defines two, totally independent set of tags:

  • The first set supposed to be inlined in a Multiboot2 compliant kernel, called OS image's Multiboot2 header (section 3.1.2), hence provided by the kernel. Easyboot does not care about these tags, and it does not parse your kernel for these either. You simply don't need any special magical data embedded in your kernel file with Easyboot, ELF and PE headers suffice.

  • The second set is passed to the kernel dynamically on boot, Easyboot uses only these tags. However it does not generate all that Multiboot2 specifies (it simply omits the old, obsoleted or legacy ones). These tags are called the MBI tags, see Boot information (section 3.6).

NOTE: the Multiboot2 specification on MBI tags is buggy as hell. You can find a fixed up version below, which aligns with the multiboot2.h header file that you can find in GRUB's source repository.

The first parameter to your kernel is the magic 0x36d76289 (in rax, rcx and rdi). You can locate the MBI tags using the second parameter (in rbx, rdx and rsi). On ARM platform magic is in x0 and address is in x1. On RISC-V and MIPS a0 and a1 used, respectively. If and when this loader is ported to another architecture, then always the registers specified by SysV ABI for function arguments must be used. If there are other common ABIs on the platform which do not interfere with SysV ABI, then the values should be duplicated in those ABI's registers (or on the top of the stack) too.

Headers

The passed address is always 8-bytes aligned, and starts with an MBI header:

        +-------------------+
u32     | total_size        |
u32     | reserved          |
        +-------------------+

This is followed by a series of also 8-bytes aligned tags. Every tag begins with the following tag header fields:

        +-------------------+
u32     | type              |
u32     | size              |
        +-------------------+

type contains an identifier of contents of the rest of the tag. size contains the size of tag including header fields but not including padding. Tags follow one another padded when necessary in order for each tag to start at 8-bytes aligned address.

Terminator

        +-------------------+
u32     | type = 0          |
u32     | size = 8          |
        +-------------------+

Tags are terminated by a tag of type 0 and size 8.

Boot command line

        +-------------------+
u32     | type = 1          |
u32     | size              |
u8[n]   | string            |
        +-------------------+

string contains the command line specified in menuentry's kernel line (without the kernel's path and filename). The command line is a normal C-style zero-terminated UTF-8 string.

Boot loader name

        +----------------------+
u32     | type = 2             |
u32     | size = 17            |
u8[n]   | string "Easyboot"    |
        +----------------------+

string contains the name of a boot loader booting the kernel. The name is a normal C-style UTF-8 zero-terminated string.

Modules

        +-------------------+
u32     | type = 3          |
u32     | size              |
u32     | mod_start         |
u32     | mod_end           |
u8[n]   | string            |
        +-------------------+

This tag indicates to the kernel what boot module was loaded along with the kernel image, and where it can be found. The mod_start and mod_end contain the start and end physical addresses of the boot module itself. You'll never get a gzip compressed buffer, because Easyboot transparently uncompresses those for you (and if you provide a plugin, also works with other than gzip compressed data too). The string field provides an arbitrary string to be associated with that particular boot module; it is a normal C-style zero-terminated UTF-8 string. Specified in menuentry's module line and its exact use is specific to the operating system. Unlike the boot command line tag, the module tags also include the module's path and filename.

One tag appears per module. This tag type may appear multiple times. If an initial ramdisk was loaded along with your kernel, then that will appear as the first module.

There's a special case, if the file is a DSDT ACPI table, an FDT (dtb) or GUDT blob, then it won't appear as a module, rather ACPI old RSDP (type 14) or ACPI new RSDP (type 15) will be patched and their DSDT replaced with the contents of this file.

Memory map

This tag provides memory map.

        +-------------------+
u32     | type = 6          |
u32     | size              |
u32     | entry_size = 24   |
u32     | entry_version = 0 |
varies  | entries           |
        +-------------------+

size contains the size of all the entries including this field itself. entry_size is always 24. entry_version is set at 0. Each entry has the following structure:

        +-------------------+
u64     | base_addr         |
u64     | length            |
u32     | type              |
u32     | reserved          |
        +-------------------+

base_addr is the starting physical address. length is the size of the memory region in bytes. type is the variety of address range represented, where a value of 1 indicates available RAM, value of 3 indicates usable memory holding ACPI information, value of 4 indicates reserved memory which needs to be preserved on hibernation, value of 5 indicates a memory which is occupied by defective RAM modules and all other values currently indicated a reserved area. reserved is set to 0 on BIOS boots.

When the MBI generated on a UEFI machine, then various EFI Memory Map entries are stored as type 1 (available RAM) or 2 (reserved RAM), and should you need it, the original EFI Memory Type is placed in the reserved field.

The map provided is guaranteed to list all standard RAM that should be available for normal use, and it is always ordered by ascending base_addr. This available RAM type however includes the regions occupied by kernel, mbi, segments and modules. Kernel must take care not to overwrite these regions (Easyboot could easily exclude those regions, but that would break Multiboot2 compatibility).

Framebuffer info

        +----------------------------------+
u32     | type = 8                         |
u32     | size = 38                        |
u64     | framebuffer_addr                 |
u32     | framebuffer_pitch                |
u32     | framebuffer_width                |
u32     | framebuffer_height               |
u8      | framebuffer_bpp                  |
u8      | framebuffer_type = 1             |
u16     | reserved                         |
u8      | framebuffer_red_field_position   |
u8      | framebuffer_red_mask_size        |
u8      | framebuffer_green_field_position |
u8      | framebuffer_green_mask_size      |
u8      | framebuffer_blue_field_position  |
u8      | framebuffer_blue_mask_size       |
        +----------------------------------+

The field framebuffer_addr contains framebuffer physical address. The field framebuffer_pitch contains the length of one row in bytes. The fields framebuffer_width, framebuffer_height contain framebuffer dimensions in pixels. The field framebuffer_bpp contains number of bits per pixel. framebuffer_type is always set to 1, and reserved always contains 0 in current version of specification and must be ignored by OS image. The remaining fields describe the packed pixel format, the channels' position and size in bits. You can use the expression ((~(0xffffffff << size)) << position) & 0xffffffff to get an UEFI GOP like channel mask.

EFI 64-bit system table pointer

This tag only exists if Easyboot is running on a UEFI machine. On a BIOS machine this tag never generated.

        +-------------------+
u32     | type = 12         |
u32     | size = 16         |
u64     | pointer           |
        +-------------------+

This tag contains pointer to EFI system table.

EFI 64-bit image handle pointer

This tag only exists if Easyboot is running on a UEFI machine. On a BIOS machine this tag never generated.

        +-------------------+
u32     | type = 20         |
u32     | size = 16         |
u64     | pointer           |
        +-------------------+

This tag contains pointer to EFI image handle. Usually it is boot loader image handle.

SMBIOS tables

        +-------------------+
u32     | type = 13         |
u32     | size              |
u8      | major             |
u8      | minor             |
u8[6]   | reserved          |
        | smbios tables     |
        +-------------------+

This tag contains a copy of SMBIOS tables as well as their version.

ACPI old RSDP

        +-------------------+
u32     | type = 14         |
u32     | size              |
        | copy of RSDPv1    |
        +-------------------+

This tag contains a copy of RSDP as defined per ACPI 1.0 specification. (With a 32-bit address.)

ACPI new RSDP

        +-------------------+
u32     | type = 15         |
u32     | size              |
        | copy of RSDPv2    |
        +-------------------+

This tag contains a copy of RSDP as defined per ACPI 2.0 or later specification. (With a 64-bit address probably.)

These (type 14 and 15) point to an RSDT or XSDT table with a pointer to a FACP table, which in turn contains two pointers to a DSDT table, which describes the machine. Easyboot fakes these tables on machines that do not support ACPI otherwise. Also if you provide a DSDT table, an FDT (dtb) or GUDT blob as a module, then Easyboot will patch the pointers to point to that user provided table. To parse these tables, you can use my dependency-free, single header hwdet library (or the bloated apcica and libfdt).

Kernel-specific Tags

Tags with type greater than or equal to 256 are not part of the Multiboot2 specification, nonetheless provided by Easyboot. These might be added by optional plugins to the list, if and when a kernel needs them.

EDID

        +-------------------+
u32     | type = 256        |
u32     | size              |
        | copy of EDID      |
        +-------------------+

This tag contains a copy of the supported monitor resolution list according to the EDID specification.

SMP

        +-------------------+
u32     | type = 257        |
u32     | size              |
u32     | numcores          |
u32     | running           |
u32     | bspid             |
        +-------------------+

This tag exists if multicore directive was given. numcores contains the number of CPU cores in the system, running is the number of cores that have successfully initialized and running the same kernel in parallel. The bspid contains the BSP core's identifier (on x86 lAPIC id), so that kernels can distinguish APs and run a different code on those. All APs have their own stack, and on top of the stack there'll be the id of the current core.

Partition Identifiers

        +-------------------+
u32     | type = 258        |
u32     | size = 24 / 40    |
u128    | bootuuid          |
u128    | rootuuid          |
        +-------------------+

This tag contains the unique identifier fields in the GPT of the boot and root partition's. If the booting does not use a GUID Partitioning Table, then bootuuid is generated as 54524150-(device code)-(partition number)-616F6F7400000000.

Memory Layout

BIOS machines

Start End Description
0x0 0x400 Interrupt Vector Table (usable, real mode IDT)
0x400 0x4FF BIOS Data Area (usable)
0x4FF 0x500 BIOS boot drive code (most likely 0x80, usable)
0x500 0x5A0 synchronization data for SMP (usable)
0x5A0 0x1000 exception handler stack (usable after you set up your IDT)
0x1000 0x8000 paging tables (usable after you set up your paging tables)
0x8000 0x20000 loader code and data (usable after you set up your IDT)
0x20000 0x40000 config + tags (usable after MBI parsed)
0x40000 0x90000 plugin ids; from the top to bottom: kernel's stack
0x90000 0x9A000 Linux kernel only: zero page + cmdline
0x9A000 0xA0000 Extended BIOS Data Area (better not to touch)
0xA0000 0xFFFFF VRAM and BIOS ROM (not usable)
0x100000 x kernel segments, followed by the modules, each page aligned

UEFI machines

Nobody knows. UEFI allocates memory as it pleases. Expect anything and everything. All area will be surely listed in the memory map as type = 1 (MULTIBOOT_MEMORY_AVAILABLE) and reserved = 2 (EfiLoaderData), however this isn't exclusive, other kinds of memory too might be listed like that (boot manager's bss section for example).

Raspberry Pi

Start End Description
0x0 0x500 reserved by firmware (better not to touch)
0x500 0x5A0 synchronization data for SMP (usable)
0x5A0 0x1000 exception handler stack (usable after you set up your VBAR)
0x1000 0x9000 paging tables (usable after you set up your paging tables)
0x9000 0x20000 loader code and data (usable after you set up your VBAR)
0x20000 0x40000 config + tags (usable after MBI parsed)
0x40000 0x80000 firmware provided FDT (dtb); from the top to bottom: kernel's stack
0x100000 x kernel segments, followed by the modules, each page aligned

The first few bytes are reserved for armstub. Only the core 0 started, so to start Application Processors, write a function's address to 0xE0 (core 1), 0xE8 (core 2), 0xF0 (core 3), which addresses are located in this area. This is irrelevant when the multicore directive is used, then all cores will execute the kernel.

Although natively not supported on the RPi, you still get an ACPI old RSDP (type 14) tag, with fake tables. The APIC table is used to communicate the number of available CPU cores to the kernel. The startup function address is stored in the RSD PTR -> RSDT -> APIC -> cpu[x].apic_id field (and core id in cpu[x].acpi_id, where BSP is always cpu[0].acpi_id = 0 and cpu[0].apic_id = 0xD8. Watch out, "acpi" and "apic" looks pretty similar).

If a valid FDT blob is passed by the firmware, or if one of the modules is a .dtb, .gud or .aml file, then a FADT (with magic FACP) table is also added. In this table, the DSDT pointer (32-bit, at offset 40) is pointing to the provided flattened device tree blob.

Although no memory map feature provided by the firmware, you'll still get a Memory Map (type 6) tag too, listing detected RAM and the MMIO region. You can use this to detect the MMIO's base address, which is different on RPi3 and RPi4.