Creating multiboot ELF kernels with FASM
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:
- Multiboot 1 (1995)
- Multiboot 2 (somewhere around ~2007)
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.
- Knowledge of x86 assembly in intel syntax
- FASM: available on windows and linux. written w/ 1.71.62
- QEMU: qemu system i386 is required if you want to try booting it
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.
Creating an ELF with FASM
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 ; 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:
Check your work pt 1
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:
|12||u32||header_addr||present if flags set|
|16||u32||load_addr||present if flags set|
|20||u32||load_end_addr||present if flags set|
|24||u32||bss_end_addr||present if flags set|
|28||u32||entry_addr||present if flags set|
|32||u32||mode_type||present if flags set|
|36||u32||width||present if flags set|
|40||u32||height||present if flags set|
|44||u32||depth||present if flags set|
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.
Adding multiboot header
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
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
1100 0000 0000 0001
Following the multiboot docs,
we have set the following bits:
- bit 0 (MULTIBOOT_ALIGN): align on page boundaries
- bit 1 (MULTIBOOT_MEMINFO): fill out the mem_* fields of the header
- bit 16 (MULTIBOOT_AOUT_KLUDGE): fields at 12-28 of MB header are valid, use those over the ELF header to determine loading addresses.
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.
this satisifes the 'magic' part of the header for multiboot1
Put our flags we set above in the right spot
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
_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:
This gives an effective memory address in our kstart label.
Check your work pt 2
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.
Add some video functionality
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 _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
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
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
next steps and other resources
Understanding writing to the video memory above: https://wiki.osdev.org/Printing_To_Screen