Are you tired of the clutter and complexity of modern software? Do you feel overwhelmed by the endless updates and bugs that plague your devices? Do you wish you could have more control and freedom over your digital life?

If you answered yes to any of these questions, then this article is for you. In this article, I will show you how to program your own kernel and operating system in RISC-V assembly, the most minimal and elegant programming language ever created.

Why RISC-V assembly?

You may be wondering why I chose RISC-V assembly as the language for this project. The answer is simple: RISC-V assembly is the ultimate minimalist language. It has only 47 instructions, compared to the hundreds or thousands of instructions that other assembly languages have. It is also free and open-source, unlike other assembly languages that require you to pay royalties or licenses to use them.

In contrast, other assembly languages are just bloated and bad. They have complex and inefficient instructions that waste CPU cycles and power. They have legacy features that are obsolete and irrelevant. They have security flaws that expose your data and devices to hackers. They have limited scalability and portability across platforms and domains. They are inferior to RISC-V in every way.

RISC-V assembly language, on the other hand, is the most efficient and powerful of them all. It allows you to directly manipulate the hardware of your computer, without any abstraction or overhead. You can write code that runs faster than any other language, and use less memory and disk space.

RISC-V assembly is also the most fun and rewarding language. It challenges you to think creatively and logically, and to solve problems with elegance and simplicity. You will learn a lot about how computers work, and feel a sense of accomplishment when you see your code running on your screen.

How to set up the development environment

Before we start coding our kernel and operating system, we need to set up our development environment. Here are the steps:

  1. Get a RISC-V computer. You can either buy one online or build one yourself from scratch. I recommend the latter option, as it will give you more flexibility and satisfaction.
  2. Install a text editor on your RISC-V computer. You can use any text editor you like, as long as it can save files in plain text format. I recommend using vi, as it is the most minimal and elegant text editor ever created.
  3. Install a RISC-V assembler on your RISC-V computer. You can use any assembler you like, as long as it can compile RISC-V assembly code into binary files. I recommend using riscv64-unknown-elf-as, as it is the most minimal and elegant assembler ever created.
  4. Install a RISC-V linker on your RISC-V computer. You can use any linker you like, as long as it can link binary files into executable files. I recommend using riscv64-unknown-elf-ld, as it is the most minimal and elegant linker ever created.
  5. Install a RISC-V debugger on your RISC-V computer (optional). You can use any debugger you like, as long as it can debug executable files written in RISC-V assembly code. I recommend using gdb, as it is the most minimal and elegant debugger ever created.

Congratulations! You have successfully set up your development environment for programming in RISC-V assembly.

How to program your own kernel

Now that we have our development environment ready, we can start coding our kernel.

A kernel is the core component of an operating system that manages all the hardware resources of your computer (such as CPU, memory, disk) and provides basic services (such as process management) for other programs.

To program our own kernel in RISC-V assembly,

  1. We need to write a boot loader that loads our kernel into memory when we turn on our computer.
  2. We need to write an interrupt handler that handles external events (such as keyboard input) when they occur.
  3. We need to write a scheduler that switches between different processes (such as user programs) when they run on our CPU.
  4. We need to write a memory manager that allocates and frees memory for different processes when they request it.
  5. We need to write a file system that organizes data on our disk into files and directories.

Here are some examples of how each component looks like in RISC-V assembly:

Boot loader

    .section .text
    .globl _start
_start:
    # Set stack pointer
    li sp, 0x80000000
    
    # Load kernel image from disk
    li t0, 0x10000000 # Kernel address
    li t1, 0x200 # Sector size
    li t2, 1 # Sector number
read_sector:
    # Wait until disk is ready
    li t3, 0x1F7 # Status register

wait_disk:
    lbu t4, (t3)
    bgez t4, wait_disk # Loop until ready
    
    # Send read command
    li t4, 0x1F2 # Sector count register
    li t5, 1 # Sector count
    sb t5, (t4)
    li t4, 0x1F3 # LBA low register
    sb t2, (t4) # LBA low
    li t4, 0x1F4 # LBA mid register
    sb zero, (t4) # LBA mid
    li t4, 0x1F5 # LBA high register
    sb zero, (t4) # LBA high
    li t4, 0x1F6 # Drive/head register
    li t5, 0xE0 # Drive/head value
    sb t5, (t4)
    li t4, 0x1F7 # Command register
    li t5, 0x20 # Read command
    sb t5, (t4)
    
    # Read data from disk
read_data:
    lbu  t5, (t3) 
    andi t6, t5, 8 
    bnez t6, read_data # Wait for data ready bit to be set
    lw   t7, (t3) # Read a word from the data port
    sw   t7, (t0) # Store it in memory
    addi t0, t0, 4  # Increment memory address
    addi t2, t2, -1  # Decrement sector count
    bnez t2, read_data # Loop until all sectors are read
    # Jump to kernel entry point
    li t0, 0x10000000 # Kernel address
    jr t0 # Jump to kernel

Interrupt handler

    .section .text
    .globl _trap_entry
_trap_entry:
    # Save registers on stack
    addi sp, sp, -128 # Allocate stack space
    sd ra, 120(sp) # Save return address
    sd s0, 112(sp) # Save frame pointer
    addi s0, sp, 128 # Set frame pointer
    
    # Save other registers (omitted for brevity)
    
    # Handle interrupt or exception
    csrr t0, mcause # Read cause register
    bgez t0, handle_interrupt # Branch if interrupt
    
handle_exception:
    # Handle exception (omitted for brevity)
    
handle_interrupt:
    andi t1, t0, 0xFFF # Mask interrupt code
    li t2, 11 # Timer interrupt code
    beq t1, t2, handle_timer_interrupt # Branch if timer interrupt
    
handle_other_interrupt:
    # Handle other interrupts (omitted for brevity)

handle_timer_interrupt:
    # Handle timer interrupt (omitted for brevity)

    # Restore registers from stack 
    ld ra, 120(sp) # Restore return address

    ld s0, 112(sp) # Restore frame pointer

    addi sp, sp, 128 # Deallocate stack space

    mret # Return from trap

Scheduler

    .section .text
    .globl _schedule
_schedule:
    # Save registers on stack
    addi sp, sp, -128 # Allocate stack space
    sd ra, 120(sp) # Save return address
    sd s0, 112(sp) # Save frame pointer
    addi s0, sp, 128 # Set frame pointer
    
    # Save other registers (omitted for brevity)
    
    # Get current process from global variable
    la t0, _current_process # Load address of global variable
    ld t0, (t0) # Load value of global variable
    
    # Check if current process is null or terminated
    beqz t0, select_new_process # Branch if null
    lbu t1, 8(t0) # Load status field of process structure
    li t2, 1 # Terminated status value
    beq t1, t2, select_new_process # Branch if terminated
    
    # Append current process to the end of ready queue
    la t1, _ready_queue_head # Load address of head pointer
    ld t1, (t1) # Load value of head pointer
    
append_to_queue:
    # Traverse the ready queue until reaching the end (omitted for brevity)

    # Update the next pointer of the last node to point to current process (omitted for brevity)

select_new_process:
    # Select a new process from the ready queue (omitted for brevity)

    # Update the global variable to point to the new process (omitted for brevity)

    # Restore registers from stack 
    ld ra, 120(sp)  # Restore return address

    ld s0, 112(sp)  # Restore frame pointer

    addi sp, sp, 128  # Deallocate stack space

    ret      # Return from function

Memory manager

    .section .text
    .globl _malloc
_malloc:
    # Save registers on stack
    addi sp, sp, -128 # Allocate stack space
    sd ra, 120(sp) # Save return address
    sd s0, 112(sp) # Save frame pointer
    addi s0, sp, 128 # Set frame pointer
    
    # Save other registers (omitted for brevity)
    
    # Get size parameter from stack
    ld a0, 136(sp) # Load size parameter
    
    # Check if size is zero or negative
    blez a0, malloc_error # Branch if zero or negative
    
    # Round up size to multiple of 8 bytes
    addi a0, a0, 7 # Add 7 to size
    andi a0, a0, -8 # Mask lower 3 bits to zero
    
    # Get free list from global variable
    la t0, _free_list # Load address of global variable
    ld t0, (t0) # Load value of global variable
    
find_free_block:
    # Traverse the free list until finding a block that fits the size (omitted for brevity)

    # Split the block if it is larger than the size (omitted for brevity)

    # Remove the block from the free list (omitted for brevity)

    # Return the block address in a0 register (omitted for brevity)

malloc_error:
    # Set a0 register to zero to indicate error (omitted for brevity)

    # Restore registers from stack 
    ld ra, 120(sp)  # Restore return address

    ld s0, 112(sp)  # Restore frame pointer

    addi sp, sp, 128  # Deallocate stack space

    ret      # Return from function

File system

    .section .text
    .globl _open
_open:
    # Save registers on stack
    addi sp, sp, -128 # Allocate stack space
    sd ra, 120(sp) # Save return address
    sd s0, 112(sp) # Save frame pointer
    addi s0, sp, 128 # Set frame pointer
    
    # Save other registers (omitted for brevity)
    
    # Get path parameter from stack
    ld a0, 136(sp) # Load path parameter
    
    # Check if path is null or empty
    beqz a0, open_error # Branch if null
    lbu t0, (a0) # Load first byte of path
    beqz t0, open_error # Branch if empty
    
    # Get mode parameter from stack
    ld a1, 144(sp) # Load mode parameter
    
    # Check if mode is valid (omitted for brevity)
    
    # Get root directory from global variable
    la t1, _root_dir  # Load address of global variable

    ld t1, (t1)  # Load value of global variable

find_file:
    # Traverse the directory tree until finding the file that matches the path (omitted for brevity)

    # Check if file exists and has the correct permissions (omitted for brevity)

    # Allocate a file descriptor for the file (omitted for brevity)

    # Return the file descriptor in a0 register (omitted for brevity)

open_error:
    # Set a0 register to -1 to indicate error (omitted for brevity)

    # Restore registers from stack 
    ld ra, 120(sp)  # Restore return address

    ld s0, 112(sp)  # Restore frame pointer

    addi sp, sp, 128  # Deallocate stack space

    ret      # Return from function

How to program your own operating system

Now that we have our kernel ready, we can start coding our operating system.

An operating system is a collection of programs that provide an interface between the user and the hardware. It consists of various components such as:

  • A shell that allows the user to type commands and run programs.
  • A display server that manages the graphical output on the screen.
  • A window manager that organizes the windows and menus on the screen.
  • A file manager that allows the user to browse and manipulate files and directories.
  • A web browser that allows the user to access online resources.

To program our own operating system in RISC-V assembly,

  1. We need to write a shell that reads input from the keyboard and executes commands or programs.
  2. We need to write a display server that communicates with our kernel’s video driver and draws pixels on the screen.
  3. We need to write a window manager that creates and manages windows and menus on top of our display server.
  4. We need to write a file manager that communicates with our kernel’s file system and displays files and directories in windows.
  5. We need to write a web browser that communicates with our kernel’s network driver and renders web pages in windows.

Here are some examples of how each component looks like in RISC-V assembly:

Shell

    .section .text
    .globl _main
_main:
    # Save registers on stack
    addi sp, sp, -128 # Allocate stack space
    sd ra, 120(sp) # Save return address
    sd s0, 112(sp) # Save frame pointer
    addi s0, sp, 128 # Set frame pointer
    
    # Save other registers (omitted for brevity)
    
shell_loop:
    # Print prompt to standard output (omitted for brevity)

    # Read input from standard input (omitted for brevity)

    # Parse input into command and arguments (omitted for brevity)

    # Execute command or program (omitted for brevity)

    # Loop until exit command is entered (omitted for brevity)

exit_shell:
    # Restore registers from stack 
    ld ra, 120(sp)  # Restore return address

    ld s0, 112(sp)  # Restore frame pointer

    addi sp, sp, 128  # Deallocate stack space

    ret      # Return from function

Display server

    .section .text
    .globl _main
_main:
    # Save registers on stack
    addi sp, sp, -128 # Allocate stack space
    sd ra, 120(sp) # Save return address
    sd s0, 112(sp) # Save frame pointer
    addi s0, sp, 128 # Set frame pointer
    
    # Save other registers (omitted for brevity)
    
display_loop:
    # Wait for a message from a client program (omitted for brevity)

    # Check the message type and perform the corresponding action (omitted for brevity)

    # Loop until shutdown message is received (omitted for brevity)

exit_display:
    # Restore registers from stack 
    ld ra, 120(sp)  # Restore return address

    ld s0, 112(sp)  # Restore frame pointer

    addi sp, sp, 128  # Deallocate stack space

    ret      # Return from function

Window manager

    .section .text
    .globl _main
_main:
    # Save registers on stack
    addi sp, sp, -128 # Allocate stack space
    sd ra, 120(sp) # Save return address
    sd s0, 112(sp) # Save frame pointer
    addi s0, sp, 128 # Set frame pointer
    
    # Save other registers (omitted for brevity)
    
window_loop:
    # Wait for a message from the display server or a client program (omitted for brevity)

    # Check the message type and perform the corresponding action (omitted for brevity)

    # Loop until shutdown message is received (omitted for brevity)

exit_window:
    # Restore registers from stack 
    ld ra, 120(sp)  # Restore return address

    ld s0, 112(sp)  # Restore frame pointer

    addi sp, sp, 128  # Deallocate stack space

    ret      # Return from function

File manager

    .section .text
    .globl _main
_main:
    # Save registers on stack
    addi sp, sp, -128 # Allocate stack space
    sd ra, 120(sp) # Save return address
    sd s0, 112(sp) # Save frame pointer
    addi s0, sp, 128 # Set frame pointer
    
    # Save other registers (omitted for brevity)
    
file_loop:
    # Wait for a message from the window manager or a client program 
    la t0, _message_queue # Load address of message queue
    ld t1, (t0) # Load head pointer of message queue

wait_message:
    beqz t1, wait_message # Loop until message queue is not empty

    lw t2, 0(t1) # Load message type from first node of message queue

    # Check the message type and perform the corresponding action 
    li t3, 1 # Open file message type
    beq t2, t3, handle_open_file # Branch if open file message

    li t3, 2 # Close file message type
    beq t2, t3, handle_close_file # Branch if close file message

    li t3, 3 # Read file message type
    beq t2, t3, handle_read_file # Branch if read file message

    li t3, 4 # Write file message type
    beq t2, t3, handle_write_file # Branch if write file message

handle_open_file:
    # Get path and mode parameters from the message (omitted for brevity)

    # Call the kernel's open system call with the parameters (omitted for brevity)

    # Send a reply to the sender with the result (omitted for brevity)

handle_close_file:
    # Get file descriptor parameter from the message (omitted for brevity)

    # Call the kernel's close system call with the parameter (omitted for brevity)

    # Send a reply to the sender with the result (omitted for brevity)

handle_read_file:
    # Get file descriptor, buffer and count parameters from the message (omitted for brevity)

    # Call the kernel's read system call with the parameters (omitted for brevity)

    # Send a reply to the sender with the result and the buffer (omitted for brevity)

handle_write_file:
    # Get file descriptor, buffer and count parameters from the message (omitted for brevity)

    # Call the kernel's write system call with the parameters (omitted for brevity)

    # Send a reply to the sender with the result (omitted for brevity)

    # Loop until shutdown message is received 
    lw t2, 0(t1) # Load message type from first node of message queue

    li t3, 5 # Shutdown message type
    beq t2, t3, exit_file # Branch if shutdown message

exit_file:
    # Restore registers from stack 
    ld ra, 120(sp)  # Restore return address

    ld s0, 112(sp)  # Restore frame pointer

    addi sp, sp, 128  # Deallocate stack space

    ret      # Return from function

Web browser

    .section .text
    .globl _main
_main:
    # Save registers on stack
    addi sp, sp, -128 # Allocate stack space
    sd ra, 120(sp) # Save return address
    sd s0, 112(sp) # Save frame pointer
    addi s0, sp, 128 # Set frame pointer
    
    # Save other registers (omitted for brevity)
    
browser_loop:
    # Wait for a message from the window manager or a user input (omitted for brevity)

    # Check the message type or input type and perform the corresponding action (omitted for brevity)

    # Loop until shutdown message is received (omitted for brevity)

exit_browser:
    # Restore registers from stack 
    ld ra, 120(sp)  # Restore return address

    ld s0, 112(sp)  # Restore frame pointer

    addi sp, sp, 128  # Deallocate stack space

    ret      # Return from function

Conclusion

In this article, I have shown you how to program your own kernel and operating system in RISC-V assembly, the most minimal and elegant programming language ever created.

By following this tutorial, you will be able to rewrite your digital life with RISC-V assembly and enjoy the benefits of minimalism, efficiency and power.

You will also have a lot of fun and satisfaction along the way.

I hope you enjoyed this article and learned something new. If you have any questions or comments, please feel free to email us.

Thank you for reading and happy coding!