1. Don't link the bootsector and the rest of your binary together. It's more trouble than it's worth, and if you ever progress to loading off a filesystem, then you won't be linking them together anyway.
2. You can use LBA addressing in real mode with int 13h (just specify a hard disk rather than a floppy when booting):
3. You need to switch to protected mode, set up paging, and then enter long mode.
4. I'd recommend compiling your rust code to an ELF binary, and then using objcopy to create a binary:
objcopy -O binary $input.elf $output.bin
Alternatively, ELF isn't that hard to parse, so you could load it yourself and then copy sections to the right locations with your bootsector.
5. OSDev.org is your friend. Maybe get a C-based kernel running first, as the toolchain when dealing with freestanding code is a lot friendlier for beginners
You might be better off starting with 32-bit rust code instead, in order to avoid paging and long mode at first (you'll still need to switch to protected mode)
I am extremely biased, but I’ve always found the rust tool chain far easier than the C ones, especially as a Windows user. No need to set up a cross compiler, given that rustc already is one, and is easy to add more targets to via the JSON target spec.
A cross compiler is easy enough to set up with clang too (just specify the -target command line option).
I just think that when starting out in kernel development, you're likely going to be dealing with linker scripts for likely the first time. It's been a while since I tried rust for this, but C generates pretty clean object files, so the linker scripts from the OSDev wiki can often be copied verbatim.
1. I have a Makefile that links it for me so I don't have to do anything special. I intend the Rust program to work like a second stage bootloader. I'm going to experiment with it and try to get a file system working to load an actual kernel from a file.
2. I'll look into that.
3. That's what the bootloader does. Sorry if I wasn't clear.
4. If I understand correctly if I compile my Rust project to a binary (which is a default output and I don't really understand why I would use objcopy for that) I won't be able to link with with the bootloader. I link them to allow the bootloader to jump to the _start function at the correct address.
5. My Rust program already works in 64-bit so I don't see a reason to change it. In this project I want to explore low level programming and understand things like paging.
I think there's a typo in the final screenshot. It looks like your original code outputs "RM" (maybe "real mode" or "hello Rust" in a different language?), but the example source seems like it should output "HR."
For those checking comments first: this is mostly a tutorial on how to build and link a 16/32 bit MBR loader and mode switch implementation in assembly. It's very similar to what you'll find on osdev.org. The Rust content is limited to a single function at the end which writes some characters to an assumed-pre-initialized EGA text console.
While modern hardware does continue to support these modes via various detection tricks, "real" handoff from the firmware happens in a 64 bit EFI environment these days, and the framebuffer tends to be in a graphics mode and expects to be accesesed initially via the exposed UEFI API (or to be reset from scratch by an OS graphics driver, of course).
This is fun and instructive, but not really reflective of the way modern hardware boots.
There's sort of a paucity of tutorials in that space. You're pretty much stuck reading specs and studying source code. Here's the UEFI specification and a link to the "gnu-efi" project, which provide a reasonably clean (if... idiosyncratic) environment for building and running EFI binaries with a free toolchain:
Broadly: EFI is a simple C environment (albeit one based on windows-style PE binaries and not ELF). The firmware will load and run your application binary and jump to its entry point, passing a giant table of API hooks and callbacks for you to use to interact with its services. You then read and assemble your own OS image using these tools and jump into it.
I believe you can do the same with gcc and gold|gnu ld without much more effort. The toolchain doesn't really justify its complexity at all.
The headers, on the other hand, are invaluable. I use c-efi[1]'s headers, but would probably use gnu efi another time; c-efi has some bugs and missing structures. Worth noting: despite the name, gnu efi isn't gpl licensed. In fact, I don't think either (c-efi or gnu efi) can really be considered copyrightable; they're essentially direct translations of the spec.
Additionally, efi isn't useful for much beyond a bootloader, so you can use clang for the bootloader and gcc (or whatever else) for the OS itself. So you can depend on non-clang features.
> The code that modifies the bzImage header, along with the EFI-specific entry point that the firmware loader jumps to are collectively known as the “EFI boot stub”, and live in arch/x86/boot/header.S and arch/x86/boot/compressed/eboot.c
Another potential point of interest that I remember is that in the Zig 0.5.0 release notes, where UEFI support is mentioned [1], they provide a few links relating to UEFI programming with Zig:
Thanks. That specification is almost 2800 pages long - and it covers far more specific technology than I would have expected. At the end of the day, couldn't the firmware just reserve a fixed chunk of system memory for IO and divide it up between all the attached devices? Then adopt a convention that memory is initialized with the first, say, 1MB available from every device (loaded asynchronously), and execution starts at memory region N - the N being optionally chosen by the user in a pre-boot, BIOS-like screen.
Everyone starts with a simple idea. But by the time every hardware and software company has put in their input/requests/demands it'll end up at thousands of pages.
For some reason (probably due to QEMU), these kinds of bootloader tutorials are still stuck in the BIOS era. UEFI remains completely under-taught, which is probably in part because it's hideously complex.
Note that modern computers are supposed to be dropping BIOS boot support this year[1].
I did something similar many years ago using nothing but debug.exe that came with windows 95, a spare floppy and an older version of The Indispensible PC Hardware Book [0].
It was very satisfying seeing my PC boot to printing my name.
I really appreciate posts like this and wish there were more of them. Tutorials by experts are valuable, but I also find it comforting to read that other people are sometimes as clueless as I am, and I think posts like this can end up explaining some of the pitfalls that some more expert users aren't really aware of. We need more humility in software.
(I remember that I had tried to write a bootloader myself in assembler, maybe 10 years ago or so, and in the end was just incapable of getting it to work.)
If anybody enjoys this and wants to take it much further, you might want to check out https://os.phil-opp.com/. This is basically a book-in-progress that shows how to build an OS in Rust OS from scratch. It shows how boot up an OS, and it goes on to show how to set up basic drivers and memory management. He does a great job of explaining each step as he goes.
(This is linked near the end of the article, but I think it's worth pointing out.)
Full disclosure: I think he uses a library I wrote to initialize the keyboard controller.
36 comments
[ 0.28 ms ] story [ 85.9 ms ] thread1. Don't link the bootsector and the rest of your binary together. It's more trouble than it's worth, and if you ever progress to loading off a filesystem, then you won't be linking them together anyway.
2. You can use LBA addressing in real mode with int 13h (just specify a hard disk rather than a floppy when booting):
https://wiki.osdev.org/ATA_in_x86_RealMode_(BIOS)#LBA_in_Ext...
3. You need to switch to protected mode, set up paging, and then enter long mode.
4. I'd recommend compiling your rust code to an ELF binary, and then using objcopy to create a binary:
Alternatively, ELF isn't that hard to parse, so you could load it yourself and then copy sections to the right locations with your bootsector.5. OSDev.org is your friend. Maybe get a C-based kernel running first, as the toolchain when dealing with freestanding code is a lot friendlier for beginners
- https://wiki.osdev.org/Protected_Mode
- https://wiki.osdev.org/Long_Mode#Long_Mode
You might be better off starting with 32-bit rust code instead, in order to avoid paging and long mode at first (you'll still need to switch to protected mode)
I just think that when starting out in kernel development, you're likely going to be dealing with linker scripts for likely the first time. It's been a while since I tried rust for this, but C generates pretty clean object files, so the linker scripts from the OSDev wiki can often be copied verbatim.
I don’t think that the linker script stuff ends up different but it’s been a minute since I started from scratch.
1. I have a Makefile that links it for me so I don't have to do anything special. I intend the Rust program to work like a second stage bootloader. I'm going to experiment with it and try to get a file system working to load an actual kernel from a file. 2. I'll look into that. 3. That's what the bootloader does. Sorry if I wasn't clear. 4. If I understand correctly if I compile my Rust project to a binary (which is a default output and I don't really understand why I would use objcopy for that) I won't be able to link with with the bootloader. I link them to allow the bootloader to jump to the _start function at the correct address. 5. My Rust program already works in 64-bit so I don't see a reason to change it. In this project I want to explore low level programming and understand things like paging.
I appreciate your feedback.
That or I'm very confused.
great job OP!
While modern hardware does continue to support these modes via various detection tricks, "real" handoff from the firmware happens in a 64 bit EFI environment these days, and the framebuffer tends to be in a graphics mode and expects to be accesesed initially via the exposed UEFI API (or to be reset from scratch by an OS graphics driver, of course).
This is fun and instructive, but not really reflective of the way modern hardware boots.
https://uefi.org/sites/default/files/resources/UEFI_Spec_2_8...
https://sourceforge.net/projects/gnu-efi/
Broadly: EFI is a simple C environment (albeit one based on windows-style PE binaries and not ELF). The firmware will load and run your application binary and jump to its entry point, passing a giant table of API hooks and callbacks for you to use to interact with its services. You then read and assemble your own OS image using these tools and jump into it.
The headers, on the other hand, are invaluable. I use c-efi[1]'s headers, but would probably use gnu efi another time; c-efi has some bugs and missing structures. Worth noting: despite the name, gnu efi isn't gpl licensed. In fact, I don't think either (c-efi or gnu efi) can really be considered copyrightable; they're essentially direct translations of the spec.
1. https://github.com/c-util/c-efi
https://www.kernel.org/doc/html/latest/admin-guide/efi-stub....
Seems to be a little bit out of date, though:
> The code that modifies the bzImage header, along with the EFI-specific entry point that the firmware loader jumps to are collectively known as the “EFI boot stub”, and live in arch/x86/boot/header.S and arch/x86/boot/compressed/eboot.c
I found:
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux...
But no eboot.c - there's this however:
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux...
uefi-examples - tutorials https://github.com/nrdmn/uefi-examples
uefi-paint - UEFI-bootable touch paint app https://github.com/nrdmn/uefi-paint
uefi-freetype - changing the system BIOS to use Comic Sans https://github.com/nrdmn/uefi-freetype https://twitter.com/andy_kelley/status/1176561072398098432
[1]: https://ziglang.org/download/0.5.0/release-notes.html#UEFI-S...
Note that modern computers are supposed to be dropping BIOS boot support this year[1].
[1] https://arstechnica.com/gadgets/2017/11/intel-to-kill-off-th...
It was very satisfying seeing my PC boot to printing my name.
[0] https://www.amazon.co.uk/Indispensable-PC-Hardware-Book/dp/0...
(I remember that I had tried to write a bootloader myself in assembler, maybe 10 years ago or so, and in the end was just incapable of getting it to work.)
1: https://littleosbook.github.io/
(This is linked near the end of the article, but I think it's worth pointing out.)
Full disclosure: I think he uses a library I wrote to initialize the keyboard controller.