Running SerenityOS on Bare Metal
Published on 2022-02-19.In a previous post, I mentioned:
I haven't managed to do a full successful boot on bare metal yet though, so that will definitely have to join this list one day.
Well, five months later I finally did it! :^)
View tweet: https://twitter.com/linusgroh/status/1486111320181149697
This is a post explaining what it took to get there, both as documentation for myself and hopefully to help someone else to get Serenity up and running on their hardware!
Hardware
While I also tried running SerenityOS on my laptop in the past (ThinkPad T450S), all serious attempts were on my desktop machine, as the availability of a serial port is highly important for debugging — especially when the kernel panics before even setting up a framebuffer and console for graphical output, and you only end up with a blank screen after booting. The alternative to this is beeping the PC speaker…
This is also the machine I do recording and some development on, so I didn't want to mess with the Linux partitions and ended up just giving Serenity its own SSD.
- Motherboard: MSI B350 PC MATE
- CPU: AMD Ryzen 5 1600x
- Graphics card: NVIDIA GeForce RTX 2060
- RAM: 4 x 8 GB DDR4 (Crucial)
- Disk: Serenity/Linux each on their own SSD (also Crucial)
- Monitor: 27 inch QHD (Dell), connected to the GPU's HDMI output (CPU has no integrated graphics!)
Setup
Note that I won't explain the general bare metal installation process here, that's all outlined in Documentation/BareMetalInstallation.md
.
I usually keep my local checkout up to date, to get the latest kernel and userspace changes. This can mean that things that used to work suddenly stop working, and another investigation is required. That's ok though, as things improve for the most part. :^)
A recent example of this is an update to the i8042 initialisation process that would fail on my machine (I8042: Trying to probe for existence of controller failed
), causing the mouse and keyboard to not work. Fixed in #12641.
For the record though: I'm using commit eca8208
as of writing this. I also tested i686 and x86_64, and they both boot.
Building & Creating a Bootable Disk
Basically: apply patches (discussed later), do a regular build, create GRUB disk image, dd
to disk, reboot.
Serial Console
I use an adapter cable that's permanently connected to the 9-pin JCOM1
connector on the motherboard and has an RS-232 connector on the other end. Then I plug in an RS-232 to USB cable whenever I need to and connect from my laptop by running cu -s 57600 -l /dev/ttyUSB0
in a terminal, where I will then see exactly what QEMU would show on the debug console.
Input Devices
While trying to get Serenity to boot I already knew I wouldn't be able to actually use it once successful — USB HID devices aren't working in the kernel yet. The recommended first step is checking if the BIOS has an option to emulate PS/2 input devices; mine doesn't, but luckily the computer has a PS/2 port, so I ordered a cheap PS/2 mouse and keyboard. In an ironic twist, both the USB mouse and keyboard start working on Serenity once their PS/2 counterparts are plugged in, so the BIOS does have some sort of emulation, just doesn't seem to expose an option for it. I haven't investigated further.
Required Tweaks
As mentioned before, I still have to do some small modifications for Serenity to actually boot.
Additional Kernel Options
While all of these could be provided in GRUB on demand, I think it makes sense to hardcode them if the boot is known to fail without them.
serial_debug
: This should be self explanatory. I boot with serial debug enabled by default for now, but disabling it greatly decreases boot time, so I won't keep it forever. Can still be enabled manually in GRUB of course.enable_ioapic=off
: Without this option, I used to get the following panic:
This was fixed in #12182, but still causes the boot to hang indefinitely when0.000 [Kernel]: APICTimer: Using HPET as calibration source [Kernel]: KERNEL PANIC! :^( [Kernel]: Unhandled IRQ [Kernel]: at ./Kernel/Arch/x86/common/Interrupts.cpp:532 in void Kernel::unimp_trap()
on
.ahci_reset_mode=aggressive
: I wish I tried this option much earlier — many of my previous attempts failed at the stage where it was trying to locate the root file system, with the following panic:
The different reset mode fixes that![init_stage2(1:1)]: ASSERTION FAILED: !m_storage_devices.is_empty() [init_stage2(1:1)]: ./Kernel/Storage/StorageManagement.cpp:153 in void Kernel::StorageManagement::enumerate_disk_partitions() [init_stage2(1:1)]: KERNEL PANIC! :^( [init_stage2(1:1)]: Aborted [init_stage2(1:1)]: at ./Kernel/Arch/x86/common/CPU.cpp:35 in void abort() [init_stage2(1:1)]: Kernel + 0x00c39df1 Kernel::__panic(char const*, unsigned int, char const*) +0xf1 [init_stage2(1:1)]: Kernel + 0x00fbb2d9 abort.localalias +0x245 [init_stage2(1:1)]: Kernel + 0x00fbb094 abort.localalias +0x0 [init_stage2(1:1)]: Kernel + 0x011c37f9 Kernel::StorageManagement::enumerate_disk_partitions() [clone .localalias] +0xdf9 [init_stage2(1:1)]: Kernel + 0x011c92c5 Kernel::StorageManagement::initialize(AK::StringView, bool, bool) +0xb5 [init_stage2(1:1)]: Kernel + 0x00f9d4ff Kernel::init_stage2(void*) +0x11af [init_stage2(1:1)]: Kernel + 0x00fc3a50 exit_kernel_thread +0x0
So the patch to add all of them to the first GRUB menu entry looks as follows:
--- a/Meta/grub-mbr.cfg
+++ b/Meta/grub-mbr.cfg
@@ -2,7 +2,7 @@ timeout=1
menuentry 'SerenityOS (normal)' {
root=hd0,1
- multiboot /boot/Prekernel root=/dev/hda1
+ multiboot /boot/Prekernel root=/dev/hda1 serial_debug enable_ioapic=off ahci_reset_mode=aggressive
module /boot/Kernel
}
Note that in many circumstances you will also need to specify a different value for the root
option, especially in a dual boot environment or when having multiple disks. I simply reordered the connected disks so that Serenity is connected to SATA1
and the default option (root=/dev/hda1
) just works. :^)
Increasing INITIAL_KMALLOC_MEMORY_SIZE
With the default value of 2 MiB, the kernel panics:
[Kernel]: ASSERTION FAILED: m_has_value
[Kernel]: ././AK/Optional.h:157 in T& AK::Optional::value() & [with T = KmallocGlobalData::ExpansionData]
[Kernel]: KERNEL PANIC! :^(
[Kernel]: Aborted
[Kernel]: at ./Kernel/Arch/x86/common/CPU.cpp:35 in void abort()
Various kernel changes have been made over time to reduce the amount of initial kmalloc memory that's needed, but it's still not enough in my case.
Increasing it to 8 MiB fixes the panic:
--- a/Kernel/Heap/kmalloc.cpp
+++ b/Kernel/Heap/kmalloc.cpp
@@ -23,7 +23,7 @@ static constexpr size_t CHUNK_SIZE = 32;
static constexpr size_t CHUNK_SIZE = 64;
#endif
-static constexpr size_t INITIAL_KMALLOC_MEMORY_SIZE = 2 * MiB;
+static constexpr size_t INITIAL_KMALLOC_MEMORY_SIZE = 8 * MiB;
// Treat the heap as logically separate from .bss
__attribute__((section(".heap"))) static u8 initial_kmalloc_memory[INITIAL_KMALLOC_MEMORY_SIZE];
Enabling the MULTIBOOT_VIDEO_MODE
Flag
This is needed as I rely on Multiboot (which we use when booting in combination with GRUB) to prepare a framebuffer for the kernel. As far as I understand, QEMU & co. use a different approach, so it's not enabled by default.
--- a/Kernel/Prekernel/Arch/x86/multiboot.S
+++ b/Kernel/Prekernel/Arch/x86/multiboot.S
@@ -3,7 +3,7 @@
.set MULTIBOOT_PAGE_ALIGN, 0x1
.set MULTIBOOT_MEMORY_INFO, 0x2
.set MULTIBOOT_VIDEO_MODE, 0x4
-.set multiboot_flags, MULTIBOOT_PAGE_ALIGN | MULTIBOOT_MEMORY_INFO
+.set multiboot_flags, MULTIBOOT_PAGE_ALIGN | MULTIBOOT_MEMORY_INFO | MULTIBOOT_VIDEO_MODE
.set multiboot_checksum, -(MULTIBOOT_MAGIC + multiboot_flags)
.section .multiboot, "a"
Changing the WindowServer Screen Resolution
The default WindowServer screen resolution is 1024x768. However, the framebuffer we ask Multiboot to set up for us has a resolution of 1280x1024 by default:
/* for MULTIBOOT_VIDEO_MODE */
.long 0x00000000 /* mode_type */
.long 1280 /* width */
.long 1024 /* height */
.long 32 /* depth */
While it's possible to change these values (which would be great, as the monitor resolution is much larger), any attempts to do so resulted in a 640x480 framebuffer. Checking vbeinfo
in a GRUB console revealed that no larger resolution is supported by this particular VBE setup:
grub> vbeinfo
List of supported video modes:
Legend: mask/position=red/green/blue/reserved
Adapter `VESA BIOS Extension Video Driver':
VBE info: version: 3.0 OEM software rev: 144.6
total memory: 16384 KiB
Ox101 640 x 480 x 8 ( 640) Paletted
0x103 800 x 600 x 8 (1024) Paletted
Ox105 1024 x 768 x 8 (1024) Paletted
Ox107 1280 x 1024 x 8 (1280) Paletted
Ox111 640 x 480 x 16 (1280) Direct color, mask: 5/6/5/0 pos: 11/5/0/0
Ox112 640 x 480 x 32 (2560) Direct color, mask: 8/8/8/8 pos: 16/8/0/24
Ox114 800 x 600 x 16 (2048) Direct color, mask: 5/6/5/0 pos: 11/5/0/0
0x115 800 x 600 x 32 (4096) Direct color, mask: 8/8/8/8 pos: 16/8/0/24
Ox117 1024 x 768 x 16 (2048) Direct color, mask: 5/6/5/0 pos: 11/5/0/0
Ox118 1024 x 768 x 32 (4096) Direct color, mask: 8/8/8/8 pos: 16/8/0/24
Ox11a 1280 x 1024 x 16 (2560) Direct color, mask: 5/6/5/0 pos: 11/5/0/0
Ox11b 1280 x 1024 x 32 (5120) Direct color, mask: 8/8/8/8 pos: 16/8/0/24
grub>
Changing the resolution in DisplaySettings doesn't do anything either (see #4414).
This situation will likely improve in the future with more native graphics drivers (we already have one for Intel graphics), but for now I set the monitor to a 5:4 aspect ratio and apply this patch to make use of the full available resolution:
--- a/Base/etc/WindowServer.ini
+++ b/Base/etc/WindowServer.ini
@@ -5,8 +5,8 @@ MainScreen=0
Device=/dev/fb0
Left=0
Top=0
-Width=1024
-Height=768
+Width=1280
+Height=1024
ScaleFactor=1
[Fonts]
Demo
With all of these changes, it finally boots! The only major issue I found is that time advances way too quickly — we'd have to adjust it via NTP every now and then. Everything else seems to work just fine, including networking :^)
Thanks!
None of this would've been possible without Idan, Liav, Luke & all the other folks in the #bare-metal
channel on our Discord server. I needed quite a bit of hand holding to understand the various panics and find workarounds or fixes for them!