I've been meaning to write about this one for a while, since I hacked together some simple OSes last summer! Multiboot is magical.
Multiboot is a specification that provides a standard format for bootloaders to load kernels.
The most common (and in fact only I know of) implementation is unsurprisingly GRUB. There is actually two multiboot specifications at this time:
I will cover creating a multiboot 1 loadable 32 bit kernel using FASM. The killer nice thing is, you can boot an ELF directly with Multiboot.
A basic knowledge of ELF layout is strongly recommended. If you are not familiar with ELF format layout sections, this may be a bit foreign, I recommend at least skimming an overview of the sections, here is a good overview. Ange Albertini (of corkami.com) has amazing poster graphics as well here.
I have to admit the assembler I remember how to use well enough anymore is FASM, so I will cover how to generate a bootable elf with it. It is probably easy enough to translate mnemonics to nasm or gas if you have the docs handy. One benefit of using FASM is it handles linkage for you so you can assemble a working binary in a single step. However, NASM is more popular.
Our first step is to generate an ELF, lets do that first. This will make a buildable empty ELF:
format elf ; you can use elf or binary with mb kludge
org 0x100000 ; this will be your kernel reserved memory
use32 ; 32 bit
_start:
; we will add multiboot header here!
_kstart:
; kernel here
; data here
_end_data:
; bss section here
; reserve bytes for kernel stack
rb 16384
_kstack:
_end:
Save this file as kernel1.asm
Make sure you didnt typo it by trying to compile it:
$ fasm kernel1.asm
flat assembler version 1.71.62 (1048576 kilobytes memory)
2 passes, 16683 bytes.
If you get no errors (and a kernel1.o) it worked. We can now move on to adding a multiboot header!
The only thing you have to do for a binary to be loadable is include a correctly formatted header (and add it to your grub list). Importantly you do not need to fill out the complete header. I've recreated the multiboot 1 header here:
Offset | Type | Field name | Note |
---|---|---|---|
0 | u32 | magic | required |
4 | u32 | flags | required |
8 | u32 | checksum | required |
12 | u32 | header_addr | present if flags[16] set |
16 | u32 | load_addr | present if flags[16] set |
20 | u32 | load_end_addr | present if flags[16] set |
24 | u32 | bss_end_addr | present if flags[16] set |
28 | u32 | entry_addr | present if flags[16] set |
32 | u32 | mode_type | present if flags[2] set |
36 | u32 | width | present if flags[2] set |
40 | u32 | height | present if flags[2] set |
44 | u32 | depth | present if flags[2] set |
You can see the multiboot1 docs here, as well as full latest specification, scroll to 'Boot information format'
The idea is to setup magic, flags, checksum correctly. You toggle certain flags to have multiboot fill out fields you are interested in and let it know what it is loading.
To make the ELF we created above bootable, we need to
fill out the multiboot header. It needs to be the first
thing in the binary the multiboot loader will see.
This means we need to stick it directly under _start
and
that is why there is _start
and _kstart
.
I will present the entire filled out header below.
Add it below the _start
label we created before.
Save this file as kernel2.asm
to keep track
of our work.
; ... snip
_start:
; this is the multiboot header
mbflags=0x03 or (1 shl 16)
dd 0x1BADB002
dd mbflags ; 4k alignment, provide meminfo
dd -0x1BADB002-mbflags ; mb checksum
dd _start ; header_addr
dd _start ; load_addr
dd _end_data ; load_end_addr
dd _end ; bss_end_addr
dd _kstart ; entry point
; end mb header
Here is an explanation of what each line is accomplishing
mbflags=0x03 or (1 shl 16)
This is taking 0x03 and bitwise or'ing it with (1 << 16). 1 shifted left 16 is 65536.
in other words It is taking these two binary values (shown as 16 bit/u16 since thats all we need):
(Note: this is BIG ENDIAN / logical)
0x03: 1100 0000 0000 0000
(1 shl 16): 0000 0000 0000 0001
bitwise or'd: 1100 0000 0000 0001
Following the multiboot docs,
we have set the following bits:
The big gotcha for me said bit 16 is not required for ELF format kernels:
This information does not need to be provided if the kernel image is in elf format, but it must be provided if the images is in a.out format or in some other format.
(which is known in sources as `MULTIBOOT_AOUT_KLUDGE`)
I always had to provide it even with the above ELF, Im unsure if I built the sections incorrectly or otherwise, but all online sources I could find did same when building ELF kernels.
We will put these flags in header later on.
dd 0x1BADB002
this satisifes the 'magic' part of the header for multiboot1
dd mbflags
Put our flags we set above in the right spot
dd -0x1BADB002-mbflags
This is a tricky way to set the checksum of the 3 required fields which needs to be 0 mod 2^32.
I will breeze over the rest, loading our _start
,_end_data
,
_end
and _kstart
addresses into the header with dd lets
multiboot our elf sections and where to jump to after loading.
That's it (phew)!
Lets a hlt in _kstart:
_kstart:
hlt
This gives an effective memory address in our kstart label.
Make sure we can assemble:
$ fasm kernel2.asm
flat assembler version 1.71.62 (1048576 kilobytes memory)
2 passes, 16683 bytes.
In theory, this kernel2.o is actually bootable but we won't be able to tell because it will hang with the QEMU boot messages still visible.
Lets add some basic video memory functionality, I will not cover this in detail, I will provide working code, but its an exercise for the reader.
It will clear the framebuffer and write a message, confirming we did actually load and jump to our _kstart code.
here is the full listing with a hello world:
; fasm multiboot example
; this shows how to use elf (or bin if you want)
; fasm output with a multiboot header with grub
format elf ; you can use elf or binary with mb kludge
org 0x100000
use32
_start:
; this is the multiboot header
mbflags=0x03 or (1 shl 16)
dd 0x1BADB002
dd mbflags ; 4k alignment, provide meminfo
dd -0x1BADB002-mbflags ; mb checksum
dd _start ; header_addr
dd _start ; load_addr
dd _end_data ; load_end_addr
dd _end ; bss_end_addr
dd _kstart ; entry point
; end mb header
; code
_kstart:
; set stack right away
mov esp, _kstack
; grub sets up 80x25 mode for us
mov edi, 0xB8000 ; video memory
; the screen data is left as is, showing
; qemu boot messages and crap, so clear it out
; since we dont care about the actual
; rows and heights we can just linearly nuke the total
; num of bytes:
; 80x25 = 2000 chars,
; each visible char has a value byte and display control byte
; so total bytes = 2000 * 2, 4000 bytes
; however we can simplify this by setting a full 32 bits each
; loop (4 bytes or 2 chars)
mov ecx, 1000
cld
@@:
; set 1F control bytes, 0x00 text bytes
mov dword [edi + ecx * 4], 0x1F001F00
loop @b
; now display a message before halting
mov esi,msg
mov ecx,msglen
@@:
lodsb
stosb
mov byte [edi], 0x1F
inc edi
loop @b
hlt
; data section
msg db 'hello from a multiboot elf'
msglen = $ - msg
_end_data:
; bss uninit data here
; reserve the number of bytes of how big you want the kernel stack to be
rb 16384
_kstack:
_end:
Save it as kernel3.asm
Lets assemble and boot it!
$ fasm kernel3.asm
flat assembler version 1.71.62 (1048576 kilobytes memory)
2 passes, 16683 bytes.
$ qemu-system-i386 -kernel kernel3.o
Amazing!
If you got stuck, full sources and a script to make bootable as an ISO using el-torito (covered later on) is available on my github
Understanding writing to the video memory above: https://wiki.osdev.org/Printing_To_Screen