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]]
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.
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.
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."
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.
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.
+-------------------+
u32 | type = 0 |
u32 | size = 8 |
+-------------------+
Tags are terminated by a tag of type 0
and size 8
.
+-------------------+
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.
+----------------------+
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.
+-------------------+
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.
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).
+----------------------------------+
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.
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.
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.
+-------------------+
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.
+-------------------+
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.)
+-------------------+
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).
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.
+-------------------+
u32 | type = 256 |
u32 | size |
| copy of EDID |
+-------------------+
This tag contains a copy of the supported monitor resolution list according to the EDID specification.
+-------------------+
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.
+-------------------+
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
.
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 |
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).
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.