v5gdb 0.1.0-alpha.2

GDB-compatible debugger backend for the VEX V5 platform
.text
.arm

@ An overlay vector table that intercepts debug exception (breakpoints and watchpoints) but
@ otherwise delegates exceptions to an existing vector table.
.global v5gdb_debugger_vector_table
.align 5
v5gdb_debugger_vector_table:
    @ To fall through to an original vector table entry, we jump to the offset of the
    @ original_vector_addresses array which stores the address of the entry we need.
    ldr pc, original_reset_addr @ Reset
    ldr pc, original_udf_addr   @ Undefined instruction
    ldr pc, original_svc_addr   @ Supervisor Call
    b prefetch_abort_handler    @ Prefetch abort
    b data_abort_handler        @ Data abort
    nop                         @ Not used
    b v5gdb_irq_handler         @ IRQ interrupt
    ldr pc, original_fiq_addr   @ FIQ interrupt

@ This is an array of pointers to each offset of the base vector table.
@ When configured at runtime, each entry is (original_vector_table + <OFFSET> * 4).
@ Since we pre-calculate the offsets, evaluating (*original_vector_addresses) + N is the same as
@ evaluating *(original_vector_addresses + N). The latter can be done in just one instruction, which
@ allows us to use it in the overlay vector table.
.global v5gdb_original_vector_addresses
v5gdb_original_vector_addresses:
    original_reset_addr: .word 0
    original_udf_addr: .word 0
    original_svc_addr: .word 0
    original_prefetch_abt_addr: .word 0
    original_data_abt_addr: .word 0
    .word 0
    original_irq_addr: .word 0
    original_fiq_addr: .word 0

@ Intercept IRQ exceptions so can periodically poll the debugger without user code cooperation.
@
@ We intentionally avoid identifying the interrupt source here (by reading ICCIAR) since that would
@ ACK the interrupt which isn't our responsibility. Instead the `v5gdb_irq_poll` is responsible
@ for ratelimiting itself to avoid taking up too much time from user code.
v5gdb_irq_handler:
    @ Save caller-saved registers and the exception return address since `bl` will clobber `lr`.
    push {r0-r3, r12, lr}
    vpush {d16-d31}
    vpush {d0-d7}
    vmrs r0, fpscr
    push {r0, r1} @ r1 is just a bogus register for stack alignment
    blx v5gdb_irq_poll
    pop {r0, r1}
    vmsr fpscr, r0
    vpop {d0-d7}
    vpop {d16-d31}
    pop {r0-r3, r12, lr}
    @ Chain to the original IRQ handler, which will perform the actual exception return.
    ldr pc, original_irq_addr

@ These vector table handlers will fall through for normal aborts, but debug events are caught and
@ redirected to the Rust breakpoint handling logic.
prefetch_abort_handler:
    dsb @ Workaround for Cortex-A9 erratum (id 775420)

    @ First, update the processor's condition flag to indicate whether the exception is a debug
    @ event. Importantly (!), no general-purpose registers are clobbered, allowing the framework's
    @ primary abort handler to capture and show the user the values of these registers from the time
    @ of the abort.
    push {r0}
    mrc p15, 0, r0, c5, c0, 1 @ r0 <- IFSR
    and r0, #0b1111 @ Check the FS[3:0](Fault status) bits.
    @ This comparison does overwrite the comparison flags in APSR, but the ones from before the
    @ abort are stored in SPSR anyways so it's not an issue.
    cmp r0, #0b00010 @ FS == 0b00010(Debug event)?
    pop {r0}

    @ Debug event -> catch it.
    @ Not a debug event -> fall through to original prefetch abort handler.
    ldrne pc, original_prefetch_abt_addr
    sub lr, #4  @ Offset LR to match the preferred return address (see B1.9.7 in ARMv7-A manual).
    b catch_debug_event

@ For watchpoints.
data_abort_handler:
    dsb

    push {r0}
    mrc p15, 0, r0, c5, c0, 0 @ r0 <- DFSR
    and r0, #0b1111 @ Check the FS[3:0](Fault status) bits.
    cmp r0, #0b00010 @ FS == 0b00010(Debug event)?
    pop {r0}

    ldrne pc, original_data_abt_addr
    sub lr, #8  @ Offset LR to match the preferred return address (see B1.9.8 in ARMv7-A manual).
    b catch_debug_event

@ Saves the current program state after a breakpoint or watchpoint and switches into the debug
@ monitor for inspection and modification.
@ The stack should be aligned to 8 bytes when calling this routine.
catch_debug_event:
    @ -- Save --
    @ Create a DebugEventContext struct on the stack.

    @ Store general purpose registers for debugging and exception return
    push {r0-r12,lr}

    @ Save floating-point/simd registers (they are the same).
    @ VPUSH has a maximum of 16 registers, so we do it in groups.
    vpush {d16-d31}
    vpush {d0-d15}
    vmrs r0, fpscr
    push {r0}

    @ Store original user/system-mode stack pointer and link register for debug
    stmdb sp, {sp,lr}^
    @ Adjust our sp, since we can't use writeback on STM (User registers)
    sub sp, sp, #8

    @ Store the caller's program status register
    mrs r0, spsr
    push {r0}

    @ -- Handle exception --

    @ Pass it to our handler using the C ABI, fn(*mut DebugEventContext) -> bool:
    mov r0, sp              @ Set param 0
    blx v5gdb_handle_debug_event  @ Actually call the function now (stack is already aligned correctly)

    @ -- Restore --

    @ Prevent ourselves from getting preempted while messing with task state - this is especially
    @ important on systems with a preemptive scheduler (i.e. FreeRTOS).
    cpsid i

    @ Saved program status
    pop {r1}
    msr spsr, r1

    @ Pop user/system mode banked registers back - SP, LR
    ldm sp, {sp,lr}^
    add sp, sp, #8

    @ Re-enable the scheduler if necessary. When configured for FreeRTOS, the debug monitor disables
    @ the task scheduler to prevent other tasks from running while the debugger is editing their
    @ state.
    @ Note that this happens before we restore the general purpose registers, so we can clobber them
    @ as desired.
.ifdef FREERTOS
    .ifdef PROS
        .set xTaskResumeAll, rtos_resume_all
    .endif

    @ Check the return value from `v5gdb_handle_debug_event`, which indicates whether to resume the
    @ scheduler.
    @ Motivation: This will be false in the case of a single step, since the debugger's immunity
    @ from hardware breakpoints is only because it runs in Abort Mode. Resuming the scheduler
    @ involves switching to System Mode, which forfeits that power, firing the single step inside of
    @ the abort handler itself. Obviously, single steps are supposed to step *user* code, so this
    @ would be suboptimal.
    cmp r0, #0
    beq .Lafter_freertos_scheduler_unpause

    @ Switch to System Mode -- resuming the scheduler can yield, which is only sound if the CPU is
    @ in System Mode.
    @ NB. This will set the current stack to the system mode stack, which has already been updated
    @ (above) to its new location. LR and SPSR are also banked.
    cps #0b11111

    @ (Save old stack ptr, then...) Align stack to 8 bytes for the AAPCS call
    mov r0, sp
    bic sp, sp, #0b111

    @ Resume tasks - this might yield. It's important this is run with the system/user mode stack
    @ to prevent false positive stack overflow checks.
    push {r0,lr}
    blx xTaskResumeAll @ fn() -> u32
    pop {r0,lr}

    @ Restore stack pointer to previous value
    mov sp, r0

    @ Switch back to abort mode so that we restore the rest of the saved registers from the task.
    @ This is also necessary so that the current SPSR is the one we popped into (SPSR_abt).
    cps #0b10111
.Lafter_freertos_scheduler_unpause:
.endif

    @ Floating-point/simd registers
    pop {r0}
    vmsr fpscr, r0
    vpop {d0-d15}
    vpop {d16-d31}

    @ And... perform an exception return by loading the old LR_abt into PC.
    @ Note that we've already offset LR as required to point to the correct exception return
    @ address.
    ldm sp!, {r0-r12, pc}^