mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-08-03 02:27:48 +00:00
c3d514a751
Replace the ROTW-based register flush with a recursive call4 approach that properly triggers hardware window-overflow exceptions. ROTW only modifies WindowBase without saving registers, causing corruption when switching goroutines. The recursive call4 correctly spills all 15 window panes. Also clear WindowStart after the stack switch to prevent stale overflow of garbage register values. Add tinygo_task_current export for C interop. Signed-off-by: deadprogram <ron@hybridgroup.com>
127 lines
5.3 KiB
ArmAsm
127 lines
5.3 KiB
ArmAsm
//go:build tinygo
|
|
|
|
.section .text.tinygo_startTask,"ax",@progbits
|
|
.global tinygo_startTask
|
|
.type tinygo_startTask, %function
|
|
tinygo_startTask:
|
|
// Small assembly stub for starting a goroutine. This already runs on the
|
|
// new stack, control reaches this function after returning from the initial
|
|
// tinygo_swapTask below (the retw.n instruction).
|
|
//
|
|
// The stack was set up in such a way that it looks as if this function was
|
|
// paused using tinygo_swapTask by setting up the parent register window and
|
|
// return pointer as a call4 instruction - except such a call never took
|
|
// place. Instead, the stack pointer is switched to the new stack after all
|
|
// live-but-invisible registers have been flushed to the stack. This means
|
|
// that all registers as present in tinygo_swapTask are moved four up (a2 in
|
|
// tinygo_swapTask is a6 in this function). We don't use any of those
|
|
// registers however. Instead, the retw.n instruction will load them through
|
|
// an underflow exception from the stack which means we get a0-a3 as defined
|
|
// in task_stack_esp32.go.
|
|
|
|
// Branch to the "goroutine start" function. The first (and only) parameter
|
|
// is stored in a2, but has to be moved to a6 to make it appear as a2 in the
|
|
// goroutine start function (due to changing the register window by four
|
|
// with callx4).
|
|
mov.n a6, a2
|
|
callx4 a3
|
|
|
|
// After return, exit this goroutine. This call never returns.
|
|
call4 tinygo_task_exit
|
|
|
|
.section .text.tinygo_swapTask,"ax",@progbits
|
|
.global tinygo_swapTask
|
|
.type tinygo_swapTask, %function
|
|
tinygo_swapTask:
|
|
// This function gets the following parameters:
|
|
// a2 = newStack uintptr
|
|
// a3 = oldStack *uintptr
|
|
|
|
// Reserve 32 bytes on the stack. It really needs to be 32 bytes, with 16
|
|
// extra at the bottom to adhere to the ABI.
|
|
entry sp, 32
|
|
|
|
// Disable interrupts while flushing registers. This is necessary because
|
|
// interrupts might want to use the stack pointer (at a2) which will be some
|
|
// arbitrary register while registers are flushed.
|
|
rsil a4, 3 // XCHAL_EXCM_LEVEL
|
|
|
|
// Save the old PS (in a4) to the stack because call4 clobbers a4-a7.
|
|
s32i a4, sp, 4
|
|
|
|
// Flush all register windows to the stack using recursive call4.
|
|
//
|
|
// The previous ROTW-based approach (borrowed from Zephyr) does NOT
|
|
// actually trigger window overflow exceptions on Xtensa LX7 — ROTW
|
|
// simply modifies WindowBase without saving any registers to the stack.
|
|
// This leaves stale window data in the physical register file after a
|
|
// goroutine switch, corrupting the overflow save chain when the new
|
|
// goroutine's deeper calls trigger overflow of old goroutine windows.
|
|
//
|
|
// Instead, we recursively call a small function via call4. Each call4 +
|
|
// entry triggers the hardware window-overflow mechanism for any occupied
|
|
// pane being reused, correctly saving registers to the stack.
|
|
// For NAREG=64 (16 panes), 15 recursive levels cover all panes except
|
|
// the current one (tinygo_swapTask), which must stay active.
|
|
movi a6, 15
|
|
call4 .Lspill_windows
|
|
|
|
// After the recursive spill returns, the physical register file still
|
|
// has WindowStart bits set for the spill helper frames.
|
|
// We will clear WindowStart completely (to 0) right before the retw.n
|
|
// below, after the stack switch is done. This prevents stale overflow
|
|
// when the new goroutine's calls rotate back into these panes.
|
|
|
|
// Restore interrupts.
|
|
l32i a4, sp, 4 // reload saved PS
|
|
wsr.ps a4
|
|
rsync
|
|
|
|
// At this point, the following is true:
|
|
// WindowStart == 1 << WindowBase
|
|
// All other windows have been properly flushed to their stacks.
|
|
// It also means that the stack pointer can now be safely modified.
|
|
|
|
// Save a0, which stores the return address and the parent register window
|
|
// in the upper two bits.
|
|
s32i.n a0, sp, 0
|
|
|
|
// Save the current stack pointer in oldStack.
|
|
s32i.n sp, a3, 0
|
|
|
|
// Switch to the new stack pointer (newStack).
|
|
mov.n sp, a2
|
|
|
|
// Load a0, which is the previous return address from before the previous
|
|
// switch or the constructed return address to tinygo_startTask. This
|
|
// register also stores the parent register window.
|
|
l32i.n a0, sp, 0
|
|
|
|
// Clear ALL WindowStart bits. With all windows spilled to the stack,
|
|
// we must ensure no stale WS bits remain: the retw.n below will trigger
|
|
// underflow4 to load the new goroutine's registers from the new stack
|
|
// (which sets the appropriate WS bit via rfwu). Any stale WS bits
|
|
// (from spill helpers or the old goroutine) would cause spurious
|
|
// overflows of garbage register values into memory.
|
|
movi a5, 0
|
|
wsr a5, WINDOWSTART
|
|
rsync
|
|
|
|
// Return into the new stack. This instruction will trigger a window
|
|
// underflow, reloading the saved registers from the stack.
|
|
retw.n
|
|
|
|
// Recursive helper for flushing all register windows.
|
|
// Parameter: a2 = remaining recursion depth (passed via caller's a6).
|
|
// Each call4 + entry triggers the hardware overflow mechanism for any
|
|
// occupied pane at the new WindowBase position.
|
|
.balign 4
|
|
.Lspill_windows:
|
|
entry a1, 16
|
|
beqz a2, .Lspill_done
|
|
addi a2, a2, -1
|
|
mov a6, a2
|
|
call4 .Lspill_windows
|
|
.Lspill_done:
|
|
retw.n
|