home

Booting into a program

After getting the skeleton of a UEFI bootloader up, I need to give it something to load. The overall goal is to have the bootloader load a program into memory at the correct address so that it loads, and give it the tools to write to a console. This post is still following along the Khaled Hammouda’s really great documentation of their os project here.

For work getting the bootloader up see my previous posts part1 and part2

The bootloader is running enough to create some framework for the kernel. After reorganizing some files, my directory tree is looking like this:

.
├── justfile
├── nim.cfg
└── src
    ├── boot
    │   ├── bootx64.nim
    │   └── nim.cfg
    ├── common
    │   ├── libc.nim
    │   ├── malloc.nim
    │   └── uefi.nim
    ├── debugcon.nim
    └── kernel
        ├── main.nim
        └── nim.cfg

The nim configurations are split for their target. This is a nice feature of nim for organizing projects. I added a kernel target for the justfile to create a binary:

nimflags := "--os:any"

bootloader:
  nim c {{nimflags}} --out:build/bootx64.efi src/boot/bootx64.nim

kernel:
  nim c {{nimflags}} --out:build/kernel.bin src/kernel/main.nim

run: bootloader
  mkdir -p diskimg/efi/boot
  cp build/bootx64.efi diskimg/efi/boot/bootx64.efi
  qemu-system-x86_64 \
      -machine q35 \
      -drive if=pflash,format=raw,readonly=on,file=/usr/share/ovmf/OVMF.fd \
      -drive format=raw,file=fat:rw:diskimg \
      -net none

debugcon.nim is an interface for the qemu debug console. Once the bootloader finishes, there is no longer access to the uefi console, and it would be more complex to write a serial driver to start. Qemu offers an easy debug port which can be written to through memory:

const
  DebugConPort = 0xE9

proc portOut8(port: uint16, data: uint8) =
  asm """
    out %0, %1
    :
    :"Nd"(`port`), "a"(`data`)
  """

proc debug*(msgs: varargs[string]) =
  ## Send messages to the debug console.
  for msg in msgs:
    for ch in msg:
      portOut8(DebugConPort, ch.uint8)

proc debugln*(msgs: varargs[string]) =
  ## Send messages to the debug console. A newline is appended at the end.
  debug(msgs)
  debug("\r\n")

Note that nim initially complains about the assembly, saying that the there is an unkown mnemonic. I added --passc:"-masm=intel" to interpret assembly in the intel syntax, which fixed the issue.

I added some additional flags to src/kernel/nim.cfg. First turning off special registers with the -mgeneral-regs-only switch, and then disabling an automatic stack frame with -mno-red-zone. The red zone is a scratch space made by the compiler when a function doesn’t call anything. The CPU will reuse this space in kernel mode, causing two sets of overlapping data in the memory space.

The nim.cfg looks like this:

amd64.any.clang.linkerexe="ld.lld"
--passc:"-mgeneral-regs-only"
--passc:"-mno-red-zone"
--passc:"-target x86_64-unknown-elf"
--passc:"-masm=intel"
--passc:"-ffreestanding"
--passl:"-nostdlib"
--passl:"-Map=build/kernel.map"
--passl:"-entry KernelMain"

Running just kernel produces a binary file. The elf information can be shown with llvm-readelf --headers. This elf file isn’t quite right. The compiler needs some additional linking information, so that it loads in the right location.

I can add a linker file in src/kernel/kernel.ld

SECTIONS
{
  . = 0x100000
  .text : { *(.text) }
  .rodata : { *(.rodata*) }
  .data : { *(.data) }
  .bss : { *(.bss) }

  /DISCARD/ : { *(*) }
}

Adding this and the -passl:"-T src/kernel/kernel.ld" argument creates an elf file which has an entry point of 0x10B800. Which is closer, but the linker is putting the .text section before the code we want to start into. I have to find the kernel object and then throw it ahead of .text:

SECTIONS
{
  . = 0x100000;
  .text     : { *main*.o(.text) *(.text) }
  .rodata   : { *(.rodata*) }
  .data     : { *(.data) }
  .bss      : { *(.bss) }
  .shstrtab : { *(.shstrtab) }

  /DISCARD/ : { *(*) }
}

From what I can infer I want to match based on the name of my nim file (which is here main). This seems a little fraught, as it would be easy to have a hanging object file with a similar name, which would cause problems. But it’s also not quite easy to guess how the compiler creates the object file.

I can check that this maps to the start of binary file:

$ head -n 10 build/kernel.map
    VMA              LMA     Size Align Out     In      Symbol
        0                0   100000     1 . = 0x100000
   100000           100000     b3e2    16 .text
   100000           100000      270    16         /home/anne/Documents/os/m os/build/@mmain.nim.c.o:(.text)
   100000           100000       89     1                 KernelMain
   100090           100090       86     1                 nimFrame
   100120           100120       17     1                 nimErrorFlag
   100140           100140       a9     1                 quit__system_u6454
   1001f0           1001f0       17     1                 popFrame
   100210           100210        6     1                 NimDestroyGlobals

I got a lot of the format working for the binary, but was having trouble getting the linker to recognize the .bss section, or specifically to allocate the empty heap.

I tried a few options like moving .bss into the .data section, and adding a dummy section with a defined size after .bss. But nothing was working. Turns out I had -oformat=binary instead of --oformat-binary, in the nim.cfg.

{
  . = 0x100000;
  .text     : {
    *main*.o(.text.KernelMain)
    *main*.o(.text.*)
    *(.text.*)
  }
  .rodata   : { *(.rodata*) }
  .data     : { *(.data) *(.bss) }
  .shstrtab : { *(.shstrtab) }

  /DISCARD/ : { *(*) }
}

This starts data at 0x100000, finds the entry point, and then defines the sections. The binary output file is now the correct size:

$ wc -c build/kernel.bin
1099760 build/kernel.bin

Now that I have a binary format I can use, I can start putting some real kernel operations in.