#include #include #include #include #include #include #include #include #include #include #include "color.h" #include "io.h" #include "jit.h" #include "riscv.h" #include "riscv/debug.h" #include "types.h" #ifndef PATH_MAX #define PATH_MAX 4096 #endif /* Define BINARY for `bail` and `assert` functions. */ #undef BINARY #define BINARY "emulator" #undef assert #define assert(condition) \ ((condition) ? (void)0 : _assert_failed(#condition, __FILE__, __LINE__)) static inline __attribute__((noreturn)) void _assert_failed( const char *condition, const char *file, int line ) { fprintf(stderr, "%s:%d: assertion `%s` failed\n", file, line, condition); abort(); } /* Maximum physical memory size (384MB). The runtime can choose any size up to * this value with `-memory-size=...`. */ #define MEMORY_SIZE (384 * 1024 * 1024) /* Default physical memory size (128MB). */ #define DEFAULT_MEMORY_SIZE (128 * 1024 * 1024) /* Program memory size (4MB), reserved at start of memory for program code. */ #define PROGRAM_SIZE (4 * 1024 * 1024) /* Writable data region base. */ #define DATA_RW_OFFSET 0xFFFFF0 /* Data memory starts at writable data region. */ #define DATA_MEMORY_START DATA_RW_OFFSET /* Default data memory size. */ #define DEFAULT_DATA_MEMORY_SIZE (DEFAULT_MEMORY_SIZE - DATA_MEMORY_START) /* Default stack size (256KB), allocated at the end of memory. */ #define DEFAULT_STACK_SIZE (256 * 1024) /* Maximum instructions to show in the TUI. */ #define MAX_INSTR_DISPLAY 40 /* Stack words to display in the TUI. */ #define STACK_DISPLAY_WORDS 32 /* Maximum number of CPU state snapshots to store for undo. */ #define MAX_SNAPSHOTS 64 /* Maximum open files in guest runtime. */ #define MAX_OPEN_FILES 32 /* Number of history entries to print when reporting faults. */ #define FAULT_TRACE_DEPTH 8 /* Instruction trace depth for headless tracing. */ #define TRACE_HISTORY 64 /* Maximum number of steps executed in headless mode before timing out. */ #define HEADLESS_MAX_STEPS ((u64)1000000000000) /* Height of header and footer in rows. */ #define HEADER_HEIGHT 2 #define FOOTER_HEIGHT 3 /* Read-only data offset. */ #define DATA_RO_OFFSET 0x10000 /* TTY escape codes. */ #define TTY_CLEAR "\033[2J\033[H" #define TTY_GOTO_RC "\033[%d;%dH" /* Exit code returned on EBREAK. */ #define EBREAK_EXIT_CODE 133 /* Registers displayed in the TUI, in order. */ static const reg_t registers_displayed[] = { SP, FP, RA, A0, A1, A2, A3, A4, A5, A6, A7, T0, T1, T2, T3, T4, T5, T6 }; /* Display mode for immediates and values. */ enum display { DISPLAY_HEX, DISPLAY_DEC }; /* Debug info entry mapping PC to source location. */ struct debug_entry { u32 pc; u32 offset; char file[PATH_MAX]; }; /* Debug info table. */ struct debug_info { struct debug_entry *entries; size_t count; size_t capacity; }; /* Global debug info. */ static struct debug_info g_debug = { 0 }; /* CPU state. */ struct cpu { u64 regs[REGISTERS]; u32 pc; /* Program counter. */ u32 programsize; /* Size of loaded program. */ instr_t *program; /* Program instructions. */ bool running; /* Execution status. */ bool faulted; /* There was a fault in execution. */ bool ebreak; /* Program terminated via EBREAK. */ reg_t modified; /* Index of the last modified register. */ }; /* Snapshot of CPU and memory state for reversing execution. */ struct snapshot { struct cpu cpu; /* Copy of CPU state. */ u8 memory[MEMORY_SIZE]; /* Copy of memory. */ }; /* Circular buffer for snapshots. */ struct snapshot_buffer { struct snapshot snapshots[MAX_SNAPSHOTS]; int head; /* Index of most recent snapshot. */ int count; }; /* CPU memory. */ static u8 memory[MEMORY_SIZE]; /* Loaded section sizes, used for bounds checking and diagnostics. */ static u32 program_base = 0; static u32 program_bytes = 0; static u32 rodata_bytes = 0; static u32 data_bytes = 0; /* Snapshot buffer. */ static struct snapshot_buffer snapshots; /* File descriptor table for guest file operations. */ static int guest_fds[MAX_OPEN_FILES]; /* Initialize the guest file descriptor table. */ static void guest_fd_table_init(void) { for (int i = 0; i < MAX_OPEN_FILES; i++) { guest_fds[i] = -1; } } /* Add a host file descriptor to the guest table. */ static int guest_fd_table_add(int host_fd) { /* Start at 3 to skip stdin/stdout/stderr. */ for (int i = 3; i < MAX_OPEN_FILES; i++) { if (guest_fds[i] == -1) { guest_fds[i] = host_fd; return i; } } return -1; } /* Get the host fd for a guest file descriptor. */ static int guest_fd_table_get(int guest_fd) { if (guest_fd < 0 || guest_fd >= MAX_OPEN_FILES) { return -1; } /* Standard streams map directly. */ if (guest_fd < 3) { return guest_fd; } return guest_fds[guest_fd]; } /* Remove a file descriptor from the guest table. */ static void guest_fd_table_remove(int guest_fd) { if (guest_fd >= 3 && guest_fd < MAX_OPEN_FILES) { guest_fds[guest_fd] = -1; } } /* Single entry in the instruction trace ring buffer. */ struct trace_entry { u32 pc; instr_t instr; u64 regs[REGISTERS]; }; /* Circular buffer of recent instruction traces. */ struct trace_ring { struct trace_entry entries[TRACE_HISTORY]; int head; int count; }; /* Headless-mode instruction trace buffer. */ static struct trace_ring headless_trace = { .head = -1, .count = 0 }; /* Terminal dimensions in rows and columns. */ struct termsize { int rows; int cols; }; /* Forward declarations. */ static void ui_render_instructions( struct cpu *, int col, int width, int height ); static void ui_render_registers( struct cpu *, enum display, int col, int height ); static void ui_render_stack(struct cpu *, enum display, int col, int height); static void ui_render(struct cpu *, enum display); static void cpu_execute(struct cpu *, enum display, bool headless); static void emit_fault_diagnostics(struct cpu *, u32 pc); /* Emulator runtime options, populated from CLI flags. */ struct emulator_options { bool stack_guard; u32 stack_size; bool debug_enabled; bool trace_headless; bool trace_enabled; bool trace_print_instructions; u32 trace_depth; u64 headless_max_steps; u32 memory_size; u32 data_memory_size; bool watch_enabled; u32 watch_addr; u32 watch_size; u32 watch_arm_pc; bool watch_zero_only; u32 watch_skip; bool watch_backtrace; u32 watch_backtrace_depth; bool validate_memory; bool count_instructions; bool jit_disabled; }; /* Global emulator options. */ static struct emulator_options g_opts = { .stack_guard = true, .stack_size = DEFAULT_STACK_SIZE, .debug_enabled = false, .trace_headless = false, .trace_enabled = false, .trace_print_instructions = false, .trace_depth = 32, .headless_max_steps = HEADLESS_MAX_STEPS, .memory_size = DEFAULT_MEMORY_SIZE, .data_memory_size = DEFAULT_DATA_MEMORY_SIZE, .watch_enabled = false, .watch_addr = 0, .watch_size = 0, .watch_arm_pc = 0, .watch_zero_only = false, .watch_skip = 0, .watch_backtrace = false, .watch_backtrace_depth = 8, .validate_memory = true, .count_instructions = false, .jit_disabled = false, }; static void dump_watch_context(struct cpu *, u32 addr, u32 size, u32 value); /* Return true if the given address range overlaps the watched region. */ static inline bool watch_hit(u32 addr, u32 size) { if (!g_opts.watch_enabled) return false; u32 start = g_opts.watch_addr; u32 end = start + (g_opts.watch_size ? g_opts.watch_size : 1); return addr < end && (addr + size) > start; } /* Check a store against the memory watchpoint and halt on a hit. */ static inline void watch_store(struct cpu *cpu, u32 addr, u32 size, u32 value) { if (!watch_hit(addr, size)) return; if (g_opts.watch_arm_pc && cpu && cpu->pc < g_opts.watch_arm_pc) return; if (g_opts.watch_zero_only && value != 0) return; if (g_opts.watch_skip > 0) { g_opts.watch_skip--; return; } fprintf( stderr, "[WATCH] pc=%08x addr=%08x size=%u value=%08x\n", cpu ? cpu->pc : 0, addr, size, value ); dump_watch_context(cpu, addr, size, value); if (cpu) { cpu->running = false; cpu->faulted = true; cpu->ebreak = true; } } /* Fixed stack guard zone size. */ #define STACK_GUARD_BYTES 16 /* Clamp and align stack size to a valid range. */ static inline u32 sanitize_stack_bytes(u32 bytes) { if (bytes < WORD_SIZE) bytes = WORD_SIZE; bytes = (u32)align((i32)bytes, WORD_SIZE); /* Keep at least one word for guard computations. */ if (bytes >= g_opts.memory_size) bytes = g_opts.memory_size - WORD_SIZE; return bytes; } /* Return the active stack guard size, or 0 if guards are disabled. */ static inline u32 stack_guard_bytes(void) { if (!g_opts.stack_guard) return 0; return STACK_GUARD_BYTES; } /* Return the configured stack size. */ static inline u32 stack_size(void) { return g_opts.stack_size; } /* Return the highest addressable word-aligned memory address. */ static inline u32 memory_top(void) { return g_opts.memory_size - WORD_SIZE; } /* Return the lowest address in the stack region. */ static inline u32 stack_bottom(void) { return memory_top() - stack_size() + WORD_SIZE; } /* Return the highest usable stack address (inside the guard zone). */ static inline u32 stack_usable_top(void) { u32 guard = stack_guard_bytes(); u32 size = stack_size(); if (guard >= size) guard = size - WORD_SIZE; return memory_top() - guard; } /* Return the lowest usable stack address (inside the guard zone). */ static inline u32 stack_usable_bottom(void) { u32 guard = stack_guard_bytes(); u32 size = stack_size(); if (guard >= size) guard = size - WORD_SIZE; return stack_bottom() + guard; } /* Return true if addr falls within the stack region. */ static inline bool stack_contains(u32 addr) { return addr >= stack_bottom() && addr <= memory_top(); } /* Return true if the range [start, end] overlaps a stack guard zone. */ static inline bool stack_guard_overlaps(u32 guard, u32 start, u32 end) { if (guard == 0) return false; u32 low_guard_end = stack_bottom() + guard - 1; u32 high_guard_start = memory_top() - guard + 1; return (start <= low_guard_end && end >= stack_bottom()) || (end >= high_guard_start && start <= memory_top()); } /* Return true if addr falls inside a stack guard zone. */ static inline bool stack_guard_contains(u32 guard, u32 addr) { return stack_guard_overlaps(guard, addr, addr); } /* Load a 16-bit value from memory in little-endian byte order. */ static inline u16 memory_load_u16(u32 addr) { return (u16)(memory[addr] | (memory[addr + 1] << 8)); } /* Load a 32-bit value from memory in little-endian byte order. */ static inline u32 memory_load_u32(u32 addr) { return memory[addr] | (memory[addr + 1] << 8) | (memory[addr + 2] << 16) | (memory[addr + 3] << 24); } /* Load a 64-bit value from memory in little-endian byte order. */ static inline u64 memory_load_u64(u32 addr) { return (u64)memory[addr] | ((u64)memory[addr + 1] << 8) | ((u64)memory[addr + 2] << 16) | ((u64)memory[addr + 3] << 24) | ((u64)memory[addr + 4] << 32) | ((u64)memory[addr + 5] << 40) | ((u64)memory[addr + 6] << 48) | ((u64)memory[addr + 7] << 56); } /* Store a byte to memory. */ static inline void memory_store_u8(u32 addr, u8 value) { memory[addr] = value; } /* Store a 16-bit value to memory in little-endian byte order. */ static inline void memory_store_u16(u32 addr, u16 value) { memory[addr] = (u8)(value & 0xFF); memory[addr + 1] = (u8)((value >> 8) & 0xFF); } /* Store a 32-bit value to memory in little-endian byte order. */ static inline void memory_store_u32(u32 addr, u32 value) { assert(addr + 3 < g_opts.memory_size); memory[addr] = (u8)(value & 0xFF); memory[addr + 1] = (u8)((value >> 8) & 0xFF); memory[addr + 2] = (u8)((value >> 16) & 0xFF); memory[addr + 3] = (u8)((value >> 24) & 0xFF); } /* Store a 64-bit value to memory in little-endian byte order. */ static inline void memory_store_u64(u32 addr, u64 value) { assert(addr + 7 < g_opts.memory_size); memory_store_u32(addr, (u32)(value & 0xFFFFFFFF)); memory_store_u32(addr + 4, (u32)(value >> 32)); } /* Load a 32-bit word from memory, returning false if out of bounds. */ static inline bool load_word_safe(u32 addr, u32 *out) { if (addr > g_opts.memory_size - WORD_SIZE) return false; *out = memory_load_u32(addr); return true; } /* Dump register state and backtrace when a watchpoint fires. */ static void dump_watch_context(struct cpu *cpu, u32 addr, u32 size, u32 value) { if (!g_opts.watch_backtrace || !cpu) return; (void)addr; (void)size; (void)value; fprintf( stderr, " regs: SP=%08x FP=%08x RA=%08x A0=%08x A1=%08x A2=%08x " "A3=%08x\n", (u32)cpu->regs[SP], (u32)cpu->regs[FP], (u32)cpu->regs[RA], (u32)cpu->regs[A0], (u32)cpu->regs[A1], (u32)cpu->regs[A2], (u32)cpu->regs[A3] ); u64 fp64 = cpu->regs[FP]; u32 fp = (fp64 <= (u64)UINT32_MAX) ? (u32)fp64 : 0; u32 pc = cpu->pc; fprintf( stderr, " backtrace (depth %u):\n", g_opts.watch_backtrace_depth ); for (u32 depth = 0; depth < g_opts.watch_backtrace_depth; depth++) { bool has_frame = stack_contains(fp) && fp >= (2 * WORD_SIZE); u32 saved_ra = 0; u32 prev_fp = 0; if (has_frame) { has_frame = load_word_safe(fp - WORD_SIZE, &saved_ra) && load_word_safe(fp - 2 * WORD_SIZE, &prev_fp) && stack_contains(prev_fp); } fprintf( stderr, " #%u pc=%08x fp=%08x ra=%08x%s\n", depth, pc, fp, has_frame ? saved_ra : 0, has_frame ? "" : " (?)" ); if (!has_frame || prev_fp == fp || prev_fp == 0) break; pc = saved_ra; fp = prev_fp; } } /* Print usage information and return 1. */ static int usage(const char *prog) { fprintf( stderr, "usage: %s [-run] [-no-guard-stack]" " [-stack-size=KB] [-no-validate] [-debug]" " [-trace|-trace-headless] [-trace-depth=n] [-trace-instructions]" " [-max-steps=n] [-memory-size=KB] [-data-size=KB]" " [-watch=addr] [-watch-size=bytes] [-watch-arm-pc=addr]" " [-watch-zero-only] [-watch-skip=n]" " [-watch-backtrace] [-watch-bt-depth=n]" " [-count-instructions] [-no-jit]" " [program args...]\n", prog ); return 1; } /* Configuration parsed from CLI flags prior to launching the emulator. */ struct cli_config { bool headless; const char *program_path; int arg_index; }; /* Parse a string as an unsigned 32-bit integer. Returns false on error. */ static bool parse_u32(const char *str, const char *label, int base, u32 *out) { char *end = NULL; errno = 0; unsigned long val = strtoul(str, &end, base); if (errno != 0 || end == str || *end != '\0') { fprintf(stderr, "invalid %s '%s'; expected integer\n", label, str); return false; } if (val > UINT32_MAX) val = UINT32_MAX; *out = (u32)val; return true; } /* Parse a string as an unsigned 64-bit integer. Returns false on error. */ static bool parse_u64(const char *str, const char *label, u64 *out) { char *end = NULL; errno = 0; unsigned long long val = strtoull(str, &end, 10); if (errno != 0 || end == str || *end != '\0') { fprintf(stderr, "invalid %s '%s'; expected integer\n", label, str); return false; } *out = (u64)val; return true; } /* Parse and validate the physical memory size passed to -memory-size=. */ static bool parse_memory_size_value(const char *value) { u64 parsed; if (!parse_u64(value, "memory size", &parsed)) return false; u64 bytes = parsed * 1024; if (bytes <= (u64)(DATA_MEMORY_START + WORD_SIZE)) { fprintf( stderr, "memory size too small; minimum is %u KB\n", (DATA_MEMORY_START + WORD_SIZE + 1024) / 1024 ); return false; } if (bytes > (u64)MEMORY_SIZE) { fprintf( stderr, "memory size too large; maximum is %u KB (recompile emulator " "to increase)\n", MEMORY_SIZE / 1024 ); return false; } g_opts.memory_size = (u32)bytes; return true; } /* Parse and validate the depth passed to -trace-depth=. */ static bool parse_trace_depth_value(const char *value) { u32 parsed; if (!parse_u32(value, "trace depth", 10, &parsed)) return false; if (parsed == 0) { fprintf(stderr, "trace depth must be greater than zero\n"); return false; } if (parsed > TRACE_HISTORY) parsed = TRACE_HISTORY; g_opts.trace_depth = parsed; return true; } /* Parse and validate the step limit passed to -max-steps=. */ static bool parse_max_steps_value(const char *value) { u64 parsed; if (!parse_u64(value, "max steps", &parsed)) return false; if (parsed == 0) { fprintf(stderr, "max steps must be greater than zero\n"); return false; } g_opts.headless_max_steps = parsed; return true; } /* Parse and validate the stack size passed to -stack-size=. */ static bool parse_stack_size_value(const char *value) { u64 parsed; if (!parse_u64(value, "stack size", &parsed)) return false; if (parsed == 0) { fprintf(stderr, "stack size must be greater than zero\n"); return false; } u64 bytes = parsed * 1024; if (bytes >= MEMORY_SIZE) { fprintf( stderr, "stack size too large; maximum is %u KB\n", MEMORY_SIZE / 1024 ); return false; } g_opts.stack_size = sanitize_stack_bytes((u32)bytes); return true; } /* Parse and validate the data size passed to -data-size=. */ static bool parse_data_size_value(const char *value) { u64 parsed; if (!parse_u64(value, "data size", &parsed)) return false; if (parsed == 0) { fprintf(stderr, "data size must be greater than zero\n"); return false; } u64 bytes = parsed * 1024; if (bytes > (u64)MEMORY_SIZE) { fprintf( stderr, "data size too large; maximum is %u KB\n", MEMORY_SIZE / 1024 ); return false; } g_opts.data_memory_size = (u32)bytes; return true; } /* Validate that the stack fits within available memory. */ static bool validate_memory_layout(void) { if (g_opts.stack_size >= g_opts.memory_size) { fprintf( stderr, "stack size (%u) must be smaller than memory size (%u)\n", g_opts.stack_size, g_opts.memory_size ); return false; } return true; } /* Parse emulator CLI arguments, returning the selected mode and file path. */ static bool parse_cli_args(int argc, char *argv[], struct cli_config *cfg) { bool headless = false; int argi = 1; while (argi < argc) { const char *arg = argv[argi]; if (strcmp(arg, "--") == 0) { argi++; break; } if (arg[0] != '-') break; if (strcmp(arg, "-run") == 0) { headless = true; argi++; continue; } if (strncmp(arg, "-stack-size=", 12) == 0) { if (!parse_stack_size_value(arg + 12)) return false; argi++; continue; } if (strcmp(arg, "-no-guard-stack") == 0) { g_opts.stack_guard = false; argi++; continue; } if (strcmp(arg, "-no-validate") == 0) { g_opts.validate_memory = false; argi++; continue; } if (strcmp(arg, "-debug") == 0) { g_opts.debug_enabled = true; argi++; continue; } if (strcmp(arg, "-trace") == 0 || strcmp(arg, "-trace-headless") == 0) { g_opts.trace_enabled = true; argi++; continue; } if (strcmp(arg, "-trace-instructions") == 0) { g_opts.trace_print_instructions = true; argi++; continue; } if (strncmp(arg, "-trace-depth=", 13) == 0) { if (!parse_trace_depth_value(arg + 13)) return false; argi++; continue; } if (strncmp(arg, "-max-steps=", 11) == 0) { if (!parse_max_steps_value(arg + 11)) return false; argi++; continue; } if (strncmp(arg, "-memory-size=", 13) == 0) { if (!parse_memory_size_value(arg + 13)) return false; argi++; continue; } if (strncmp(arg, "-data-size=", 11) == 0) { if (!parse_data_size_value(arg + 11)) return false; argi++; continue; } if (strncmp(arg, "-watch=", 7) == 0) { if (!parse_u32(arg + 7, "watch address", 0, &g_opts.watch_addr)) return false; g_opts.watch_enabled = true; argi++; continue; } if (strncmp(arg, "-watch-size=", 12) == 0) { if (!parse_u32(arg + 12, "watch size", 0, &g_opts.watch_size)) return false; if (g_opts.watch_size == 0) { fprintf(stderr, "watch size must be greater than zero\n"); return false; } argi++; continue; } if (strcmp(arg, "-watch-zero-only") == 0) { g_opts.watch_zero_only = true; argi++; continue; } if (strncmp(arg, "-watch-skip=", 12) == 0) { if (!parse_u32(arg + 12, "watch skip", 0, &g_opts.watch_skip)) return false; argi++; continue; } if (strcmp(arg, "-watch-disable") == 0) { g_opts.watch_enabled = false; argi++; continue; } if (strncmp(arg, "-watch-arm-pc=", 14) == 0) { if (!parse_u32(arg + 14, "watch arm pc", 0, &g_opts.watch_arm_pc)) return false; argi++; continue; } if (strcmp(arg, "-watch-backtrace") == 0) { g_opts.watch_backtrace = true; argi++; continue; } if (strncmp(arg, "-watch-bt-depth=", 16) == 0) { u32 depth; if (!parse_u32(arg + 16, "watch backtrace depth", 0, &depth)) return false; if (depth == 0) { fprintf( stderr, "watch backtrace depth must be greater than zero\n" ); return false; } g_opts.watch_backtrace = true; g_opts.watch_backtrace_depth = depth; argi++; continue; } if (strcmp(arg, "-count-instructions") == 0) { g_opts.count_instructions = true; argi++; continue; } if (strcmp(arg, "-no-jit") == 0) { g_opts.jit_disabled = true; argi++; continue; } usage(argv[0]); return false; } if (argi >= argc) { usage(argv[0]); return false; } cfg->program_path = argv[argi++]; cfg->arg_index = argi; cfg->headless = headless; if (g_opts.watch_enabled && g_opts.watch_size == 0) g_opts.watch_size = 4; if (!validate_memory_layout()) return false; g_opts.stack_size = sanitize_stack_bytes(g_opts.stack_size); return true; } /* Validate a load or store against memory bounds and stack guards. */ static bool validate_memory_access( struct cpu *cpu, u64 addr, u32 size, reg_t base_reg, const char *op, bool is_store ) { /* Skip validation for performance if disabled. */ if (!g_opts.validate_memory) return true; if (size == 0) size = 1; const char *kind = is_store ? "store" : "load"; u64 span_end = addr + (u64)size; if (addr > (u64)g_opts.memory_size || span_end > (u64)g_opts.memory_size) { printf( "Memory %s out of bounds at PC=%08x: addr=%016llx size=%u (%s)\n", kind, cpu->pc, (unsigned long long)addr, size, op ); cpu->running = false; emit_fault_diagnostics(cpu, cpu->pc); return false; } u32 addr32 = (u32)addr; u32 end = (u32)(span_end - 1); if (addr32 < DATA_MEMORY_START) { if (is_store) { printf( "Read-only memory store at PC=%08x: addr=%08x size=%u (%s)\n", cpu->pc, addr32, size, op ); cpu->running = false; emit_fault_diagnostics(cpu, cpu->pc); return false; } return true; } u32 guard = stack_guard_bytes(); u64 base_val = cpu->regs[base_reg]; bool base_in_stack = base_val <= (u64)UINT32_MAX && stack_contains((u32)base_val); bool start_in_stack = stack_contains(addr32); bool end_in_stack = stack_contains(end); if (base_in_stack || start_in_stack || end_in_stack) { u32 bottom = stack_bottom(); if (addr32 < bottom || end > memory_top()) { printf( "Stack %s out of bounds at PC=%08x: base=%s (0x%08x) addr=%08x " "size=%u (%s)\n", kind, cpu->pc, reg_names[base_reg], (u32)cpu->regs[base_reg], addr32, size, op ); cpu->running = false; emit_fault_diagnostics(cpu, cpu->pc); return false; } if (stack_guard_overlaps(guard, addr32, end)) { printf( "Stack guard %s violation at PC=%08x: base=%s (0x%08x) " "addr=%08x " "size=%u guard=%u (%s)\n", kind, cpu->pc, reg_names[base_reg], (u32)cpu->regs[base_reg], addr32, size, guard, op ); cpu->running = false; emit_fault_diagnostics(cpu, cpu->pc); return false; } } return true; } /* Validate that a register holds a valid stack address. */ static bool validate_stack_register( struct cpu *cpu, reg_t reg, const char *label, u32 pc, bool optional ) { /* Skip validation for performance if disabled. */ if (!g_opts.validate_memory) return true; u64 value = cpu->regs[reg]; if (optional && value == 0) return true; /* Detect addresses with upper bits set -- these can never be valid * stack addresses in the emulator's physical memory. */ if (value > (u64)UINT32_MAX || (u32)value < stack_bottom() || (u32)value > memory_top()) { printf( "%s (%s) out of stack bounds at PC=%08x: value=%016llx\n", label, reg_names[reg], pc, (unsigned long long)value ); cpu->running = false; emit_fault_diagnostics(cpu, pc); return false; } u32 guard = stack_guard_bytes(); if (stack_guard_contains(guard, (u32)value)) { printf( "Stack guard triggered by %s (%s) at PC=%08x: value=%08x " "guard=%u\n", label, reg_names[reg], pc, (u32)value, guard ); cpu->running = false; emit_fault_diagnostics(cpu, pc); return false; } return true; } /* Toggle stack guarding in the TUI and re-validate live stack registers. */ static void toggle_stack_guard(struct cpu *cpu) { g_opts.stack_guard = !g_opts.stack_guard; printf( "\nStack guard %s (%u bytes)\n", g_opts.stack_guard ? "enabled" : "disabled", STACK_GUARD_BYTES ); if (g_opts.stack_guard && cpu->running) { validate_stack_register(cpu, SP, "SP", cpu->pc, false); if (cpu->running) { validate_stack_register(cpu, FP, "FP", cpu->pc, true); } } } /* Get terminal dimensions. */ static struct termsize termsize(void) { struct winsize w; struct termsize size = { 24, 80 }; /* Default fallback. */ if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) != -1) { size.rows = w.ws_row; size.cols = w.ws_col; } return size; } /* Take a snapshot of the current CPU and memory state. */ static void snapshot_save(struct cpu *cpu) { int nexti = (snapshots.head + 1 + MAX_SNAPSHOTS) % MAX_SNAPSHOTS; memcpy(&snapshots.snapshots[nexti].cpu, cpu, sizeof(struct cpu)); memcpy(snapshots.snapshots[nexti].memory, memory, g_opts.memory_size); /* Fix the program pointer to reference the snapshot's own memory. */ snapshots.snapshots[nexti].cpu.program = (instr_t *)snapshots.snapshots[nexti].memory; snapshots.head = nexti; if (snapshots.count < MAX_SNAPSHOTS) snapshots.count++; } /* Restore the most recent snapshot, returning false if none remain. */ static bool snapshot_restore(struct cpu *cpu) { if (snapshots.count <= 1) return false; snapshots.head = (snapshots.head + MAX_SNAPSHOTS - 1) % MAX_SNAPSHOTS; snapshots.count--; int previ = snapshots.head; memcpy(cpu, &snapshots.snapshots[previ].cpu, sizeof(struct cpu)); memcpy(memory, snapshots.snapshots[previ].memory, g_opts.memory_size); /* Fix the program pointer to reference the live memory buffer. */ cpu->program = (instr_t *)memory; return true; } /* Initialize the snapshot buffer with an initial snapshot. */ static void snapshot_init(struct cpu *cpu) { snapshots.head = -1; snapshots.count = 0; snapshot_save(cpu); } /* Reset the headless instruction trace buffer. */ static void trace_reset(void) { headless_trace.head = -1; headless_trace.count = 0; } /* Record the current instruction into the trace ring buffer. */ static void trace_record(struct cpu *cpu, instr_t ins) { if (!g_opts.trace_enabled || !g_opts.trace_headless) return; int next = (headless_trace.head + 1 + TRACE_HISTORY) % TRACE_HISTORY; headless_trace.head = next; headless_trace.entries[next].pc = cpu->pc; headless_trace.entries[next].instr = ins; memcpy(headless_trace.entries[next].regs, cpu->regs, sizeof(cpu->regs)); if (headless_trace.count < TRACE_HISTORY) headless_trace.count++; } /* Dump the headless instruction trace to stdout. */ static bool trace_dump(u32 fault_pc) { if (!g_opts.trace_enabled || !g_opts.trace_headless || headless_trace.count == 0) return false; int limit = (int)g_opts.trace_depth; if (limit <= 0) limit = FAULT_TRACE_DEPTH; if (limit > TRACE_HISTORY) limit = TRACE_HISTORY; if (limit > headless_trace.count) limit = headless_trace.count; printf("Headless trace (newest first):\n"); for (int i = 0; i < limit; i++) { int idx = (headless_trace.head - i + TRACE_HISTORY) % TRACE_HISTORY; struct trace_entry *entry = &headless_trace.entries[idx]; char istr[MAX_INSTR_STR_LEN] = { 0 }; sprint_instr(entry->instr, istr, true); printf( " [%d] PC=%08x %s%s\n", i, entry->pc, istr, (entry->pc == fault_pc) ? " <-- fault" : "" ); printf( " SP=%08x FP=%08x RA=%08x A0=%08x A1=%08x A2=%08x\n", (u32)entry->regs[SP], (u32)entry->regs[FP], (u32)entry->regs[RA], (u32)entry->regs[A0], (u32)entry->regs[A1], (u32)entry->regs[A2] ); } return true; } /* Dump recent snapshot history to stdout for fault diagnostics. */ static bool snapshot_dump_history(struct cpu *cpu, u32 fault_pc) { (void)cpu; if (snapshots.count == 0) return false; int limit = FAULT_TRACE_DEPTH; if (limit > snapshots.count) limit = snapshots.count; printf("Snapshot history (newest first):\n"); for (int i = 0; i < limit; i++) { int idx = (snapshots.head - i + MAX_SNAPSHOTS) % MAX_SNAPSHOTS; struct snapshot *snap = &snapshots.snapshots[idx]; u32 next_pc = snap->cpu.pc; u32 exec_pc = next_pc >= INSTR_SIZE ? next_pc - INSTR_SIZE : next_pc; char istr[MAX_INSTR_STR_LEN] = { 0 }; u32 instr_index = exec_pc / INSTR_SIZE; if (instr_index < snap->cpu.programsize) { sprint_instr(snap->cpu.program[instr_index], istr, true); } else { snprintf(istr, sizeof(istr), "", exec_pc); } printf( " [%d] PC next=%08x prev=%08x %s%s\n", i, next_pc, exec_pc, istr, (exec_pc == fault_pc || next_pc == fault_pc) ? " <-- fault" : "" ); printf( " SP=%08x FP=%08x RA=%08x A0=%08x\n", (u32)snap->cpu.regs[SP], (u32)snap->cpu.regs[FP], (u32)snap->cpu.regs[RA], (u32)snap->cpu.regs[A0] ); } return true; } /* Emit runtime fault diagnostics including trace and snapshot history. */ static void emit_fault_diagnostics(struct cpu *cpu, u32 pc) { if (cpu->faulted) return; cpu->faulted = true; printf("\n--- runtime fault diagnostics ---\n"); bool printed = false; printed |= trace_dump(pc); printed |= snapshot_dump_history(cpu, pc); if (!printed) { printf("No trace data available.\n"); } printf("--- end diagnostics ---\n"); fflush(stdout); } /* Return true if the CPU's PC is outside the loaded program bounds. */ static inline bool cpu_out_of_bounds(struct cpu *cpu) { if (!g_opts.validate_memory) return false; if (program_bytes == 0) return true; if (cpu->pc < program_base) return true; return (cpu->pc - program_base) >= program_bytes; } /* Last executed PC, used for detecting branches/jumps in trace mode. */ static u32 last_executed_pc = 0; /* Reset CPU state (keeping program loaded). */ static void cpu_reset(struct cpu *cpu) { trace_reset(); memset(cpu->regs, 0, sizeof(cpu->regs)); /* Set SP to the top of the usable stack, aligned to 16 bytes * as required by the RISC-V ABI. */ cpu->regs[SP] = stack_usable_top() & ~0xF; cpu->pc = program_base; cpu->running = true; cpu->faulted = false; cpu->ebreak = false; cpu->modified = ZERO; last_executed_pc = 0; } /* Initialize CPU and memory to a clean state. */ static void cpu_init(struct cpu *cpu) { memset(memory, 0, g_opts.memory_size); cpu->program = (instr_t *)memory; cpu->programsize = 0; trace_reset(); guest_fd_table_init(); cpu_reset(cpu); } /* Open a file via the openat syscall (56). */ static i32 ecall_openat(u32 pathname_addr, i32 flags) { if (pathname_addr >= g_opts.memory_size) return -1; /* Find the null terminator to validate the string is in bounds. */ u32 path_end = pathname_addr; while (path_end < g_opts.memory_size && memory[path_end] != 0) path_end++; if (path_end >= g_opts.memory_size) return -1; i32 host_fd = open((const char *)&memory[pathname_addr], flags, 0644); if (host_fd < 0) return -1; i32 guest_fd = guest_fd_table_add(host_fd); if (guest_fd < 0) { close(host_fd); return -1; } return guest_fd; } /* Close a file descriptor via the close syscall (57). */ static i32 ecall_close(i32 guest_fd) { /* Don't close standard streams. */ if (guest_fd < 3) return 0; i32 host_fd = guest_fd_table_get(guest_fd); if (host_fd >= 0) { i32 result = close(host_fd); guest_fd_table_remove(guest_fd); return result; } return -1; } /* Load a binary section from disk into emulator memory at the given offset. */ static u32 load_section( const char *filepath, const char *suffix, u32 offset, u32 limit, const char *label ) { char path[PATH_MAX]; snprintf(path, sizeof(path), "%s.%s", filepath, suffix); FILE *file = fopen(path, "rb"); if (!file) return 0; if (fseek(file, 0, SEEK_END) != 0) { fclose(file); bail("failed to seek %s section", label); } long size = ftell(file); if (size < 0) { fclose(file); bail("failed to determine size of %s section", label); } if (fseek(file, 0, SEEK_SET) != 0) { fclose(file); bail("failed to rewind %s section", label); } if (size == 0) { fclose(file); return 0; } u32 u_size = (u32)size; u64 end = (u64)offset + (u64)u_size; if (end > (u64)limit) { fclose(file); u32 max_size = limit - offset; bail( "%s section too large for emulator memory: required %u bytes, max " "%u bytes", label, u_size, max_size ); } if (end > (u64)g_opts.memory_size) { fclose(file); u32 max_size = g_opts.memory_size - offset; bail( "%s section exceeds physical memory: required %u bytes at offset " "%u, " "but only %u bytes available (total memory=%u, use " "-memory-size=... or recompile emulator with a larger " "MEMORY_SIZE)", label, u_size, offset, max_size, g_opts.memory_size ); } size_t read = fread(&memory[offset], 1, u_size, file); fclose(file); if (read != u_size) { bail( "could not read entire %s section: read %zu bytes, expected %u " "bytes (offset=%u, limit=%u)", label, read, u_size, offset, limit ); } return u_size; } /* Prepare the environment block (argv) on the guest stack. */ static void prepare_env(struct cpu *cpu, int argc, char **argv) { if (argc < 0 || argv == NULL) argc = 0; usize bytes = 0; for (int i = 0; i < argc; i++) bytes += strlen(argv[i]) + 1; /* Include terminating NUL. */ /* In RV64, slices are 16 bytes (8-byte ptr + 4-byte len + 4 padding). */ u32 slice_size = 16; u32 slice_array_size = (u32)argc * slice_size; u32 base_size = slice_size + slice_array_size; u32 total_size = align(base_size + (u32)bytes, 16); /* Place the env block below the current stack pointer so it doesn't * overlap with uninitialized static data. The .rw.data file only * contains initialized statics; undefined statics occupy memory after * the loaded data but are not in the file. Placing the env block in * the data region would clobber those zero-initialized areas. */ u32 sp = (u32)cpu->regs[SP]; u32 env_addr = (sp - total_size) & ~0xFu; if (env_addr <= DATA_MEMORY_START + data_bytes) bail("not enough memory to prepare environment block"); /* Move SP below the env block so the program's stack doesn't overwrite it. */ cpu->regs[SP] = env_addr; u32 slices_addr = env_addr + slice_size; u32 strings_addr = slices_addr + (argc > 0 ? slice_array_size : 0); /* Write the Env slice header. */ memory_store_u64(env_addr, argc > 0 ? slices_addr : 0); memory_store_u32(env_addr + 8, (u32)argc); /* Copy argument strings and populate slices. */ u32 curr = strings_addr; for (int i = 0; i < argc; i++) { size_t len = strlen(argv[i]); if (curr + len >= g_opts.memory_size) bail("environment string does not fit in emulator memory"); memcpy(&memory[curr], argv[i], len); memory[curr + len] = 0; /* Null-terminate for syscall compatibility. */ u32 slice_entry = slices_addr + (u32)i * slice_size; memory_store_u64(slice_entry, curr); memory_store_u32(slice_entry + 8, (u32)len); curr += (u32)len + 1; } cpu->regs[A0] = env_addr; cpu->regs[A1] = env_addr; } /* Load debug information from the .debug file. */ static void debug_load(const char *program_path) { char debugpath[PATH_MAX]; snprintf(debugpath, sizeof(debugpath), "%s.debug", program_path); FILE *file = fopen(debugpath, "rb"); if (!file) return; /* Debug file is optional. */ g_debug.capacity = 64; g_debug.entries = malloc(sizeof(struct debug_entry) * g_debug.capacity); g_debug.count = 0; if (!g_debug.entries) { fclose(file); return; } while (!feof(file)) { struct debug_entry entry; if (fread(&entry.pc, sizeof(u32), 1, file) != 1) break; if (fread(&entry.offset, sizeof(u32), 1, file) != 1) break; /* Read null-terminated file path. */ size_t i = 0; int c; while (i < PATH_MAX - 1 && (c = fgetc(file)) != EOF && c != '\0') { entry.file[i++] = (char)c; } entry.file[i] = '\0'; if (c == EOF && i == 0) break; /* Grow array if needed. */ if (g_debug.count >= g_debug.capacity) { g_debug.capacity *= 2; g_debug.entries = realloc( g_debug.entries, sizeof(struct debug_entry) * g_debug.capacity ); if (!g_debug.entries) { fclose(file); return; } } g_debug.entries[g_debug.count++] = entry; } fclose(file); } /* Look up source location for a given PC. */ static struct debug_entry *debug_lookup(u32 pc) { struct debug_entry *best = NULL; for (size_t i = 0; i < g_debug.count; i++) { if (g_debug.entries[i].pc == pc) { return &g_debug.entries[i]; } /* Track the closest entry at or before this PC. */ if (g_debug.entries[i].pc <= pc) { best = &g_debug.entries[i]; } } return best; } /* Compute the line number from a file path and byte offset. */ static int line_from_offset(const char *filepath, u32 offset) { FILE *file = fopen(filepath, "r"); if (!file) return 0; u32 line = 1; for (u32 i = 0; i < offset; i++) { int c = fgetc(file); if (c == EOF) break; if (c == '\n') line++; } fclose(file); return line; } /* Load the program binary and data sections into memory. */ static void program_init(struct cpu *cpu, const char *filepath) { program_bytes = 0; data_bytes = 0; rodata_bytes = load_section( filepath, "ro.data", DATA_RO_OFFSET, DATA_MEMORY_START, "ro.data" ); program_base = align(DATA_RO_OFFSET + rodata_bytes, WORD_SIZE); if (g_opts.debug_enabled) debug_load(filepath); FILE *file = fopen(filepath, "rb"); if (!file) bail("failed to open file '%s'", filepath); if (fseek(file, 0, SEEK_END) != 0) { fclose(file); bail("failed to seek program '%s'", filepath); } long size = ftell(file); if (size <= 0 || size > PROGRAM_SIZE) { fclose(file); bail( "invalid file size: %ld; maximum program size is %d bytes", size, PROGRAM_SIZE ); } if (program_base + (u32)size > DATA_MEMORY_START) { fclose(file); bail("text section exceeds available program memory"); } if (fseek(file, 0, SEEK_SET) != 0) { fclose(file); bail("failed to rewind program '%s'", filepath); } usize read = fread(&memory[program_base], 1, size, file); fclose(file); if (read != (size_t)size) bail("could not read entire file"); program_bytes = (u32)size; cpu->programsize = (u32)size / sizeof(instr_t); u32 data_limit = DATA_MEMORY_START + g_opts.data_memory_size; if (data_limit > g_opts.memory_size) data_limit = g_opts.memory_size; data_bytes = load_section( filepath, "rw.data", DATA_MEMORY_START, data_limit, "rw.data" ); cpu->pc = program_base; } /* Execute a single instruction. */ static void cpu_execute(struct cpu *cpu, enum display display, bool headless) { if (cpu_out_of_bounds(cpu)) { cpu->running = false; emit_fault_diagnostics(cpu, cpu->pc); if (headless) { fprintf(stderr, "program is out of bounds\n"); return; } bail("program is out of bounds"); } u32 executed_pc = cpu->pc; instr_t ins = cpu->program[cpu->pc / sizeof(instr_t)]; u32 pc_next = cpu->pc + INSTR_SIZE; u32 opcode = ins.r.opcode; cpu->modified = ZERO; trace_record(cpu, ins); /* Print instruction if tracing is enabled in headless mode. * Skip NOPs (addi x0, x0, 0 = 0x00000013). */ if (headless && g_opts.trace_print_instructions && ins.raw != 0x00000013) { /* Print ellipsis if we jumped to a non-sequential instruction. */ if (last_executed_pc != 0 && executed_pc != last_executed_pc + INSTR_SIZE) { printf("%s :%s\n", COLOR_GREY, COLOR_RESET); } char istr[MAX_INSTR_STR_LEN] = { 0 }; int len = sprint_instr(ins, istr, true); int padding = INSTR_STR_LEN - len; if (padding < 0) padding = 0; printf( "%s%08x%s %s%-*s%s", COLOR_GREY, executed_pc, COLOR_RESET, istr, padding, "", COLOR_GREY ); /* Print all non-zero registers. */ bool first = true; for (int i = 0; i < REGISTERS; i++) { if (cpu->regs[i] != 0) { if (!first) printf(" "); printf("%s=%08x", reg_names[i], (u32)cpu->regs[i]); first = false; } } printf("%s\n", COLOR_RESET); } switch (opcode) { case OP_LUI: if (ins.u.rd != 0) { u32 lui_val = ins.u.imm_31_12 << 12; cpu->regs[ins.u.rd] = (u64)(i64)(i32)lui_val; /* RV64: sign-extend to 64 bits. */ cpu->modified = ins.u.rd; } break; case OP_AUIPC: if (ins.u.rd != 0) { u32 auipc_val = ins.u.imm_31_12 << 12; cpu->regs[ins.u.rd] = cpu->pc + (u64)(i64)(i32)auipc_val; /* RV64: sign-extend offset. */ cpu->modified = (reg_t)ins.u.rd; } break; case OP_JAL: { i32 imm = get_j_imm(ins); if (ins.j.rd != 0) { cpu->regs[ins.j.rd] = pc_next; cpu->modified = (reg_t)ins.j.rd; } pc_next = cpu->pc + imm; break; } case OP_JALR: { i32 imm = get_i_imm(ins); if (ins.i.rd != 0) { cpu->regs[ins.i.rd] = pc_next; cpu->modified = (reg_t)ins.i.rd; } /* Calculate target address in full 64-bit precision. */ u64 jalr_target = (cpu->regs[ins.i.rs1] + (i64)imm) & ~(u64)1; /* Check if this is a RET instruction (jalr x0, ra, 0). */ if (ins.i.rd == 0 && ins.i.rs1 == 1 && imm == 0 && jalr_target == 0) { cpu->running = false; if (!headless) { ui_render(cpu, display); printf( "\n%sProgram terminated with return value %d (0x%08x)%s ", COLOR_BOLD_GREEN, (i32)cpu->regs[A0], (u32)cpu->regs[A0], COLOR_RESET ); } } else { pc_next = (u32)jalr_target; } break; } case OP_BRANCH: { bool jump = false; i32 imm = get_b_imm(ins); switch (ins.b.funct3) { case FUNCT3_BYTE: /* beq. */ jump = (cpu->regs[ins.b.rs1] == cpu->regs[ins.b.rs2]); break; case FUNCT3_HALF: /* bne. */ jump = (cpu->regs[ins.b.rs1] != cpu->regs[ins.b.rs2]); break; case FUNCT3_BYTE_U: /* blt. */ jump = ((i64)cpu->regs[ins.b.rs1] < (i64)cpu->regs[ins.b.rs2]); break; case FUNCT3_HALF_U: /* bge. */ jump = ((i64)cpu->regs[ins.b.rs1] >= (i64)cpu->regs[ins.b.rs2]); break; case FUNCT3_OR: /* bltu. */ jump = (cpu->regs[ins.b.rs1] < cpu->regs[ins.b.rs2]); break; case FUNCT3_AND: /* bgeu. */ jump = (cpu->regs[ins.b.rs1] >= cpu->regs[ins.b.rs2]); break; } if (jump) { pc_next = cpu->pc + imm; } break; } case OP_LOAD: { i32 imm = get_i_imm(ins); u64 addr = cpu->regs[ins.i.rs1] + (i64)imm; if (ins.i.rd == ZERO) break; cpu->modified = (reg_t)ins.i.rd; bool fault = false; switch (ins.i.funct3) { case FUNCT3_BYTE: /* lb. */ if (!validate_memory_access(cpu, addr, 1, ins.i.rs1, "lb", false)) { fault = true; break; } /* sign_extend returns i32; on RV64 we sign-extend to 64 bits. */ cpu->regs[ins.i.rd] = (u64)(i64)sign_extend(memory[addr], 8); break; case FUNCT3_HALF: /* lh. */ if (!validate_memory_access(cpu, addr, 2, ins.i.rs1, "lh", false)) { fault = true; break; } cpu->regs[ins.i.rd] = (u64)(i64)sign_extend(memory_load_u16(addr), 16); break; case FUNCT3_WORD: /* lw. */ if (!validate_memory_access(cpu, addr, 4, ins.i.rs1, "lw", false)) { fault = true; break; } /* RV64: lw sign-extends the 32-bit value to 64 bits. */ cpu->regs[ins.i.rd] = (u64)(i64)(i32)memory_load_u32(addr); break; case 0x6: /* lwu (RV64). */ if (!validate_memory_access( cpu, addr, 4, ins.i.rs1, "lwu", false )) { fault = true; break; } cpu->regs[ins.i.rd] = (u64)memory_load_u32(addr); break; case FUNCT3_BYTE_U: /* lbu. */ if (!validate_memory_access( cpu, addr, 1, ins.i.rs1, "lbu", false )) { fault = true; break; } cpu->regs[ins.i.rd] = memory[addr]; break; case FUNCT3_HALF_U: /* lhu. */ if (!validate_memory_access( cpu, addr, 2, ins.i.rs1, "lhu", false )) { fault = true; break; } cpu->regs[ins.i.rd] = memory_load_u16(addr); break; case 0x3: /* ld (RV64). */ if (!validate_memory_access(cpu, addr, 8, ins.i.rs1, "ld", false)) { fault = true; break; } cpu->regs[ins.i.rd] = memory_load_u64(addr); break; } if (fault || !cpu->running) break; break; } case OP_STORE: { i32 imm = get_s_imm(ins); u64 addr = cpu->regs[ins.s.rs1] + (i64)imm; switch (ins.s.funct3) { case FUNCT3_BYTE: /* sb. */ if (!validate_memory_access(cpu, addr, 1, ins.s.rs1, "sb", true)) break; watch_store(cpu, (u32)addr, 1, (u32)cpu->regs[ins.s.rs2]); memory_store_u8(addr, (u8)cpu->regs[ins.s.rs2]); break; case FUNCT3_HALF: /* sh. */ if (!validate_memory_access(cpu, addr, 2, ins.s.rs1, "sh", true)) break; watch_store(cpu, (u32)addr, 2, (u32)cpu->regs[ins.s.rs2]); memory_store_u16(addr, (u16)cpu->regs[ins.s.rs2]); break; case FUNCT3_WORD: /* sw. */ if (!validate_memory_access(cpu, addr, 4, ins.s.rs1, "sw", true)) break; watch_store(cpu, (u32)addr, 4, (u32)cpu->regs[ins.s.rs2]); memory_store_u32(addr, (u32)cpu->regs[ins.s.rs2]); break; case 0x3: /* sd (RV64). */ if (!validate_memory_access(cpu, addr, 8, ins.s.rs1, "sd", true)) break; watch_store(cpu, (u32)addr, 8, (u32)cpu->regs[ins.s.rs2]); memory_store_u64(addr, cpu->regs[ins.s.rs2]); break; } break; } case OP_IMM: { i32 imm = get_i_imm(ins); u32 shamt_mask = 0x3F; /* RV64: 6-bit shift amounts. */ if (ins.i.rd == ZERO) break; cpu->modified = (reg_t)ins.i.rd; switch (ins.i.funct3) { case FUNCT3_ADD: /* addi. */ cpu->regs[ins.i.rd] = cpu->regs[ins.i.rs1] + imm; break; case FUNCT3_SLL: /* slli. */ cpu->regs[ins.i.rd] = cpu->regs[ins.i.rs1] << (imm & shamt_mask); break; case FUNCT3_SLT: /* slti. */ cpu->regs[ins.i.rd] = ((i64)cpu->regs[ins.i.rs1] < (i64)imm) ? 1 : 0; break; case FUNCT3_SLTU: /* sltiu. */ cpu->regs[ins.i.rd] = (cpu->regs[ins.i.rs1] < (u64)(i64)imm) ? 1 : 0; break; case FUNCT3_XOR: /* xori. */ cpu->regs[ins.i.rd] = cpu->regs[ins.i.rs1] ^ imm; break; case FUNCT3_SRL: /* srli/srai. */ if ((imm & 0x400) == 0) { /* srli -- logical right shift. */ cpu->regs[ins.i.rd] = cpu->regs[ins.i.rs1] >> (imm & shamt_mask); } else { /* srai -- arithmetic right shift. */ cpu->regs[ins.i.rd] = (u64)((i64)cpu->regs[ins.i.rs1] >> (imm & shamt_mask)); } break; case FUNCT3_OR: /* ori. */ cpu->regs[ins.i.rd] = cpu->regs[ins.i.rs1] | imm; break; case FUNCT3_AND: /* andi. */ cpu->regs[ins.i.rd] = cpu->regs[ins.i.rs1] & imm; break; } break; } case OP_IMM_32: { /* RV64I: 32-bit immediate operations (ADDIW, SLLIW, SRLIW, SRAIW). * These operate on the lower 32 bits and sign-extend the result. */ i32 imm = get_i_imm(ins); if (ins.i.rd == ZERO) break; cpu->modified = (reg_t)ins.i.rd; switch (ins.i.funct3) { case FUNCT3_ADD: { /* addiw. */ i32 result = (i32)cpu->regs[ins.i.rs1] + imm; cpu->regs[ins.i.rd] = (u64)(i64)result; break; } case FUNCT3_SLL: { /* slliw. */ i32 result = (i32)((u32)cpu->regs[ins.i.rs1] << (imm & 0x1F)); cpu->regs[ins.i.rd] = (u64)(i64)result; break; } case FUNCT3_SRL: { /* srliw/sraiw. */ if ((imm & 0x400) == 0) { /* srliw -- logical right shift, then sign-extend. */ i32 result = (i32)((u32)cpu->regs[ins.i.rs1] >> (imm & 0x1F)); cpu->regs[ins.i.rd] = (u64)(i64)result; } else { /* sraiw -- arithmetic right shift, then sign-extend. */ i32 result = (i32)cpu->regs[ins.i.rs1] >> (imm & 0x1F); cpu->regs[ins.i.rd] = (u64)(i64)result; } break; } } break; } case OP_OP: { if (ins.r.rd == ZERO) break; cpu->modified = (reg_t)ins.r.rd; switch (ins.r.funct7) { case FUNCT7_NORMAL: { u32 shamt_mask = 0x3F; switch (ins.r.funct3) { case FUNCT3_ADD: /* add. */ cpu->regs[ins.r.rd] = cpu->regs[ins.r.rs1] + cpu->regs[ins.r.rs2]; break; case FUNCT3_SLL: /* sll. */ cpu->regs[ins.r.rd] = cpu->regs[ins.r.rs1] << (cpu->regs[ins.r.rs2] & shamt_mask); break; case FUNCT3_SLT: /* slt. */ cpu->regs[ins.r.rd] = ((i64)cpu->regs[ins.r.rs1] < (i64)cpu->regs[ins.r.rs2]) ? 1 : 0; break; case FUNCT3_SLTU: /* sltu. */ cpu->regs[ins.r.rd] = (cpu->regs[ins.r.rs1] < cpu->regs[ins.r.rs2]) ? 1 : 0; break; case FUNCT3_XOR: /* xor. */ cpu->regs[ins.r.rd] = cpu->regs[ins.r.rs1] ^ cpu->regs[ins.r.rs2]; break; case FUNCT3_SRL: /* srl. */ cpu->regs[ins.r.rd] = cpu->regs[ins.r.rs1] >> (cpu->regs[ins.r.rs2] & shamt_mask); break; case FUNCT3_OR: /* or. */ cpu->regs[ins.r.rd] = cpu->regs[ins.r.rs1] | cpu->regs[ins.r.rs2]; break; case FUNCT3_AND: /* and. */ cpu->regs[ins.r.rd] = cpu->regs[ins.r.rs1] & cpu->regs[ins.r.rs2]; break; } break; } case FUNCT7_SUB: switch (ins.r.funct3) { case FUNCT3_ADD: /* sub. */ cpu->regs[ins.r.rd] = cpu->regs[ins.r.rs1] - cpu->regs[ins.r.rs2]; break; case FUNCT3_SRL: /* sra. */ cpu->regs[ins.r.rd] = (u64)((i64)cpu->regs[ins.r.rs1] >> (cpu->regs[ins.r.rs2] & 0x3F)); break; } break; case FUNCT7_MUL: switch (ins.r.funct3) { case FUNCT3_ADD: /* mul. */ cpu->regs[ins.r.rd] = cpu->regs[ins.r.rs1] * cpu->regs[ins.r.rs2]; break; case FUNCT3_XOR: /* div. */ if (cpu->regs[ins.r.rs2] != 0) { cpu->regs[ins.r.rd] = (u64)((i64)cpu->regs[ins.r.rs1] / (i64)cpu->regs[ins.r.rs2]); } else { cpu->regs[ins.r.rd] = (u64)-1; /* Division by zero. */ } break; case FUNCT3_SRL: /* divu. */ if (cpu->regs[ins.r.rs2] != 0) { cpu->regs[ins.r.rd] = cpu->regs[ins.r.rs1] / cpu->regs[ins.r.rs2]; } else { cpu->regs[ins.r.rd] = (u64)-1; /* Division by zero. */ } break; case FUNCT3_OR: /* rem. */ if (cpu->regs[ins.r.rs2] != 0) { cpu->regs[ins.r.rd] = (u64)((i64)cpu->regs[ins.r.rs1] % (i64)cpu->regs[ins.r.rs2]); } else { cpu->regs[ins.r.rd] = cpu->regs[ins.r.rs1]; } break; case FUNCT3_AND: /* remu. */ if (cpu->regs[ins.r.rs2] != 0) { cpu->regs[ins.r.rd] = cpu->regs[ins.r.rs1] % cpu->regs[ins.r.rs2]; } else { cpu->regs[ins.r.rd] = cpu->regs[ins.r.rs1]; } break; } break; } break; } case OP_OP_32: { /* RV64I: 32-bit register-register operations (ADDW, SUBW, SLLW, SRLW, * SRAW, MULW, DIVW, DIVUW, REMW, REMUW). These operate on the lower 32 * bits and sign-extend the result to 64 bits. */ if (ins.r.rd == ZERO) break; cpu->modified = (reg_t)ins.r.rd; u32 rs1_32 = (u32)cpu->regs[ins.r.rs1]; u32 rs2_32 = (u32)cpu->regs[ins.r.rs2]; switch (ins.r.funct7) { case FUNCT7_NORMAL: switch (ins.r.funct3) { case FUNCT3_ADD: { /* addw. */ i32 result = (i32)(rs1_32 + rs2_32); cpu->regs[ins.r.rd] = (u64)(i64)result; break; } case FUNCT3_SLL: { /* sllw. */ i32 result = (i32)(rs1_32 << (rs2_32 & 0x1F)); cpu->regs[ins.r.rd] = (u64)(i64)result; break; } case FUNCT3_SRL: { /* srlw. */ i32 result = (i32)(rs1_32 >> (rs2_32 & 0x1F)); cpu->regs[ins.r.rd] = (u64)(i64)result; break; } } break; case FUNCT7_SUB: switch (ins.r.funct3) { case FUNCT3_ADD: { /* subw. */ i32 result = (i32)(rs1_32 - rs2_32); cpu->regs[ins.r.rd] = (u64)(i64)result; break; } case FUNCT3_SRL: { /* sraw. */ i32 result = (i32)rs1_32 >> (rs2_32 & 0x1F); cpu->regs[ins.r.rd] = (u64)(i64)result; break; } } break; case FUNCT7_MUL: switch (ins.r.funct3) { case FUNCT3_ADD: { /* mulw. */ i32 result = (i32)(rs1_32 * rs2_32); cpu->regs[ins.r.rd] = (u64)(i64)result; break; } case FUNCT3_XOR: { /* divw. */ if (rs2_32 != 0) { i32 result = (i32)rs1_32 / (i32)rs2_32; cpu->regs[ins.r.rd] = (u64)(i64)result; } else { cpu->regs[ins.r.rd] = (u64)(i64)(i32)-1; } break; } case FUNCT3_SRL: { /* divuw. */ if (rs2_32 != 0) { i32 result = (i32)(rs1_32 / rs2_32); cpu->regs[ins.r.rd] = (u64)(i64)result; } else { cpu->regs[ins.r.rd] = (u64)(i64)(i32)-1; } break; } case FUNCT3_OR: { /* remw. */ if (rs2_32 != 0) { i32 result = (i32)rs1_32 % (i32)rs2_32; cpu->regs[ins.r.rd] = (u64)(i64)result; } else { cpu->regs[ins.r.rd] = (u64)(i64)(i32)rs1_32; } break; } case FUNCT3_AND: { /* remuw. */ if (rs2_32 != 0) { i32 result = (i32)(rs1_32 % rs2_32); cpu->regs[ins.r.rd] = (u64)(i64)result; } else { cpu->regs[ins.r.rd] = (u64)(i64)(i32)rs1_32; } break; } } break; } break; } case OP_SYSTEM: { u32 funct12 = ins.i.imm_11_0; if (funct12 == 0) { u32 syscall_num = (u32)cpu->regs[A7]; switch (syscall_num) { case 64: { /* write. */ int guest_fd = (int)cpu->regs[A0]; u64 addr = cpu->regs[A1]; u64 count = cpu->regs[A2]; if (addr + count > g_opts.memory_size || addr > (u64)g_opts.memory_size) { printf( "sys_write out of bounds: addr=%016llx len=%llu\n", (unsigned long long)addr, (unsigned long long)count ); cpu->running = false; emit_fault_diagnostics(cpu, executed_pc); break; } ssize_t written = 0; int host_fd = guest_fd_table_get(guest_fd); if (host_fd >= 0 && count > 0) { written = write(host_fd, &memory[(u32)addr], (u32)count); if (written < 0) { written = 0; } } cpu->regs[A0] = (u64)written; break; } case 63: { /* read. */ int guest_fd = (int)cpu->regs[A0]; u64 addr = cpu->regs[A1]; u64 count = cpu->regs[A2]; if (addr + count > g_opts.memory_size || addr > (u64)g_opts.memory_size) { printf( "sys_read out of bounds: addr=%016llx len=%llu\n", (unsigned long long)addr, (unsigned long long)count ); cpu->running = false; emit_fault_diagnostics(cpu, executed_pc); break; } ssize_t read_bytes = 0; int host_fd = guest_fd_table_get(guest_fd); if (host_fd >= 0 && count > 0) { read_bytes = read(host_fd, &memory[(u32)addr], (u32)count); if (read_bytes < 0) { read_bytes = 0; } } cpu->regs[A0] = (u64)read_bytes; break; } case 93: { /* exit. */ cpu->running = false; break; } case 56: { /* openat. */ u64 pathname_addr = cpu->regs[A1]; i32 flags = (i32)cpu->regs[A2]; if (pathname_addr > (u64)g_opts.memory_size) { cpu->regs[A0] = (u64)(i64)(i32)-1; break; } cpu->regs[A0] = (u64)(i64)(i32)ecall_openat((u32)pathname_addr, flags); break; } case 57: { /* close. */ i32 guest_fd = (i32)cpu->regs[A0]; cpu->regs[A0] = (u64)(i64)ecall_close(guest_fd); break; } default: cpu->regs[A0] = (u32)syscall_num; break; } } else if (funct12 == 1) { /* Look up source location for this EBREAK. PC in the debug * file is relative to program start, so subtract base. */ u32 relative_pc = executed_pc - program_base; struct debug_entry *entry = debug_lookup(relative_pc); printf("\n%sRuntime error (EBREAK)%s", COLOR_BOLD_RED, COLOR_RESET); if (entry) { u32 line = line_from_offset(entry->file, entry->offset); printf( " at %s%s:%d%s", COLOR_CYAN, entry->file, line, COLOR_RESET ); } printf("\n"); cpu->running = false; cpu->regs[A0] = EBREAK_EXIT_CODE; cpu->ebreak = true; emit_fault_diagnostics(cpu, executed_pc); cpu->faulted = false; } else { printf( "\n%sUnknown system instruction (imm=%08x)%s\n", COLOR_BOLD_RED, funct12, COLOR_RESET ); cpu->running = false; emit_fault_diagnostics(cpu, executed_pc); } break; } case OP_FENCE: /* Memory barriers are not implemented. */ break; default: printf("Unknown opcode %02x at PC=%08x\n", opcode, cpu->pc); cpu->running = false; emit_fault_diagnostics(cpu, executed_pc); break; } /* Register x0 is hardwired to zero. */ cpu->regs[ZERO] = 0; cpu->pc = pc_next; if (cpu->running) { validate_stack_register(cpu, SP, "SP", executed_pc, false); if (cpu->running) validate_stack_register(cpu, FP, "FP", executed_pc, true); } /* Track last executed PC for trace mode. */ if (headless && g_opts.trace_print_instructions) last_executed_pc = executed_pc; } /* Render the instructions column. */ static void ui_render_instructions( struct cpu *cpu, int col, int width, int height ) { int row = 1; u32 program_start_idx = program_base / INSTR_SIZE; u32 program_end_idx = program_start_idx + cpu->programsize; /* Calculate PC index in program. */ u32 pc_idx = cpu->pc / sizeof(instr_t); if (pc_idx < program_start_idx || pc_idx >= program_end_idx) pc_idx = program_start_idx; /* Calculate first instruction to display, centering PC if possible. */ i32 progstart = (i32)pc_idx - height / 2; i32 min_start = (i32)program_start_idx; i32 max_start = (i32)program_end_idx - height; if (max_start < min_start) max_start = min_start; if (progstart < min_start) progstart = min_start; if (progstart > max_start) progstart = max_start; printf(TTY_GOTO_RC, row++, col); printf(" INSTRUCTIONS"); for (int i = 0; i < height; i++) { u32 idx = (u32)progstart + i; if (idx >= program_end_idx) break; printf(TTY_GOTO_RC, row + i + 1, col); char istr[MAX_INSTR_STR_LEN] = { 0 }; int len = sprint_instr(cpu->program[idx], istr, true); if (idx == pc_idx) { /* Highlight current instruction. */ printf("%s>%s %04x: ", COLOR_GREEN, COLOR_RESET, idx * INSTR_SIZE); } else { printf(" %s%04x:%s ", COLOR_GREY, idx * INSTR_SIZE, COLOR_RESET); } printf("%s", istr); printf("%-*s", width - len - 8, ""); } } /* Render the registers column. */ static void ui_render_registers( struct cpu *cpu, enum display display, int col, int height ) { int row = 1; printf(TTY_GOTO_RC, row++, col); printf("REGISTERS"); int reg_count = sizeof(registers_displayed) / sizeof(registers_displayed[0]); if (reg_count > height) reg_count = height; for (int i = 0; i < reg_count; i++) { printf(TTY_GOTO_RC, row + i + 1, col); reg_t r = registers_displayed[i]; const char *reg_color = (r == cpu->modified) ? COLOR_BOLD_BLUE : COLOR_BLUE; u64 reg_value = cpu->regs[r]; bool is_stack_addr = reg_value <= (u64)UINT32_MAX && stack_contains((u32)reg_value); /* Always show registers that contain stack addresses in hex. */ printf("%s%-2s%s = ", COLOR_GREEN, reg_names[r], COLOR_RESET); if (display == DISPLAY_HEX || is_stack_addr) { printf("%s0x%08x%s", reg_color, (u32)reg_value, COLOR_RESET); } else { printf("%s%-10d%s", reg_color, (i32)reg_value, COLOR_RESET); } } } /* Render the stack column. */ static void ui_render_stack( struct cpu *cpu, enum display display, int col, int height ) { int row = 1; printf(TTY_GOTO_RC, row++, col); printf(" STACK FRAME"); assert(cpu->regs[SP] <= memory_top() && cpu->regs[FP] <= memory_top()); u32 fp = (u32)cpu->regs[FP]; u32 sp = (u32)cpu->regs[SP]; u32 rows = (u32)height; if (rows > STACK_DISPLAY_WORDS) rows = STACK_DISPLAY_WORDS; if (rows == 0) return; u32 top = stack_usable_top(); u32 bottom = stack_usable_bottom(); if (sp > top) sp = top; if (fp > top) fp = top; u32 used_bytes = (top >= sp) ? (top - sp) : 0; u32 total_words = (used_bytes / WORD_SIZE) + 1; u32 frame_words = total_words; if (frame_words > rows) frame_words = rows; if (frame_words == 0) return; u32 start; if (frame_words == total_words) { start = top; } else { start = sp + (frame_words - 1) * WORD_SIZE; if (start > top) start = top; } if (start < bottom) start = bottom; u32 addr = start; i32 offset = (i32)(start - sp); for (u32 i = 0; i < frame_words; i++) { if (addr < bottom) break; assert(addr <= memory_top()); printf(TTY_GOTO_RC, row + i + 1, col); /* Mark SP and FP positions. */ const char *marker = " "; if (addr == sp) { marker = "sp"; } else if (addr == fp) { marker = "fp"; } u32 word = memory_load_u32(addr); char offset_buf[6]; if (addr == sp) { memcpy(offset_buf, " ", 5); } else { snprintf(offset_buf, sizeof(offset_buf), "%+4d", offset); } printf( "%s%s %s%s%s %08x: ", COLOR_GREEN, marker, COLOR_GREY, offset_buf, COLOR_RESET, addr ); bool is_stack_addr = stack_contains(word); if (display == DISPLAY_HEX || is_stack_addr) { printf("%s0x%08x%s", COLOR_BLUE, word, COLOR_RESET); } else { printf("%s%-10d%s", COLOR_BLUE, (i32)word, COLOR_RESET); } if (addr < WORD_SIZE) break; addr -= WORD_SIZE; offset -= WORD_SIZE; } } /* Render the full debugger TUI. */ static void ui_render(struct cpu *cpu, enum display display) { printf(TTY_CLEAR); struct termsize tsize = termsize(); /* Enforce a minimum display size. */ if (tsize.cols < 60) tsize.cols = 60; if (tsize.rows < 15) tsize.rows = 15; /* Column layout: 40% instructions, 20% registers, rest for stack. */ int instr_width = (tsize.cols * 2) / 5; int reg_width = tsize.cols / 5; int instr_col = 1; int reg_col = instr_col + instr_width + 2; int stack_col = reg_col + reg_width + 2; int display_height = tsize.rows - FOOTER_HEIGHT - HEADER_HEIGHT; if (display_height > MAX_INSTR_DISPLAY) display_height = MAX_INSTR_DISPLAY; if (display_height <= 0) display_height = 1; ui_render_instructions(cpu, instr_col, instr_width, display_height); ui_render_registers(cpu, display, reg_col, display_height); ui_render_stack(cpu, display, stack_col, display_height); printf(TTY_GOTO_RC, display_height + FOOTER_HEIGHT, 1); printf( "%sPress `j` to step forward, `k` to step backward, `q` to quit,\n" "`d` to toggle decimal display, `r` to reset program.%s ", COLOR_GREY, COLOR_RESET ); } /* Set up the terminal for interactive mode, saving the original settings. */ static void term_init(struct termios *oldterm) { struct termios term; tcgetattr(STDIN_FILENO, oldterm); term = *oldterm; term.c_lflag &= ~(ICANON | ECHO); tcsetattr(STDIN_FILENO, TCSANOW, &term); } /* Restore terminal settings. */ static void term_restore(struct termios *old) { tcsetattr(STDIN_FILENO, TCSANOW, old); } int main(int argc, char *argv[]) { struct cpu cpu; enum display display = DISPLAY_DEC; struct cli_config cli = { 0 }; if (!parse_cli_args(argc, argv, &cli)) return 1; bool headless = cli.headless; g_opts.trace_headless = headless; cpu_init(&cpu); program_init(&cpu, cli.program_path); int prog_argc = argc - cli.arg_index; char **prog_argv = &argv[cli.arg_index]; prepare_env(&cpu, prog_argc, prog_argv); if (headless) { u64 max_steps = g_opts.headless_max_steps; u64 steps = 0; /* Try to initialise the JIT for headless mode. Falls back to the * interpreter automatically when JIT is disabled, unavailable, * or when the code cache fills up. */ static struct jit_state jit; bool use_jit = false; if (!g_opts.jit_disabled && !g_opts.trace_enabled && !g_opts.trace_print_instructions && !g_opts.watch_enabled) { use_jit = jit_init(&jit); } if (use_jit) { /* ---- JIT execution loop ---- */ while (cpu.running && steps < max_steps) { struct jit_block *block = jit_get_block( &jit, cpu.pc, memory, program_base, program_bytes ); if (!block) { /* Cache full or compilation error -- fall back to * interpreter for remainder. */ while (cpu.running && steps++ < max_steps) { cpu_execute(&cpu, display, true); } break; } u32 next_pc = 0; int exit_reason = jit_exec_block(block, cpu.regs, memory, &next_pc); steps += block->insn_count; jit.blocks_executed++; jit.insns_executed += block->insn_count; switch (exit_reason) { case JIT_EXIT_BRANCH: case JIT_EXIT_CHAIN: cpu.pc = next_pc; break; case JIT_EXIT_RET: cpu.running = false; break; default: /* ECALL, EBREAK, FAULT -- interpreter handles it. * The instruction is already counted above, * so don't increment steps again. */ cpu.pc = next_pc; cpu_execute(&cpu, display, true); break; } } jit_destroy(&jit); } else { /* ---- Interpreter-only loop ---- */ while (cpu.running && steps++ < max_steps) { cpu_execute(&cpu, display, true); } } if (cpu.running) { fprintf( stderr, "program did not terminate within %zu steps\n", (size_t)max_steps ); return -1; } if (cpu.faulted) { fprintf(stderr, "program terminated due to runtime fault\n"); return -1; } if (g_opts.count_instructions) { fprintf( stderr, "Processed %llu instructions\n", (unsigned long long)steps ); } return (int)cpu.regs[A0]; } struct termios oldterm; term_init(&oldterm); snapshot_init(&cpu); for (;;) { if (cpu.running) ui_render(&cpu, display); int ch = getchar(); if (ch == 'q' || ch == 'Q') { printf("\n"); break; } else if (ch == 'd' || ch == 'D') { /* Toggle display mode. */ display = (display == DISPLAY_HEX) ? DISPLAY_DEC : DISPLAY_HEX; } else if (ch == 'r' || ch == 'R') { /* Reset program and state. */ cpu_reset(&cpu); snapshot_init(&cpu); } else if (ch == 'g' || ch == 'G') { /* Toggle stack guard. */ toggle_stack_guard(&cpu); } else if (ch == 'j' && cpu.running) { /* Step forward. */ cpu_execute(&cpu, display, false); snapshot_save(&cpu); } else if (ch == 'k' && cpu.pc > 0) { /* Step backward. */ bool restored = snapshot_restore(&cpu); if (restored) { cpu.running = true; } else { printf( "\n%sNo more history to go back to.%s\n", COLOR_BOLD_RED, COLOR_RESET ); } } } term_restore(&oldterm); return 0; }