You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: blog/content/edition-3/posts/02-booting/uefi/index.md
+47-27Lines changed: 47 additions & 27 deletions
Original file line number
Diff line number
Diff line change
@@ -748,52 +748,72 @@ Note that we also need to call `uefi::alloc::exit_boot_services()` before exitin
748
748
749
749
## Creating a Bootloader
750
750
751
+
Now that we know how to set up a framebuffer and query relevant system information, we only need a few more steps to turn our UEFI application into a bootloader. These steps includes loading a kernel executable into memory, setting up an execution environment, and passing control to the kernel's entry point function. In the following, we give some high-level instructions for each of these steps.
751
752
752
753
### Loading the Kernel
753
754
754
-
We already saw how to set up a framebuffer for screen output and query the physical memory map and the APIC base register address. This is already all the system information that a basic kernel needs from the bootloader.
755
+
The first step is to load the kernel executable and setting it up properly. This involves loading the kernel from disk into memory, allocating a stack for it, and setting up a new page table hierarchy to properly map it to virtual memory.
755
756
756
-
The next step is to load the kernel executable. This involves loading the kernel from disk into memory, allocating a stack for it, and setting up a new page table hierarchy to properly map it to virtual memory.
757
+
One approach for including our kernel could be to place it in the FAT partition created by our `disk_image` crate. Then we could use the _simple file system_ protocol of UEFI (see section 12.3 of the standard ([PDF][uefi-pdf])) to load it from disk into memory. The `uefi` crate supports this protocol through its [`SimpleFileSystem`] type.
757
758
758
-
#### Loading it from Disk
759
-
760
-
One approach for including our kernel could be to place it in the FAT partition created by our `disk_image` crate. Then we could use the TODO protocol of the `uefi` crate to load it from disk into memory.
To keep things simple, we will use a different appoach here. Instead of loading the kernel separately, we place its bytes as a `static` variable inside our bootloader executable. This way, the UEFI firmware directly loads it into memory when launching the bootloader. To implement this, we can use the [`include_bytes`] macro of Rust's `core` library:
Now that we have our kernel executable in memory, we need to parse it. In the following, we assume that the kernel uses the ELF executable format, which is popular in the Linux world. This is also the excutable format that the kernel created in this blog series uses.
771
-
772
-
The ELF format is structured like this:
773
-
774
-
TODO
775
-
776
-
The various headers are useful in different situations. For loading the executable into memory, the _program header_ is most relevant. It looks like this:
769
+
After loading the kernel executable into memory (one way or another), we need to parse it. In the following, we assume that the kernel uses the [ELF] executable format, which is popular in the Linux world. This is also the excutable format that the kernel created in this blog series uses.
777
770
778
-
TODO
771
+
The ELF format consists of several headers that describe the executable and define a number of sections. Typically, there is a section called `.text` that contains the actual executable code. Immutable values such as string constants are placed in a section named `.rodata` ("read-only data"). For mutable data (e.g. a `static` containing a `Mutex`), a section named `.data` is used. There is also a section named `.bss` that stores all data that is initialized with zero values (this allows to reduce the size of the binary).
779
772
780
-
TODO: mention readelf/objdump/etc for looking at program header
773
+
The various ELF headers are useful in different situations. For loading the executable into memory, the _program header_ is most relevant. It basically groups all the sections of the executable into different groups by their access permissions. There are typically four groups:
781
774
782
-
There are already a number of ELF parsing crates in the Rust ecosystem, so we don't need to create our own. In the following, we will use the [`xmas_elf`] crate, but other crates might work equally well.
775
+
- Read-only and executable: This contains the `.text` section and all other executable code.
776
+
- Read-only: This contains the `.rodata` section and all other sections with immutable, non-executable data.
777
+
- Read-write: This includes the `.data` section and `.bss` sections. The zeroes of the `.bss` section are not actually stored, only its size is listed. Thus, no memory is wasted for storing zeroes.
783
778
784
-
TODO: load program segements and print them
779
+
There are various tools to analyze ELF files and read out most headers. The classical tools are `readelf` and `objdump`. There are also several Rust crates for parsing an ELF files, so we don't need to to implement it on our own. Some examples are [`goblin`], [`elf`], and [`xmas-elf`]. The `xmas-elf` crate works quite well in `no_std` environments, so that's the one I would recommend for a bootloader implementation.
785
780
786
-
TODO: .bss section -> mem_size might be larger than file_size
// TODO: create page table mapping for frame with permissions
804
+
// at corresponding virtual address
805
+
}
806
+
807
+
ifvirt_end>phys_end {
808
+
// TODO: there is a `.bss` section in this segment -> map next
809
+
// (virt_end - phys_end) bytes to free physical frame and initialize
810
+
// them with zero
811
+
}
812
+
}
813
+
}
814
+
```
795
815
796
-
#### Create a Stack
816
+
After creating a new page table mapping for the kernel this way, we need to allocate a runtime stack for it. For that, we choose a region of unused physical memory and map it to some virtual address. Ideally, we choose the virtual address range in a way that the page immediately before it is not mapped. Thus, we create a so-called _guard page_ that ensures that stack overflows lead to a CPU exception (a page fault) instead of corrupting other data.
0 commit comments