summaryrefslogtreecommitdiff
path: root/doc/boot/uefi.md
blob: 0b20299e0dabee3a7eb8949002b734bed375389c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# UEFI
UEFI is Unified Extensible Firmware Interface



## How Linux is booting with uefi (without grub)
It has own built-in UEFI stub application
How it works (i hope):

linux has EFI stub, that located in arch/x86/boot/compressed/efi_stub_entry.S and drivers/firmware/efi/libstub/*
when we build a kernel with CONFIG_EFI_STUB=y, the final vmlinuz is PE32+ executable (https://docs.kernel.org/admin-guide/efi-stub.html). 
What this stub does before handing off to the real kernel:
1) Calls Graphics Output Protocol to set up a framebuffer
2) Retrieves the UEFI memory mpa via GetMemoryMap()
3) Locates the ACPI tables (RSDP) and passes them forward
4) Loads the initrd/initramfs into memory (via UEFI file protocol or passed by a bootloader like grub)
5) Calls ExitBootServices() - this is the point of no return where UEFI loses control of hardware
6) Jumps to real kernel entry point with all collected info in a boot_params structure (arch/x86/include/uapi/asm/bootparam.h)


## How Linux is booting with uefi with GRUB
Here bootloader are itself UEFI application (we can check it via looking at grubx64.efi on boot EFI partition).
Se the chain now looks like: UEFI firmware -> GRUB (EFI app) -> Linux EFI stub -> real kernel. 

BTW, we can boot linux directly by placing kernel as EFI/Boot/bootx64.efi on the EFI partition (it will run kernel, but without initram)



## Core Problem
Because of bastion kernel is in ELF format, we can't simply run it as efi application.
So, because of it i decided to split booting into two files:
1) loader.efi - efi application, that do all efi staff and run kernel file
2) bastion.elf (or kernel.elf) - kernel itself

## Boot chain

1) UEFI firmware loads loader.efi

2) GET EFI_SYSTEM_TABLE pointer (passed by UEFI firmware)

3) Open the kernel ELF file from the EFI partition (Use EFI_SIMPLE_FILE_SYSTEM_PROTOCOL -> open volume -> open file)

4) Read and parse ELF64 headers, validate arch

5) Allocate pages for each PT_LOAD segment (Use BootServices->AllocatePages())

6) Load segments into allocated memory at their p_vaddr

7) Locate the GOP (Graphic Output Protocol) (Get framebuffer base, resolution, pitch, pixel format)

8) Locate ACPI tables (for x86_64) or Device Tree pointer (for arm)  (Seach EFI Configuration Table for ACPI2_GUId or FDT_GUID)

9) Get the final memory map via GetMemoryMap() (MUST be the last Boot Services call befofe exit)

10) Call ExitBootServices(ImageHandle, MapKey) (it's point of no return - no more uefi calls)

11) Set up initial page tables 

12) Jump to kernel entry point, passing a BootInfo struct (locates in boot_info.h)


## Key UEFI Protocols i use

| Protocol | GUID | Purpose |
|---|---|---|
| `EFI_LOADED_IMAGE_PROTOCOL` | `5B1B31A1-...` | Get the device handle  loader was loaded from |
| `EFI_SIMPLE_FILE_SYSTEM_PROTOCOL` | `964E5B22-...` | Open the ESP volume to read kernel file |
| `EFI_FILE_PROTOCOL` | — | Open/read/close files on the ESP |
| `EFI_GRAPHICS_OUTPUT_PROTOCOL` | `9042A9DE-...` | Get framebuffer address, set video mode |
| `EFI_BOOT_SERVICES` | — | `AllocatePages`, `GetMemoryMap`, `LocateProtocol`, `ExitBootServices` |

---



## The Toolchain Problem

EFI loader must be a **PE32+** binary. Two ways to handle this:

**Option 1 — `gnu-efi`** — A lightweight library that provides EFI headers and a CRT0 that wraps `efi_main()`. Compile with GCC/Clang targeting ELF, then `objcopy` converts it to PE32+. This is the simplest approach.

**Option 2 — Clang with MSVC target** — Clang can directly emit PE32+ if use `--target=x86_64-unknown-windows` with `-fno-stack-protector -fshort-wchar`. No conversion step, but i need to handle the entry point conventions manually. I don't like this optien

**Option 1 with `gnu-efi` is more common** in the simple OS world and works for both architectures.

### How Linux solves it
#### Approach 1: objcopy conrevsion (the old way)
This is what gnu-efi does and what Linux used to do.
```
gcc -c -o stub.o stub.c # Compile as ELF

ld -shared -o stub.so stub.o # Link as ELF shared object

objcopy --target=elf-app-x86_64 stub.so stub.efi # Convert ELF to PE32+
```
But there is a problem: objcopy's PE support is fragile, relocations can break in subtle ways, and debugging is so painful because the format the debugger sees(PE) doesn't match watht the compiler emited (ELF).
#### Approach 2: PE header in assembly
look image_format.md for more information
in two words, Linux manually writes PE header into final Image  (look arch/x86/boot/header.S)

### ARM64 (case with EFI slub)
Linux arm64 header (arch/arm64/kernel/efi-header.S).
The first two bytes are 0x4D 0x5A. So UEFI sees this bytes, then reads it as PE, follows PE header to the entry point. (

### How I solve it (go via Option 2)
Clang by default supports Windows/PE targets, so i can simply do:
```
# Compile directly as PE/COFF
clang++ --target=x86_64-unknown-windows -c -o entry.o entry.cpp

# Link directly as PE32+ EFI application via lld-link (llvm-objcopy doesn't support efi-app-x86_64)
lld-link -subsystem:efi_application -entry:efi_main entry.o -out:BOOTX64.EFI
```
### Future
In future i want to use hand-written PE (like in Linux) without clang dependency and without gnu-efi


## Dual-Arch Boot Strategy
The UEFI spec is architecturally neutral — the same protocols exist on x86_64 and aarch64. So loader code will be ~80% shared
boot/
├── common/
│   ├── efi_loader.cpp       # Main logic: load ELF, get GOP, get mmap
│   ├── elf_parser.cpp       # Parse ELF64 headers and segments
│   └── boot_info.h          # Shared handoff structure
├── x86_64/
│   ├── entry.cpp            # efi_main() → call common loader → jump to kernel
│   └── linker.ld            # PE32+ layout for x86_64
└── aarch64/
    ├── entry.cpp            # efi_main() → call common loader → jump to kernel
    └── linker.ld            # PE32+ layout for aarch64




## ExitBootService staff

The sequence must be:
1) Call GetMemoryMap() -- returns MapKey
2) Immediately call ExitBootService(ImageHandle, MapKey);
3) If it fails (returns EFI_INVALID_PARAMETR), the memory map changed between steps 1 and 2, so we must call GetMemoryMap() again (go to first step)
4) After success, we **can not** call any Boot Service - the firmware is gone