Targeting UEFI (Part 2)

In the previous section, we were faced with the need to implement a number of ANSI C library functions. Let's implement them now.

C library functions

fwrite

The fwrite function writes count elements of data, each size bytes long, to the stream pointed to by stream, obtaining them from the location given by ptr. It returns the number of elements successfully written.

size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);

The FILE * struct pointer is already defined in Nim as File, so we can use that directly. const void *, however, has no equivalent type in Nim, so we'll define it by importing the C equivalent. Since we don't have something to write to yet, we'll just create an dummy implementation:

# src/libc.nim

type
  const_pointer {.importc: "const void *".} = pointer

proc fwrite*(ptr: const_pointer, size: csize_t, count: csize_t, stream: File):
    csize_t {.exportc.} =
  return count

fflush

The fflush function flushes the stream pointed to by stream. It returns 0 on success, or EOF on error.

int fflush(FILE *stream);

Again, we'll just create a dummy implementation for now:

# src/libc.nim

proc fflush*(stream: File): cint {.exportc.} =
  return 0.cint

stdout/stderr

The stdout and stderr global variables are pointers to a FILE struct that represents the standard output and error streams.

FILE *stdout;
FILE *stderr;

We can define it in Nim as a variable of type File, which will be initialized to nil by default:

# src/libc.nim

var
  stdout* {.exportc.}: File
  stderr* {.exportc.}: File

exit

The exit function causes normal process termination and the value of status is returned to the parent.

void exit(int status);

Since we don't have an OS yet, there is nothing to return to, so we'll just halt the CPU using inline assembly:

# src/libc.nim

proc exit*(status: cint) {.exportc, asmNoStackFrame.} =
  asm """
  .loop:
    cli
    hlt
    jmp .loop
  """

This clears the interrupt flag, then halts the CPU. The CPU can still be interrupted by NMI, SMI, or INIT interrupts, so that's why we have a loop to keep halting the CPU if this happens. The asmNoStackFrame pragma tells the compiler to not create a stack frame for this procedure, since it's pure assembly that we never return from.

Linking the C library

Now that we have implemented the missing library functions and exported them, we can link them into our executable. Let's import the libc module in main.nim:

# src/main.nim

import libc
...

Since we don't directly use the libc module functions in main.nim, we'll get a warning that the module is unused. We can tell the compiler that the library will be used by adding the {.used.} pragma at the top of the libc.nim module.

Here's the complete libc.nim module:

# src/libc.nim

{.used.}

type
  const_pointer {.importc: "const void *".} = pointer

proc fwrite*(buf: const_pointer, size: csize_t, count: csize_t, stream: File): csize_t {.exportc.} =
  return 0.csize_t

proc fflush*(stream: File): cint {.exportc.} =
  return 0.cint

var stderr* {.exportc.}: File

proc exit*(status: cint) {.exportc, asmNoStackFrame.} =
  asm """
  .loop:
    cli
    hlt
    jmp .loop
  """

Let's compile and link our code:

$ nim c --os:any src/main.nim --out:build/main.exe

$ file build/main.exe
build/main.exe: PE32+ executable (EFI application) x86-64, for MS Windows, 4 sections

Great! We were able to compile and link our Nim code into a PE32+ executable that targets UEFI with no OS support. Now we're in a good shape to start implementing our bootloader.

Last Updated: