risbee

package module
v1.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 16, 2025 License: MIT Imports: 0 Imported by: 0

README

Risbee

Build CI License

Risbee is a small, self-contained virtual machine that draws inspiration from the RISC-V instruction set. Built in Go, it offers a simple and approachable way to experiment with low-level concepts like registers, memory management, and instruction decoding. With just few kilobytes of byte-addressable memory and 32 general-purpose registers, Risbee keeps its footprint minimal while still supporting a rich subset of operations—everything from basic loads and stores to arithmetic, branching, and even custom syscalls.

You don’t need to be a seasoned compiler engineer or hardware expert to get started. Risbee’s design emphasizes clarity: the VM initializes with a clear default state, lets you load a binary or raw byte slice into memory at a fixed offset, and then runs a straightforward fetch-decode-execute loop. If you want to print text, handle I/O, or integrate with your Go application in other ways, you simply register a Go function as a syscall handler, and Risbee will invoke it whenever your code calls an environment call instruction.

All you need is a Go workspace and a compiled RISC-V binary (or a simple byte array), and you’re ready to explore how a tiny VM brings machine instructions to life.

Supported Features

  • Fetch-Decode-Execute Loop: Continuously fetches 32-bit little-endian instructions, decodes them by opcode/function-codes, and executes until halted.
  • Instruction Support
    • Loads (LB, LH, LW, LD, and unsigned variants)
    • Stores (SB, SH, SW, and SD)
    • Immediate ALU (ADDI, SLTI, XORI, ORI, ANDI, and shifts)
    • Register-Register ALU (32/64-bit adds, subs, shifts, multiplies, divides, remainders)
    • Control Flow (BEQ, BNE, BLT, BGE, BLTU, BGEU, JAL, JALR)
    • Fences (no-op placeholder)
    • Syscalls (via CALL/ECALL)
  • Syscall API
    • Register handlers with SetSystemCall(code, fn).
    • Retrieve string and pointer parameters with GetStringPointer and GetPointerParam.
    • Built-in exit syscall (code 0 uses R10 for status).
  • Memory & Registers
    • Dynamic kilobytes contiguous memory ([]byte)
    • 32 × 64-bit registers (R0 read-only zero)
    • Program Counter initialized to 0x1000
    • Stack Pointer (R2) auto-set to top of memory on load
  • Error Handling: Invalid instructions or syscalls trigger panic(), printing an error, setting exit code to -1, and halting.

Installation

To incorporate Risbee into your Go project, use go get:

# Install via go get
go get github.com/nthnn/risbee

Quick Start

This program accepts a RISC-V binary filename as a command-line argument, sets up a simple print syscall (code 1), loads the binary into the VM’s memory, and runs it. It prints usage instructions and exits with code 1 if no filename is provided, or an error message if loading fails.

import (
	"fmt"
	"os"

	"github.com/nthnn/risbee"
)

func main() {
	// Ensure a filename is provided as an argument.
	if len(os.Args) < 2 {
		fmt.Println("Usage: risbee <filename>")
		os.Exit(1)
	}

	// Create and initialize the VM.
	vm := &risbee.RisbeeVm{}
	vm.Initialize()

	// Register a simple "print" syscall at code 1.
	// When invoked, it reads a string pointer from a0,
	// prints the string, and returns.
	vm.SetSystemCall(1, func(vm *risbee.RisbeeVm) uint64 {
		ptr := vm.GetPointerParam(0)

		fmt.Print(vm.GetStringPointer(ptr))
		return ptr
	})

	// Load the RISC-V binary into VM memory; exit on failure.
	if !vm.LoadFile(os.Args[1]) {
		fmt.Println("Failed to load file:", os.Args[1])
		os.Exit(1)
	}

	// Execute the loaded program.
	vm.Run()
}

Key Methods

  • Initialize(): Reset PC, exit code, running flag, and syscall table.
  • LoadFromBytes(data []byte) bool: Load raw bytes at 0x1000 into VM memory.
  • SetSystemCall(code uint64, fn RisbeeVmSyscallFn): Register a syscall handler.
  • GetPointerParam(idx uint64) uint64: Read syscall argument from a0+idx.
  • GetStringPointer(ptr uint64) string: Read null-terminated string from VM memory.
  • Run(): Enter the fetch-decode-execute loop.
  • Stop(): Halt execution.
  • GetExitCode() int: Retrieve VM exit status.

Memory Layout

The VM reserves the first 4 KiB (0x0000–0x0FFF) as a “reserved” region that you can use for static data, heap, or simply leave untouched. At address 0x1000, the VM begins loading your program image, and everything from 0x1000 up to the end of the space is available for code, global variables, stack, and heap allocations. By convention, the stack pointer (register R2) is initialized to the very top of memory (0x10000), allowing your program to grow the stack downward into the unused upper region.

0x0000 ─────────────────────────── Reserved (data/heap)
   │
0x0FFF ────────────────────────────
0x1000 ─────────────────────────── Program image load address
   │   • Text segment (.text)
   │   • Read-only data (.rodata)
   │   • Initialized data (.data)
   │   • Uninitialized data (.bss)
   │   • Heap (grows upward)
   │   • Stack (grows downward, SP=R2 starts at 0x10000)
  ...  ─────────────────────────── End of VM memory
  • Load Offset (0x1000): All binaries and raw byte slices are copied here.
  • Stack Pointer (R2): Set to 0x10000 on load, so your stack grows downward into fresh memory.
  • Heap: Begins immediately after any static data; you can manage it entirely in software.

This fixed, contiguous layout keeps things simple and predictable, letting you focus on instruction semantics rather than complex memory mapping.

Custom SDKs

If you’re building applications that rely on your own syscall conventions, writing a small “SDK” (set of helper functions or libraries) can make life much easier. A custom SDK provides idiomatic wrappers around raw environment calls, handles parameter marshalling, and can expose higher-level APIs to your Go or C programs. Below is a guide to developing SDKs for Risbee syscalls.

On the example usage above, the system call with 0x01 address which is for printing string, is being declared as such below on the source file of binary (see next section).

static inline long prints(const char* str) {
    register long a0 asm("a0") = (long) str;
    register long scid asm("a7") = 1;

    asm volatile ("scall" : "+r"(a0) : "r"(scid));
    return a0;
}

Hence, it can be used as shown below:

int main() {
    prints("Hello, world!\r\n");
    return 0;
}

Building Program Binaries

To turn your C (or C++) and assembly sources into a raw RISC-V binary suitable for the Risbee VM, follow these steps:

  1. Compile and Link: Use the RISC-V GCC toolchain targeting RV64IM, no standard library, and your custom linker script.

    riscv64-unknown-elf-g++     \
        -march=rv64im           \
        -mabi=lp64              \
        -nostdlib               \
        -Wl,-T,scripts/link.ld  \
        -O2 -o main.out         \
        scripts/launcher.s      \
        main.c
    
    • -march=rv64im selects 64-bit integer RISC-V with multiplication.
    • -mabi=lp64 chooses the 64-bit ABI.
    • -nostdlib prevents linking against the host’s C runtime.
    • -T link.ld tells the linker to use your memory layout.
  2. Extract the Raw Image: Convert the ELF output into a flat binary blob that Risbee can load.

    riscv64-unknown-elf-objcopy -O binary main.out main.bin
    

    This strips headers and relocations, leaving just the machine code and data laid out at the offsets defined by link.ld.

License

Copyright 2025 Nathanne Isip

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Documentation

Index

Constants

View Source
const (
	// RISBEE_OPINST_LOAD is the opcode for load instructions (e.g., LB, LW).
	RISBEE_OPINST_LOAD = 3
	// RISBEE_OPINST_STORE is the opcode for store instructions (e.g., SB, SW).
	RISBEE_OPINST_STORE = 35
	// RISBEE_OPINST_IMM is the opcode for immediate ALU operations (I-type).
	RISBEE_OPINST_IMM = 19
	// RISBEE_OPINST_IALU is the opcode for 64-bit immediate ALU operations.
	RISBEE_OPINST_IALU = 27
	// RISBEE_OPINST_RT64 is the opcode for 64-bit register–register operations.
	RISBEE_OPINST_RT64 = 51
	// RISBEE_OPINST_RT32 is the opcode for 32-bit register–register word ops.
	RISBEE_OPINST_RT32 = 59
	// RISBEE_OPINST_LUI is the opcode for Load Upper Immediate (U-type).
	RISBEE_OPINST_LUI = 55
	// RISBEE_OPINST_AUIPC is the opcode for Add Upper Immediate to PC (U-type).
	RISBEE_OPINST_AUIPC = 23
	// RISBEE_OPINST_JAL is the opcode for Jump and Link (J-type).
	RISBEE_OPINST_JAL = 111
	// RISBEE_OPINST_JALR is the opcode for Jump and Link Register (I-type).
	RISBEE_OPINST_JALR = 103
	// RISBEE_OPINST_BRANCH is the opcode for conditional branches (B-type).
	RISBEE_OPINST_BRANCH = 99
	// RISBEE_OPINST_FENCE is the opcode for memory ordering fences.
	RISBEE_OPINST_FENCE = 15
	// RISBEE_OPINST_CALL is the opcode for environment calls / syscalls.
	RISBEE_OPINST_CALL = 115
)

Instruction decoding constants: Primary opcodes defining the major RISC-V instruction formats.

View Source
const (
	RISBEE_FC3_LB   = 0 // Load Byte (signed)
	RISBEE_FC3_LHW  = 1 // Load Halfword (signed 16-bit)
	RISBEE_FC3_LW   = 2 // Load Word (signed 32-bit)
	RISBEE_FC3_LDW  = 3 // Load Doubleword (64-bit)
	RISBEE_FC3_LBU  = 4 // Load Byte Unsigned
	RISBEE_FC3_LHU  = 5 // Load Halfword Unsigned
	RISBEE_FC3_LRES = 6 // Reserved or custom load
)

Function3 codes for load instruction variants (determines width and sign).

View Source
const (
	RISBEE_FC3_SB  = 0 // Store Byte
	RISBEE_FC3_SHW = 1 // Store Halfword
	RISBEE_FC3_SW  = 2 // Store Word
	RISBEE_FC3_SDW = 3 // Store Doubleword
)

Function3 codes for store instruction variants (determines width).

View Source
const (
	RISBEE_FC3_ADDI  = 0 // Add Immediate
	RISBEE_FC3_SLLI  = 1 // Shift Left Logical Immediate
	RISBEE_FC3_SLTI  = 2 // Set Less Than Immediate (signed)
	RISBEE_FC3_SLTIU = 3 // Set Less Than Immediate (unsigned)
	RISBEE_FC3_XORI  = 4 // XOR Immediate
	RISBEE_FC3_SRLI  = 5 // Shift Right Logical Immediate or Arithmetic (distinguished by funct7)
	RISBEE_FC3_ORI   = 6 // OR Immediate
	RISBEE_FC3_ANDI  = 7 // AND Immediate
)

Function3 codes for I-type arithmetic instructions.

View Source
const (
	RISBEE_FC3_SLLIW  = 0 // Shift Left Logical Immediate for 32-bit
	RISBEE_FC3_SRLIW  = 1 // Shift Right Logical Immediate for 32-bit
	RISBEE_FC3_SRAIW  = 5 // Shift Right Arithmetic Immediate for 32-bit
	RISBEE_FC3_SLLI64 = 6 // Shift Left Logical Immediate for 64-bit
	RISBEE_FC3_SRLI64 = 7 // Shift Right Logical Immediate for 64-bit
	RISBEE_FC3_SRAI64 = 8 // Shift Right Arithmetic Immediate for 64-bit
)

Function3 codes for 64-bit and word-width IALU operations.

View Source
const (
	RISBEE_FC3_BEQ  = 0 // Branch if Equal
	RISBEE_FC3_BNE  = 1 // Branch if Not Equal
	RISBEE_FC3_BLT  = 4 // Branch if Less Than (signed)
	RISBEE_FC3_BGE  = 5 // Branch if Greater or Equal (signed)
	RISBEE_FC3_BLTU = 6 // Branch if Less Than (unsigned)
	RISBEE_FC3_BGEU = 7 // Branch if Greater or Equal (unsigned)
)

Function3 codes for conditional branch types.

View Source
const (
	RISBEE_OPINST_RT32_ADDW  = 0x0   // ADDW: add word
	RISBEE_OPINST_RT32_SUBW  = 0x100 // SUBW: subtract word
	RISBEE_OPINST_RT32_SLLW  = 0x1   // SLLW: shift left logical word
	RISBEE_OPINST_RT32_SRLW  = 0x5   // SRLW: shift right logical word
	RISBEE_OPINST_RT32_SRAW  = 0x105 // SRAW: shift right arithmetic word
	RISBEE_OPINST_RT32_MULW  = 0x8   // MULW: multiply word
	RISBEE_OPINST_RT32_DIVW  = 0xC   // DIVW: divide word (signed)
	RISBEE_OPINST_RT32_DIVUW = 0xD   // DIVUW: divide word (unsigned)
	RISBEE_OPINST_RT32_REMW  = 0xE   // REMW: remainder word (signed)
	RISBEE_OPINST_RT32_REMUW = 0xF   // REMUW: remainder word (unsigned)
)

Combined funct7 and funct3 codes for RT32 (32-bit register) operations.

View Source
const (
	RISBEE_OPINST_RT64_ADD    = 0x0   // ADD: add
	RISBEE_OPINST_RT64_SUB    = 0x100 // SUB: subtract
	RISBEE_OPINST_RT64_SLL    = 0x1   // SLL: shift left logical
	RISBEE_OPINST_RT64_SLT    = 0x2   // SLT: set less than (signed)
	RISBEE_OPINST_RT64_SLTU   = 0x3   // SLTU: set less than (unsigned)
	RISBEE_OPINST_RT64_XOR    = 0x4   // XOR: exclusive OR
	RISBEE_OPINST_RT64_SRL    = 0x5   // SRL: shift right logical
	RISBEE_OPINST_RT64_SRA    = 0x105 // SRA: shift right arithmetic
	RISBEE_OPINST_RT64_OR     = 0x6   // OR: bitwise OR
	RISBEE_OPINST_RT64_AND    = 0x7   // AND: bitwise AND
	RISBEE_OPINST_RT64_MUL    = 0x8   // MUL: multiply
	RISBEE_OPINST_RT64_MULH   = 0x9   // MULH: multiply high signed
	RISBEE_OPINST_RT64_MULHSU = 0xA   // MULHSU: multiply high signed×unsigned
	RISBEE_OPINST_RT64_MULHU  = 0xB   // MULHU: multiply high unsigned
	RISBEE_OPINST_RT64_DIV    = 0xC   // DIV: divide (signed)
	RISBEE_OPINST_RT64_DIVU   = 0xD   // DIVU: divide (unsigned)
	RISBEE_OPINST_RT64_REM    = 0xE   // REM: remainder (signed)
	RISBEE_OPINST_RT64_REMU   = 0xF   // REMU: remainder (unsigned)
)

Combined funct7 and funct3 codes for RT64 (64-bit register) operations.

Variables

This section is empty.

Functions

This section is empty.

Types

type RisbeeVm

type RisbeeVm struct {
	Memory        []byte                       // Byte-addressable VM memory
	Registers     [32]uint64                   // General-purpose registers R0–R31
	Pc            uint64                       // Program counter
	ExitCode      int                          // Exit code of the VM
	Running       bool                         // VM running status
	SysCalls      map[uint64]RisbeeVmSyscallFn // Registered syscalls
	ExitCallback  func(uint64)                 // Exit system call callback function
	PanicCallback func(string)                 // Panic callback function
}

RisbeeVm encapsulates the state of the RISC-V inspired virtual machine. It includes memory, registers, program counter, exit code, running status, and a map of registered syscall handlers.

func (*RisbeeVm) GetExitCode

func (vm *RisbeeVm) GetExitCode() int

Gets the exit code of the virtual machine.

Returns the exit code.

func (*RisbeeVm) GetPointerParam

func (vm *RisbeeVm) GetPointerParam(Index uint64) uint64

GetPointerParam retrieves the syscall parameter from register a0+aIndex.

func (*RisbeeVm) GetStringPointer

func (vm *RisbeeVm) GetStringPointer(Pointer uint64) string

GetStringPointer reads a null-terminated string from VM memory at the given pointer.

Returns "(null)" if the pointer is zero, or the extracted string otherwise.

func (*RisbeeVm) GetSystemCall

func (vm *RisbeeVm) GetSystemCall(
	Address uint64,
) (RisbeeVmSyscallFn, bool)

GetSystemCall retrieves a registered syscall handler by address (code).

Returns the handler and a boolean indicating existence.

func (*RisbeeVm) Initialize

func (vm *RisbeeVm) Initialize(
	exitCallback func(uint64),
	panicCallback func(string),
)

This function initializes the Risbee virtual machine instance. It sets up the initial state of the virtual machine.

Parameters:

  • exitCallback Callback triggered when system call for exit is invoked
  • panicCallback Callback for encountered panic errors

func (*RisbeeVm) IsRunning

func (vm *RisbeeVm) IsRunning() bool

This method returns a boolean value indicating whether the virtual machine is currently running or not.

func (*RisbeeVm) LoadFromBytes

func (vm *RisbeeVm) LoadFromBytes(Data []byte) bool

It copies the contents of `data` into the VM’s internal memory starting at the fixed load offset (4096 bytes), mirroring the behavior of LoadFile but without disk I/O.

func (*RisbeeVm) Run

func (vm *RisbeeVm) Run()

This function starts the execution of the Risbee virtual machine instance and executes the loaded program, if any, and handles any system calls or instructions encountered during execution until the program exits or an error occurs.

func (*RisbeeVm) SetSystemCall

func (vm *RisbeeVm) SetSystemCall(
	Address uint64,
	Callback RisbeeVmSyscallFn,
)

SetSystemCall registers a syscall handler function at the given address (code).

func (*RisbeeVm) Stop

func (vm *RisbeeVm) Stop()

Stops the execution of the virtual machine. This method halts the execution of the virtual machine.

type RisbeeVmSyscallFn

type RisbeeVmSyscallFn func(vm *RisbeeVm) uint64

RisbeeVmSyscallFn represents the signature of a syscall handler function.

Directories

Path Synopsis
Example main.go demonstrating VM initialization, syscall registration, file loading, and execution.
Example main.go demonstrating VM initialization, syscall registration, file loading, and execution.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL