February 25, 2018

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.

Prerequisites

  • 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
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:

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!

Header magic

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.

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 _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:

  • 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.

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.

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
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!

multiboot

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

NASM version


os bootloader assembly elf


Previous post
easier updates I found a much easier way to blog without messing about, blot.im. This is great! Not that my old setup was hard (static site gen + upload to s3),
Next post
Simple sampling with Box-Muller transforms pt 1 I'm a technically uneducated idiot, so talking about math is a bit above my pay grade, but Box-Muller transform is a straightforward,