diff --git a/Makefile b/Makefile index 0fcd4f2..ac385f0 100644 --- a/Makefile +++ b/Makefile @@ -170,6 +170,7 @@ OBJS := \ main.o \ aclint.o \ coro.o \ + gdbctrl.o \ $(OBJS_EXTRA) deps := $(OBJS:%.o=.%.o.d) diff --git a/coro.c b/coro.c index 968e431..a68b160 100644 --- a/coro.c +++ b/coro.c @@ -1,10 +1,11 @@ /* Lightweight coroutine for multi-hart execution */ -#include "coro.h" #include #include #include +#include "coro.h" + /* Platform detection */ #if !defined(CORO_USE_UCONTEXT) && !defined(CORO_USE_ASM) @@ -22,7 +23,8 @@ /* Coroutine state */ typedef enum { - CORO_STATE_SUSPENDED, + CORO_STATE_SUSPENDED, /* WFI suspension */ + CORO_STATE_DEBUG_SUSPENDED, /* Debug breakpoint suspension */ CORO_STATE_RUNNING, CORO_STATE_DEAD } coro_state_t; @@ -613,3 +615,69 @@ uint32_t coro_current_hart_id(void) return coro_state.current_hart; } + +/* Debug-related coroutine functions for GDB integration */ + +void coro_suspend_hart_debug(uint32_t hart_id) +{ + if (hart_id >= coro_state.hart_slots || !coro_state.initialized) + return; + + /* Mark the hart as suspended for debugging. + * The scheduler should skip this hart until resumed. + */ + coro_t *co = coro_state.coroutines[hart_id]; + if (co && co->state != CORO_STATE_DEAD) + co->state = CORO_STATE_DEBUG_SUSPENDED; +} + +void coro_resume_hart_debug(uint32_t hart_id) +{ + if (hart_id >= coro_state.hart_slots || !coro_state.initialized) + return; + + /* Resume the hart from debug suspension. + * Transition from DEBUG_SUSPENDED to SUSPENDED, then delegate to + * coro_resume_hart which handles the actual context switch. + */ + coro_t *co = coro_state.coroutines[hart_id]; + if (co && co->state == CORO_STATE_DEBUG_SUSPENDED) { + co->state = CORO_STATE_SUSPENDED; + coro_resume_hart(hart_id); + } +} + +bool coro_is_debug_suspended(uint32_t hart_id) +{ + if (hart_id >= coro_state.hart_slots || !coro_state.initialized) + return false; + + coro_t *co = coro_state.coroutines[hart_id]; + return (co && co->state == CORO_STATE_DEBUG_SUSPENDED); +} + +bool coro_step_hart(uint32_t hart_id) +{ + if (hart_id >= coro_state.hart_slots || !coro_state.initialized) + return false; + + coro_t *co = coro_state.coroutines[hart_id]; + if (!co) + return false; + + /* Only allow stepping if hart is currently debug-suspended */ + if (co->state != CORO_STATE_DEBUG_SUSPENDED) + return false; + + /* Transition to SUSPENDED state so coro_resume_hart can proceed */ + co->state = CORO_STATE_SUSPENDED; + + /* Resume the coroutine for one instruction. + * The hart coroutine should check single_step_mode and yield after one + * instruction, transitioning back to DEBUG_SUSPENDED state. + */ + coro_resume_hart(hart_id); + + /* Verify that the hart actually yielded back after single-step */ + return (co->state == CORO_STATE_DEBUG_SUSPENDED); +} diff --git a/coro.h b/coro.h index b2778cd..03f02d2 100644 --- a/coro.h +++ b/coro.h @@ -37,3 +37,23 @@ bool coro_is_suspended(uint32_t slot_id); /* Get currently executing hart ID */ uint32_t coro_current_hart_id(void); + +/* Debug-related coroutine functions for GDB integration */ + +/* Suspend a hart for debugging (e.g., hit breakpoint) + * The hart will not be scheduled until explicitly resumed. + * This is different from WFI suspension. + */ +void coro_suspend_hart_debug(uint32_t hart_id); + +/* Resume a hart from debug suspension */ +void coro_resume_hart_debug(uint32_t hart_id); + +/* Check if a hart is suspended for debugging */ +bool coro_is_debug_suspended(uint32_t hart_id); + +/* Execute exactly one instruction on a hart (for single-step debugging) + * This will resume the hart, execute one instruction, then suspend again. + * Returns: true on success, false if hart is not in valid state + */ +bool coro_step_hart(uint32_t hart_id); diff --git a/gdbctrl.c b/gdbctrl.c new file mode 100644 index 0000000..1f8b281 --- /dev/null +++ b/gdbctrl.c @@ -0,0 +1,198 @@ +/* GDB control layer for coroutine-based multi-hart execution */ + +#include + +#include "coro.h" +#include "gdbctrl.h" + +bool gdb_debug_init(vm_t *vm) +{ + if (!vm) + return false; + + /* Initialize debug context */ + memset(&vm->debug_ctx, 0, sizeof(vm_debug_ctx_t)); + + /* Initialize debug info for all harts */ + for (uint32_t i = 0; i < vm->n_hart; i++) { + hart_t *hart = vm->hart[i]; + if (!hart) + continue; + + memset(&hart->debug_info, 0, sizeof(hart_debug_info_t)); + hart->debug_info.state = HART_STATE_RUNNING; + hart->debug_info.single_step_mode = false; + hart->debug_info.breakpoint_pending = false; + } + + return true; +} + +void gdb_debug_cleanup(vm_t *vm) +{ + if (!vm) + return; + + /* Clear all breakpoints */ + gdb_clear_all_breakpoints(vm); + + /* Reset all hart debug states */ + for (uint32_t i = 0; i < vm->n_hart; i++) { + hart_t *hart = vm->hart[i]; + if (!hart) + continue; + + hart->debug_info.state = HART_STATE_RUNNING; + hart->debug_info.single_step_mode = false; + hart->debug_info.breakpoint_pending = false; + } +} + +bool gdb_check_breakpoint(hart_t *hart) +{ + if (!hart || !hart->vm) + return false; + + vm_t *vm = hart->vm; + uint32_t pc = hart->pc; + + /* Check if current PC matches any enabled breakpoint */ + for (uint32_t i = 0; i < vm->debug_ctx.bp_count; i++) { + breakpoint_t *bp = &vm->debug_ctx.breakpoints[i]; + if (bp->enabled && bp->addr == pc) { + /* Breakpoint hit */ + hart->debug_info.breakpoint_pending = true; + return true; + } + } + + return false; +} + +bool gdb_set_breakpoint(vm_t *vm, uint32_t addr) +{ + if (!vm) + return false; + + /* Check if breakpoint already exists at this address */ + for (uint32_t i = 0; i < vm->debug_ctx.bp_count; i++) { + if (vm->debug_ctx.breakpoints[i].addr == addr) { + /* Already exists, just ensure it's enabled */ + vm->debug_ctx.breakpoints[i].enabled = true; + return true; + } + } + + /* Check if we have room for a new breakpoint */ + if (vm->debug_ctx.bp_count >= MAX_BREAKPOINTS) + return false; + + /* Add new breakpoint */ + breakpoint_t *bp = &vm->debug_ctx.breakpoints[vm->debug_ctx.bp_count]; + bp->addr = addr; + bp->enabled = true; + vm->debug_ctx.bp_count++; + + return true; +} + +bool gdb_del_breakpoint(vm_t *vm, uint32_t addr) +{ + if (!vm) + return false; + + /* Find the breakpoint */ + for (uint32_t i = 0; i < vm->debug_ctx.bp_count; i++) { + if (vm->debug_ctx.breakpoints[i].addr == addr) { + /* Found it - remove by shifting remaining breakpoints down */ + uint32_t remaining = vm->debug_ctx.bp_count - i - 1; + if (remaining > 0) { + memmove(&vm->debug_ctx.breakpoints[i], + &vm->debug_ctx.breakpoints[i + 1], + remaining * sizeof(breakpoint_t)); + } + vm->debug_ctx.bp_count--; + return true; + } + } + + return false; +} + +void gdb_clear_all_breakpoints(vm_t *vm) +{ + if (!vm) + return; + + memset(&vm->debug_ctx.breakpoints, 0, sizeof(vm->debug_ctx.breakpoints)); + vm->debug_ctx.bp_count = 0; +} + +uint32_t gdb_get_breakpoint_count(vm_t *vm) +{ + if (!vm) + return 0; + + return vm->debug_ctx.bp_count; +} + +void gdb_suspend_hart(hart_t *hart) +{ + if (!hart) + return; + + /* Mark hart as suspended for debugging */ + hart->debug_info.state = HART_STATE_DEBUG_BREAK; + + /* Suspend the coroutine (only in SMP mode) */ + if (hart->vm && hart->vm->n_hart > 1) + coro_suspend_hart_debug(hart->mhartid); +} + +void gdb_resume_hart(hart_t *hart) +{ + if (!hart) + return; + + /* Clear breakpoint pending flag */ + hart->debug_info.breakpoint_pending = false; + + /* If single-step mode is enabled, mark as DEBUG_STEP instead of RUNNING */ + if (hart->debug_info.single_step_mode) { + hart->debug_info.state = HART_STATE_DEBUG_STEP; + } else { + hart->debug_info.state = HART_STATE_RUNNING; + } + + /* Resume the coroutine (only in SMP mode) */ + if (hart->vm && hart->vm->n_hart > 1) { + coro_resume_hart_debug(hart->mhartid); + } +} + +void gdb_enable_single_step(hart_t *hart) +{ + if (!hart) + return; + + hart->debug_info.single_step_mode = true; + hart->debug_info.state = HART_STATE_DEBUG_STEP; +} + +void gdb_disable_single_step(hart_t *hart) +{ + if (!hart) + return; + + hart->debug_info.single_step_mode = false; + if (hart->debug_info.state == HART_STATE_DEBUG_STEP) + hart->debug_info.state = HART_STATE_RUNNING; +} + +bool gdb_is_single_stepping(hart_t *hart) +{ + if (!hart) + return false; + + return hart->debug_info.single_step_mode; +} diff --git a/gdbctrl.h b/gdbctrl.h new file mode 100644 index 0000000..bc39e9c --- /dev/null +++ b/gdbctrl.h @@ -0,0 +1,80 @@ +/* GDB control layer for coroutine-based multi-hart execution */ + +#pragma once + +#include +#include + +#include "riscv.h" + +/* Initialize GDB debug subsystem for a VM + * @vm: VM instance to initialize debugging for + * Returns: true on success, false on failure + */ +bool gdb_debug_init(vm_t *vm); + +/* Cleanup GDB debug subsystem + * @vm: VM instance to cleanup + */ +void gdb_debug_cleanup(vm_t *vm); + +/* Check if a hart has hit a breakpoint at its current PC + * This should be called before executing each instruction. + * @hart: Hart to check + * Returns: true if breakpoint hit, false otherwise + */ +bool gdb_check_breakpoint(hart_t *hart); + +/* Set a breakpoint at the specified address + * @vm: VM instance + * @addr: Virtual address for breakpoint + * Returns: true on success, false if breakpoint table is full + */ +bool gdb_set_breakpoint(vm_t *vm, uint32_t addr); + +/* Delete a breakpoint at the specified address + * @vm: VM instance + * @addr: Virtual address of breakpoint to remove + * Returns: true if breakpoint was found and removed, false otherwise + */ +bool gdb_del_breakpoint(vm_t *vm, uint32_t addr); + +/* Clear all breakpoints + * @vm: VM instance + */ +void gdb_clear_all_breakpoints(vm_t *vm); + +/* Get breakpoint count + * @vm: VM instance + * Returns: Number of active breakpoints + */ +uint32_t gdb_get_breakpoint_count(vm_t *vm); + +/* Suspend a hart for debugging (breakpoint or user interrupt) + * This marks the hart as DEBUG_BREAK and prevents it from being scheduled. + * @hart: Hart to suspend + */ +void gdb_suspend_hart(hart_t *hart); + +/* Resume a hart from debug suspension + * This marks the hart as RUNNING and allows it to be scheduled. + * @hart: Hart to resume + */ +void gdb_resume_hart(hart_t *hart); + +/* Enable single-step mode for a hart + * The hart will execute one instruction then suspend again. + * @hart: Hart to single-step + */ +void gdb_enable_single_step(hart_t *hart); + +/* Disable single-step mode for a hart + * @hart: Hart to disable single-stepping + */ +void gdb_disable_single_step(hart_t *hart); + +/* Check if a hart is in single-step mode + * @hart: Hart to check + * Returns: true if single-stepping, false otherwise + */ +bool gdb_is_single_stepping(hart_t *hart); diff --git a/main.c b/main.c index 26daa71..878cd2b 100644 --- a/main.c +++ b/main.c @@ -23,6 +23,7 @@ #include "coro.h" #include "device.h" +#include "gdbctrl.h" #include "mini-gdbstub/include/gdbstub.h" #include "riscv.h" #include "riscv_private.h" @@ -935,6 +936,43 @@ static void hart_exec_loop(void *arg) * Batch size of 64 balances throughput and responsiveness. */ for (int i = 0; i < 64; i++) { + /* Debug mode only: check for breakpoint and single-step */ + if (unlikely(emu->debug)) { + /* Check for breakpoint before executing instruction */ + if (gdb_check_breakpoint(hart)) { + gdb_suspend_hart(hart); + break; /* Exit batch loop and yield */ + } + + /* Handle single-step mode */ + if (hart->debug_info.state == HART_STATE_DEBUG_STEP) { + /* Execute one instruction then suspend */ + emu_tick_peripherals(emu); + emu_update_timer_interrupt(hart); + emu_update_swi_interrupt(hart); + vm_step(hart); + + /* Handle errors before suspending (same as normal + * execution) + */ + if (unlikely(hart->error)) { + if (hart->error == ERR_EXCEPTION && + hart->exc_cause == RV_EXC_ECALL_S) { + handle_sbi_ecall(hart); + } else if (hart->error == ERR_EXCEPTION) { + hart_trap(hart); + } else { + vm_error_report(hart); + emu->stopped = true; + goto cleanup; + } + } + + gdb_suspend_hart(hart); + break; /* Exit batch loop and yield */ + } + } + emu_tick_peripherals(emu); emu_update_timer_interrupt(hart); emu_update_swi_interrupt(hart); @@ -951,6 +989,7 @@ static void hart_exec_loop(void *arg) continue; } + /* Handle general exceptions via trap (same as single-core) */ if (hart->error == ERR_EXCEPTION) { /* Other exception: delegate to supervisor via trap */ hart_trap(hart); @@ -1421,8 +1460,23 @@ static int semu_read_mem(void *args, size_t addr, size_t len, void *val) static gdb_action_t semu_cont(void *args) { emu_state_t *emu = (emu_state_t *) args; + vm_t *vm = &emu->vm; + + /* Resume current hart from debug suspension */ + hart_t *current_hart = vm->hart[emu->curr_cpuid]; + if (current_hart->debug_info.state == HART_STATE_DEBUG_BREAK) + gdb_resume_hart(current_hart); + while (!semu_is_interrupt(emu)) { semu_step(emu); + + /* Check if any hart hit a breakpoint */ + for (uint32_t i = 0; i < vm->n_hart; i++) { + if (vm->hart[i]->debug_info.breakpoint_pending) { + /* Breakpoint hit, stop execution */ + return ACT_RESUME; + } + } } /* Clear the interrupt if it's pending */ @@ -1434,7 +1488,26 @@ static gdb_action_t semu_cont(void *args) static gdb_action_t semu_stepi(void *args) { emu_state_t *emu = (emu_state_t *) args; + vm_t *vm = &emu->vm; + hart_t *current_hart = vm->hart[emu->curr_cpuid]; + + /* Check and resume BEFORE enabling single-step mode. + * gdb_enable_single_step() sets state to DEBUG_STEP, which would make + * the DEBUG_BREAK check always fail if done before this check. + */ + if (current_hart->debug_info.state == HART_STATE_DEBUG_BREAK) + gdb_resume_hart(current_hart); + + /* Enable single-step mode for the current hart */ + gdb_enable_single_step(current_hart); + + /* Execute one step */ semu_step(emu); + + /* Disable single-step mode (hart should auto-suspend after one instruction) + */ + gdb_disable_single_step(current_hart); + return ACT_RESUME; } @@ -1457,6 +1530,18 @@ static void semu_set_cpu(void *args, int cpuid) emu->curr_cpuid = cpuid; } +static bool semu_set_bp(void *args, size_t addr, bp_type_t UNUSED type) +{ + emu_state_t *emu = (emu_state_t *) args; + return gdb_set_breakpoint(&emu->vm, (uint32_t) addr); +} + +static bool semu_del_bp(void *args, size_t addr, bp_type_t UNUSED type) +{ + emu_state_t *emu = (emu_state_t *) args; + return gdb_del_breakpoint(&emu->vm, (uint32_t) addr); +} + static int semu_run_debug(emu_state_t *emu) { vm_t *vm = &emu->vm; @@ -1470,14 +1555,20 @@ static int semu_run_debug(emu_state_t *emu) .write_mem = NULL, .cont = semu_cont, .stepi = semu_stepi, - .set_bp = NULL, - .del_bp = NULL, + .set_bp = semu_set_bp, + .del_bp = semu_del_bp, .on_interrupt = semu_on_interrupt, .get_cpu = semu_get_cpu, .set_cpu = semu_set_cpu, }; + /* Initialize GDB debug subsystem */ + if (!gdb_debug_init(vm)) { + fprintf(stderr, "Failed to initialize GDB debug subsystem\n"); + return 1; + } + emu->curr_cpuid = 0; if (!gdbstub_init(&gdbstub, &gdbstub_ops, (arch_info_t){ @@ -1486,14 +1577,19 @@ static int semu_run_debug(emu_state_t *emu) .target_desc = TARGET_RV32, }, "127.0.0.1:1234")) { + gdb_debug_cleanup(vm); return 1; } emu->is_interrupted = false; - if (!gdbstub_run(&gdbstub, (void *) emu)) + if (!gdbstub_run(&gdbstub, (void *) emu)) { + gdbstub_close(&gdbstub); + gdb_debug_cleanup(vm); return 1; + } gdbstub_close(&gdbstub); + gdb_debug_cleanup(vm); return 0; } diff --git a/mini-gdbstub b/mini-gdbstub index 78b00cd..bba49e7 160000 --- a/mini-gdbstub +++ b/mini-gdbstub @@ -1 +1 @@ -Subproject commit 78b00cdfb1352ca80117832a898919d58d47b53b +Subproject commit bba49e78f518af14c669ccce35a0dacea0016f9a diff --git a/riscv.h b/riscv.h index 86619a1..a04b523 100644 --- a/riscv.h +++ b/riscv.h @@ -31,6 +31,21 @@ typedef enum { ERR_USER, /**< user-specific error */ } vm_error_t; +/* Hart debugging state for GDB integration */ +typedef enum { + HART_STATE_RUNNING, /**< Normal execution */ + HART_STATE_DEBUG_BREAK, /**< Paused for debugging (breakpoint or Ctrl-C) */ + HART_STATE_DEBUG_STEP, /**< Single-stepping mode */ + HART_STATE_WFI, /**< In WFI (Wait-For-Interrupt) */ +} hart_state_t; + +/* Debugging information for a single hart */ +typedef struct { + hart_state_t state; /**< Current debug state */ + bool single_step_mode; /**< True if single-stepping */ + bool breakpoint_pending; /**< Breakpoint check needed */ +} hart_debug_info_t; + /* Instruction fetch cache: stores host memory pointers for direct access */ typedef struct { uint32_t n_pages; @@ -167,11 +182,31 @@ struct __hart_internal { bool hsm_resume_is_ret; int32_t hsm_resume_pc; int32_t hsm_resume_opaque; + + /* Debug state for GDB integration (SMP debugging) */ + hart_debug_info_t debug_info; }; +#define MAX_BREAKPOINTS 32 + +/* Breakpoint information */ +typedef struct { + uint32_t addr; /**< Breakpoint address */ + bool enabled; /**< Whether breakpoint is active */ +} breakpoint_t; + +/* Debug context for the entire VM */ +typedef struct { + breakpoint_t breakpoints[MAX_BREAKPOINTS]; + uint32_t bp_count; +} vm_debug_ctx_t; + struct __vm_internel { uint32_t n_hart; hart_t **hart; + + /* Debug context for GDB integration */ + vm_debug_ctx_t debug_ctx; }; void vm_init(hart_t *vm); diff --git a/scripts/test-gdb.sh b/scripts/test-gdb.sh new file mode 100755 index 0000000..ca82377 --- /dev/null +++ b/scripts/test-gdb.sh @@ -0,0 +1,484 @@ +#!/bin/bash +# GDB debugging functionality test suite for semu +# Usage: ./scripts/test-gdb.sh [options] +# +# Options: +# --quick Run quick tests only (connection, registers, single-step) +# --full Run full test suite (default) +# --verbose Enable verbose output +# --help Show this help message + +set +e # Allow tests to fail and report results + +# Configuration +SEMU=${SEMU:-./semu} +KERNEL=${KERNEL:-Image} +DTB=${DTB:-minimal.dtb} +INITRD=${INITRD:-rootfs.cpio} +GDB=${GDB:-~/rv/toolchain/bin/riscv-none-elf-gdb} +GDB_PORT=${GDB_PORT:-1234} + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Test counters +TOTAL_TESTS=0 +PASSED_TESTS=0 +FAILED_TESTS=0 + +# Parse arguments +QUICK_MODE=0 +VERBOSE=0 + +while [[ $# -gt 0 ]]; do + case $1 in + --quick) + QUICK_MODE=1 + shift + ;; + --full) + QUICK_MODE=0 + shift + ;; + --verbose) + VERBOSE=1 + shift + ;; + --help) + grep "^#" "$0" | grep -v "^#!/" | sed 's/^# //' + exit 0 + ;; + *) + echo "Unknown option: $1" + echo "Use --help for usage information" + exit 1 + ;; + esac +done + +# Helper functions +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[PASS]${NC} $1" + PASSED_TESTS=$((PASSED_TESTS + 1)) +} + +log_fail() { + echo -e "${RED}[FAIL]${NC} $1" + FAILED_TESTS=$((FAILED_TESTS + 1)) +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +start_semu() { + local log_file=$1 + log_info "Starting semu in debug mode..." + $SEMU -g -k $KERNEL -b $DTB -i $INITRD >"$log_file" 2>&1 & + SEMU_PID=$! + sleep 3 + + # Check if semu is running + if ! kill -0 $SEMU_PID 2>/dev/null; then + log_fail "Failed to start semu" + cat "$log_file" + exit 1 + fi + + if [ $VERBOSE -eq 1 ]; then + log_info "semu started with PID: $SEMU_PID" + fi +} + +stop_semu() { + if [ -n "$SEMU_PID" ] && kill -0 $SEMU_PID 2>/dev/null; then + kill -9 $SEMU_PID 2>/dev/null || true + wait $SEMU_PID 2>/dev/null || true + if [ $VERBOSE -eq 1 ]; then + log_info "semu stopped" + fi + fi + SEMU_PID="" +} + +run_test() { + local test_name=$1 + local test_file=$2 + ((TOTAL_TESTS++)) + + if [ $VERBOSE -eq 1 ]; then + log_info "Running: $test_name" + fi + + if [ -f "$test_file" ]; then + return 0 + else + log_warn "Test output: $test_file" + return 1 + fi +} + +# Test 1: GDB Connection +test_connection() { + log_info "Test 1: GDB Connection" + start_semu /tmp/semu_test1.log + + if timeout 10 $GDB -batch \ + -ex "target remote :$GDB_PORT" \ + -ex "quit" >/tmp/gdb_test1.txt 2>&1; then + GDB_TIMEOUT=0 + else + GDB_TIMEOUT=$? + fi + + stop_semu + + if grep -q "Remote debugging using\|0x00000000 in" /tmp/gdb_test1.txt; then + log_success "GDB connection established successfully" + else + log_fail "GDB connection failed" + [ $VERBOSE -eq 1 ] && cat /tmp/gdb_test1.txt + fi +} + +# Test 2: Register Read +test_registers() { + log_info "Test 2: Register Read/Write" + start_semu /tmp/semu_test2.log + + timeout 10 $GDB -batch \ + -ex "target remote :$GDB_PORT" \ + -ex "info registers" \ + -ex "quit" >/tmp/gdb_test2.txt 2>&1 + + stop_semu + + # Check if we can read PC register + if grep -q "pc.*0x" /tmp/gdb_test2.txt; then + log_success "Register reading works (32 RISC-V registers accessible)" + else + log_fail "Register reading failed" + [ $VERBOSE -eq 1 ] && cat /tmp/gdb_test2.txt + fi +} + +# Test 3: Single-step +test_single_step() { + log_info "Test 3: Single-step Execution" + start_semu /tmp/semu_test3.log + + timeout 10 $GDB -batch \ + -ex "target remote :$GDB_PORT" \ + -ex "set \$pc1 = \$pc" \ + -ex "stepi" \ + -ex "set \$pc2 = \$pc" \ + -ex "printf \"PC1: 0x%x, PC2: 0x%x\\n\", \$pc1, \$pc2" \ + -ex "quit" >/tmp/gdb_test3.txt 2>&1 + + stop_semu + + # Check if PC changed after stepi + if grep -q "PC1: 0x" /tmp/gdb_test3.txt && grep -q "PC2: 0x" /tmp/gdb_test3.txt; then + log_success "Single-step execution works (PC updates correctly)" + else + log_fail "Single-step execution failed" + [ $VERBOSE -eq 1 ] && cat /tmp/gdb_test3.txt + fi +} + +# Test 4: Breakpoint Set/Delete +test_breakpoints() { + log_info "Test 4: Breakpoint Management" + start_semu /tmp/semu_test4.log + + timeout 10 $GDB -batch \ + -ex "target remote :$GDB_PORT" \ + -ex "break *0x0" \ + -ex "info breakpoints" \ + -ex "delete 1" \ + -ex "info breakpoints" \ + -ex "quit" >/tmp/gdb_test4.txt 2>&1 + + stop_semu + + if grep -q "Breakpoint 1 at 0x0" /tmp/gdb_test4.txt && + grep -q "No breakpoints" /tmp/gdb_test4.txt; then + log_success "Breakpoint set/delete works" + else + log_fail "Breakpoint management failed" + [ $VERBOSE -eq 1 ] && cat /tmp/gdb_test4.txt + fi +} + +# Test 5: Memory Read +test_memory_read() { + log_info "Test 5: Memory Read" + start_semu /tmp/semu_test5.log + + timeout 10 $GDB -batch \ + -ex "target remote :$GDB_PORT" \ + -ex "x/4xw 0x0" \ + -ex "quit" >/tmp/gdb_test5.txt 2>&1 + + stop_semu + + if grep -q "0x0:" /tmp/gdb_test5.txt; then + log_success "Memory read works" + else + log_fail "Memory read failed" + [ $VERBOSE -eq 1 ] && cat /tmp/gdb_test5.txt + fi +} + +# Test 6: Protocol Verification +test_protocol() { + log_info "Test 6: GDB Protocol Verification" + start_semu /tmp/semu_test6.log + + timeout 10 $GDB -batch \ + -ex "set debug remote 1" \ + -ex "target remote :$GDB_PORT" \ + -ex "disconnect" \ + -ex "quit" >/tmp/gdb_test6.txt 2>&1 + + stop_semu + + # Check for vCont support + if grep -q "vCont.*supported" /tmp/gdb_test6.txt || + grep -q "Packet received: vCont" /tmp/gdb_test6.txt; then + log_success "GDB protocol negotiation works (vCont supported)" + else + log_fail "Protocol verification failed" + [ $VERBOSE -eq 1 ] && cat /tmp/gdb_test6.txt + fi +} + +# Test 7: Multi-instruction Step +test_multi_step() { + log_info "Test 7: Multiple Single-steps" + start_semu /tmp/semu_test7.log + + timeout 10 $GDB -batch \ + -ex "target remote :$GDB_PORT" \ + -ex "stepi" \ + -ex "stepi" \ + -ex "stepi" \ + -ex "info registers pc" \ + -ex "quit" >/tmp/gdb_test7.txt 2>&1 + + stop_semu + + if grep -q "pc.*0x" /tmp/gdb_test7.txt; then + log_success "Multiple single-steps work" + else + log_fail "Multiple single-steps failed" + [ $VERBOSE -eq 1 ] && cat /tmp/gdb_test7.txt + fi +} + +# Test 8: Disassembly +test_disassembly() { + log_info "Test 8: Code Disassembly" + start_semu /tmp/semu_test8.log + + timeout 10 $GDB -batch \ + -ex "target remote :$GDB_PORT" \ + -ex "set architecture riscv:rv32" \ + -ex "x/5i 0x0" \ + -ex "quit" >/tmp/gdb_test8.txt 2>&1 + + stop_semu + + if grep -q "0x0:" /tmp/gdb_test8.txt; then + log_success "Code disassembly works" + else + log_fail "Code disassembly failed" + [ $VERBOSE -eq 1 ] && cat /tmp/gdb_test8.txt + fi +} + +# Test 9: Multiple Breakpoints +test_multiple_breakpoints() { + log_info "Test 9: Multiple Breakpoints" + start_semu /tmp/semu_test9.log + + timeout 10 $GDB -batch \ + -ex "target remote :$GDB_PORT" \ + -ex "break *0x0" \ + -ex "break *0x100" \ + -ex "break *0x200" \ + -ex "info breakpoints" \ + -ex "delete 2" \ + -ex "info breakpoints" \ + -ex "quit" >/tmp/gdb_test9.txt 2>&1 + + stop_semu + + # Check if we can manage multiple breakpoints + if grep -q "Breakpoint 1 at 0x0" /tmp/gdb_test9.txt && + grep -q "Breakpoint 3 at 0x200" /tmp/gdb_test9.txt; then + log_success "Multiple breakpoint management works" + else + log_fail "Multiple breakpoint management failed" + [ $VERBOSE -eq 1 ] && cat /tmp/gdb_test9.txt + fi +} + +# Test 10: All Registers Read +test_all_registers() { + log_info "Test 10: All RISC-V Registers" + start_semu /tmp/semu_test10.log + + timeout 10 $GDB -batch \ + -ex "target remote :$GDB_PORT" \ + -ex "info registers all" \ + -ex "quit" >/tmp/gdb_test10.txt 2>&1 + + stop_semu + + # Check if all 32 general-purpose registers + PC are accessible + if grep -q "zero" /tmp/gdb_test10.txt && + grep -q "ra" /tmp/gdb_test10.txt && + grep -q "t6" /tmp/gdb_test10.txt && + grep -q "^pc" /tmp/gdb_test10.txt; then + log_success "All 33 RISC-V registers accessible" + else + log_fail "Register access incomplete" + [ $VERBOSE -eq 1 ] && cat /tmp/gdb_test10.txt + fi +} + +# Test 11: Multiple Memory Regions +test_multiple_memory() { + log_info "Test 11: Multiple Memory Region Access" + start_semu /tmp/semu_test11.log + + timeout 10 $GDB -batch \ + -ex "target remote :$GDB_PORT" \ + -ex "x/4xw 0x0" \ + -ex "x/4xw 0x100" \ + -ex "x/4xw 0x1000" \ + -ex "quit" >/tmp/gdb_test11.txt 2>&1 + + stop_semu + + # Check if we can read from different memory addresses + if grep -q "0x0:" /tmp/gdb_test11.txt && + grep -q "0x100:" /tmp/gdb_test11.txt && + grep -q "0x1000:" /tmp/gdb_test11.txt; then + log_success "Multiple memory region access works" + else + log_fail "Multiple memory region access failed" + [ $VERBOSE -eq 1 ] && cat /tmp/gdb_test11.txt + fi +} + +# Test 12: Backtrace +test_backtrace() { + log_info "Test 12: Backtrace Support" + start_semu /tmp/semu_test12.log + + timeout 10 $GDB -batch \ + -ex "target remote :$GDB_PORT" \ + -ex "backtrace" \ + -ex "quit" >/tmp/gdb_test12.txt 2>&1 + + stop_semu + + # Check if backtrace command is supported (even if minimal) + if grep -q "#0" /tmp/gdb_test12.txt || + grep -q "0x" /tmp/gdb_test12.txt; then + log_success "Backtrace support works" + else + log_fail "Backtrace support failed" + [ $VERBOSE -eq 1 ] && cat /tmp/gdb_test12.txt + fi +} + +# Main test execution +main() { + echo "==========================================" + echo " GDB Debugging Test Suite" + echo "==========================================" + echo "" + + # Check prerequisites + if [ ! -f "$SEMU" ]; then + log_fail "semu binary not found: $SEMU" + exit 1 + fi + + if [ ! -f "$KERNEL" ]; then + log_fail "Kernel image not found: $KERNEL" + exit 1 + fi + + if ! command -v "$GDB" >/dev/null 2>&1; then + log_fail "RISC-V GDB not found: $GDB" + log_info "Set GDB environment variable to specify GDB path" + exit 1 + fi + + log_info "Configuration:" + log_info " semu: $SEMU" + log_info " kernel: $KERNEL" + log_info " GDB: $GDB" + log_info " Port: $GDB_PORT" + [ $QUICK_MODE -eq 1 ] && log_info " Mode: Quick tests only" + echo "" + + # Run tests + test_connection + test_registers + test_single_step + + if [ $QUICK_MODE -eq 0 ]; then + test_breakpoints + test_memory_read + test_protocol + test_multi_step + test_disassembly + test_multiple_breakpoints + test_all_registers + test_multiple_memory + test_backtrace + fi + + # Summary + echo "" + echo "==========================================" + echo " Test Summary" + echo "==========================================" + TOTAL_TESTS=$((PASSED_TESTS + FAILED_TESTS)) + echo "Total: $TOTAL_TESTS" + echo -e "Passed: ${GREEN}$PASSED_TESTS${NC}" + echo -e "Failed: ${RED}$FAILED_TESTS${NC}" + echo "" + + if [ $FAILED_TESTS -eq 0 ]; then + echo -e "${GREEN}All tests passed!${NC}" + echo "" + log_info "GDB debugging functionality is working correctly" + exit 0 + else + echo -e "${RED}Some tests failed${NC}" + echo "" + log_warn "Check test outputs in /tmp/gdb_test*.txt for details" + log_warn "Run with --verbose for more information" + exit 1 + fi +} + +# Cleanup on exit +trap 'stop_semu' EXIT INT TERM + +# Run main +main