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:
- 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.
- 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. - 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. - 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. - 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,
- We need to write a boot loader that loads our kernel into memory when we turn on our computer.
- We need to write an interrupt handler that handles external events (such as keyboard input) when they occur.
- We need to write a scheduler that switches between different processes (such as user programs) when they run on our CPU.
- We need to write a memory manager that allocates and frees memory for different processes when they request it.
- 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,
- We need to write a shell that reads input from the keyboard and executes commands or programs.
- We need to write a display server that communicates with our kernel’s video driver and draws pixels on the screen.
- We need to write a window manager that creates and manages windows and menus on top of our display server.
- We need to write a file manager that communicates with our kernel’s file system and displays files and directories in windows.
- 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!