System Library

So far, we have three system calls: print, yield, and exit. It's going to be tedious to write everything in assembly, so we need to write a system library that wraps these system calls in procs that we can call from our user tasks. This library will be the interface between our user tasks and the kernel.

Writing the Library

Let's create a new top-level directory called syslib, and inside it, we'll create two modules for our system calls: io.nim (for print), and os.nim (for yield and exit). Since system call numbers need to be unique, let's define them in a common module called syscalldef.nim.

# src/syslib/syscalldef.nim

const
  SysExit = 1
  SysPrint = 2
  SysYield = 3

Now let's add io.nim.

# src/syslib/io.nim
include syscalldef

proc print*(pstr: ptr string) =
  asm """
    mov rdi, %0
    mov rsi, %1
    syscall
    :
    : "i" (`SysPrint`), "m" (`pstr`)
    : "rdi", "rsi"
  """

This is our first system call wrapper, print. It takes a pointer to a string and passes it to the kernel system call SysPrint.

Next, let's add os.nim.

# src/syslib/os.nim
include syscalldef

proc yld*() =
  asm """
    mov rdi, %0
    syscall
    :
    : "i" (`SysYield`)
    : "rdi"
  """

proc exit*(code: int) =
  asm """
    mov rdi, %0
    mov rsi, %1
    syscall
    :
    : "i" (`SysExit`), "r" (`code`)
    : "rdi", "rsi"
  """

These are the wrappers for yield and exit. yield doesn't take any arguments, so it just passes the system call number to the kernel. exit takes an integer argument, which is the exit code.

Using the Library

It's time to put our brand new system library to use. Let's modify our user task to use all three system calls, instead of the direct system calls we had before.

# src/user/utask.nim
import common/[libc, malloc]
import syslib/[io, os]

proc NimMain() {.importc.}

let
  msg = "Hello from user mode!"
  pmsg = msg.addr

proc UserMain*() {.exportc.} =
  NimMain()

  print(pmsg)
  yld()
  exit(0)


 










 
 
 

This looks much cleaner and more readable than the assembly code we had before. We can now write our user tasks in Nim, and the system library will take care of the system calls for us.

Let's try it out.

...
kernel: Adding tasks to scheduler
kernel: Starting scheduler
sched: switching -> 0
Hello from user mode!
syscall: yield
sched: switching 0 -> 1
Hello from user mode!
syscall: yield
sched: switching 1 -> 2
Hello from user mode!
syscall: yield
sched: switching 2 -> 0
syscall: exit: code=0
sched: switching 0 -> 1
syscall: exit: code=0
sched: switching 1 -> 2
syscall: exit: code=0
sched: no tasks to run, halting

No surprises, everything works as expected. We have successfully abstracted the system calls into a library, making it easier to write user tasks without worry about the details of system calls. We can now add more system calls to the library as we need them.

Last Updated: