From abb09e869e3d4a6f5a192e58abcc6a086af9d7ab Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Thu, 24 Sep 2020 23:56:48 +0200 Subject: [PATCH] runtime, internal/task: refactor to simplify stack switching The Cortex-M target isn't much changed, but much of the logic for the AVR stack switcher that was previously in assembly has now been moved to Go to make it more maintainable and in fact smaller in code size. Three functions (tinygo_getCurrentStackPointer, tinygo_switchToTask, tinygo_switchToScheduler) have been changed to one: tinygo_swapTask. This reduction in assembly code should make the code more maintainable and should make it easier to port stack switching to other architectures. I've also moved the assembly files to src/internal/task, which seems like a more appropriate location to me. --- src/internal/task/task_stack.go | 29 ++- src/internal/task/task_stack_avr.S | 99 +++++++++ src/internal/task/task_stack_avr.go | 49 ++--- .../task/task_stack_cortexm.S} | 11 - src/internal/task/task_stack_cortexm.go | 39 ++-- src/runtime/scheduler_avr.S | 189 ------------------ src/runtime/scheduler_tasks.go | 13 +- targets/avr.json | 4 +- targets/cortex-m.json | 4 +- 9 files changed, 169 insertions(+), 268 deletions(-) create mode 100644 src/internal/task/task_stack_avr.S rename src/{runtime/scheduler_cortexm.S => internal/task/task_stack_cortexm.S} (92%) delete mode 100644 src/runtime/scheduler_avr.S diff --git a/src/internal/task/task_stack.go b/src/internal/task/task_stack.go index 214f2a62..a703d10a 100644 --- a/src/internal/task/task_stack.go +++ b/src/internal/task/task_stack.go @@ -45,6 +45,11 @@ func Pause() { currentTask.state.pause() } +//export tinygo_pause +func pause() { + Pause() +} + // Resume the task until it pauses or completes. // This may only be called from the scheduler. func (t *Task) Resume() { @@ -58,10 +63,32 @@ func (s *state) initialize(fn uintptr, args unsafe.Pointer, stackSize uintptr) { // Create a stack. stack := make([]uintptr, stackSize/unsafe.Sizeof(uintptr(0))) + // Set up the stack canary, a random number that should be checked when + // switching from the task back to the scheduler. The stack canary pointer + // points to the first word of the stack. If it has changed between now and + // the next stack switch, there was a stack overflow. + s.canaryPtr = &stack[0] + *s.canaryPtr = stackCanary + + // Get a pointer to the top of the stack, where the initial register values + // are stored. They will be popped off the stack on the first stack switch + // to the goroutine, and will start running tinygo_startTask (this setup + // happens in archInit). + r := (*calleeSavedRegs)(unsafe.Pointer(&stack[uintptr(len(stack))-(unsafe.Sizeof(calleeSavedRegs{})/unsafe.Sizeof(uintptr(0)))])) + // Invoke architecture-specific initialization. - s.archInit(stack, fn, args) + s.archInit(r, fn, args) } +//export tinygo_swapTask +func swapTask(oldStack uintptr, newStack *uintptr) + +// startTask is a small wrapper function that sets up the first (and only) +// argument to the new goroutine and makes sure it is exited when the goroutine +// finishes. +//go:extern tinygo_startTask +var startTask [0]uint8 + //go:linkname runqueuePushBack runtime.runqueuePushBack func runqueuePushBack(*Task) diff --git a/src/internal/task/task_stack_avr.S b/src/internal/task/task_stack_avr.S new file mode 100644 index 00000000..20f98990 --- /dev/null +++ b/src/internal/task/task_stack_avr.S @@ -0,0 +1,99 @@ +.section .bss.tinygo_systemStack +.global tinygo_systemStack +.type tinygo_systemStack, %object +tinygo_systemStack: + .short 0 + +.section .text.tinygo_startTask +.global tinygo_startTask +.type tinygo_startTask, %function +tinygo_startTask: + // Small assembly stub for starting a goroutine. This is already run on the + // new stack, with the callee-saved registers already loaded. + // Most importantly, r2r3 contain the pc of the to-be-started function and + // r4r5 contain the only argument it is given. Multiple arguments are packed + // into one by storing them in a new allocation. + + // Set the first argument of the goroutine start wrapper, which contains all + // the arguments. + movw r24, r4 + + // Branch to the "goroutine start" function. Note that the Z register is + // call-clobbered, so does not need to be restored after use. + movw Z, r2 + icall + + // After return, exit this goroutine. This is a tail call. +#if __AVR_ARCH__ == 2 || __AVR_ARCH__ == 25 + // Small memory devices (≤8kB flash) that do not have the long call + // instruction availble will need to use rcall instead. + // Note that they will probably not be able to run more than the main + // goroutine anyway, but this file is compiled for all AVRs so it needs to + // compile at least. + rcall tinygo_pause +#else + // Other devices can (and must) use the regular call instruction. + call tinygo_pause +#endif + +.global tinygo_swapTask +.type tinygo_swapTask, %function +tinygo_swapTask: + // This function gets the following parameters: + // r24:r25 = newStack uintptr + // r22:r23 = oldStack *uintptr + + // Save all call-saved registers: + // https://gcc.gnu.org/wiki/avr-gcc#Call-Saved_Registers + push r29 // Y + push r28 // Y + push r17 + push r16 + push r15 + push r14 + push r13 + push r12 + push r11 + push r10 + push r9 + push r8 + push r7 + push r6 + push r5 + push r4 + push r3 + push r2 + + // Save the current stack pointer in oldStack. + in r2, 0x3d; SPL + in r3, 0x3e; SPH + movw Y, r22 + std Y+0, r2 + std Y+1, r3 + + // Switch to the new stack pointer. + out 0x3d, r24; SPL + out 0x3e, r25; SPH + + // Load saved register from the new stack. + pop r2 + pop r3 + pop r4 + pop r5 + pop r6 + pop r7 + pop r8 + pop r9 + pop r10 + pop r11 + pop r12 + pop r13 + pop r14 + pop r15 + pop r16 + pop r17 + pop r28 // Y + pop r29 // Y + + // Return into the new task, as if tinygo_swapTask was a regular call. + ret diff --git a/src/internal/task/task_stack_avr.go b/src/internal/task/task_stack_avr.go index 08c0b63e..655280ac 100644 --- a/src/internal/task/task_stack_avr.go +++ b/src/internal/task/task_stack_avr.go @@ -4,9 +4,12 @@ package task import "unsafe" +//go:extern tinygo_systemStack +var systemStack uintptr + // calleeSavedRegs is the list of registers that must be saved and restored when -// switching between tasks. Also see scheduler_avr.S that relies on the -// exact layout of this struct. +// switching between tasks. Also see task_stack_avr.S that relies on the exact +// layout of this struct. // // https://gcc.gnu.org/wiki/avr-gcc#Call-Saved_Registers type calleeSavedRegs struct { @@ -23,34 +26,15 @@ type calleeSavedRegs struct { pc uintptr } -// registers gets a pointer to the registers stored at the top of the stack. -func (s *state) registers() *calleeSavedRegs { - return (*calleeSavedRegs)(unsafe.Pointer(s.sp + 1)) -} - -// startTask is a small wrapper function that sets up the first (and only) -// argument to the new goroutine and makes sure it is exited when the goroutine -// finishes. -//go:extern tinygo_startTask -var startTask [0]uint8 - // archInit runs architecture-specific setup for the goroutine startup. // Note: adding //go:noinline to work around an AVR backend bug. //go:noinline -func (s *state) archInit(stack []uintptr, fn uintptr, args unsafe.Pointer) { - // Set up the stack canary, a random number that should be checked when - // switching from the task back to the scheduler. The stack canary pointer - // points to the first word of the stack. If it has changed between now and - // the next stack switch, there was a stack overflow. - s.canaryPtr = &stack[0] - *s.canaryPtr = stackCanary - +func (s *state) archInit(r *calleeSavedRegs, fn uintptr, args unsafe.Pointer) { // Store the initial sp for the startTask function (implemented in assembly). - s.sp = uintptr(unsafe.Pointer(&stack[uintptr(len(stack))-(unsafe.Sizeof(calleeSavedRegs{})/unsafe.Sizeof(uintptr(0)))])) - 1 + s.sp = uintptr(unsafe.Pointer(r)) - 1 // Initialize the registers. // These will be popped off of the stack on the first resume of the goroutine. - r := s.registers() // Start the function at tinygo_startTask. startTask := uintptr(unsafe.Pointer(&startTask)) @@ -67,20 +51,17 @@ func (s *state) archInit(stack []uintptr, fn uintptr, args unsafe.Pointer) { } func (s *state) resume() { - switchToTask(s.sp) + swapTask(s.sp, &systemStack) } -//export tinygo_switchToTask -func switchToTask(uintptr) - -//export tinygo_switchToScheduler -func switchToScheduler(*uintptr) - func (s *state) pause() { - switchToScheduler(&s.sp) + newStack := systemStack + systemStack = 0 + swapTask(newStack, &s.sp) } -//export tinygo_pause -func pause() { - Pause() +// SystemStack returns the system stack pointer when called from a task stack. +// When called from the system stack, it returns 0. +func SystemStack() uintptr { + return systemStack } diff --git a/src/runtime/scheduler_cortexm.S b/src/internal/task/task_stack_cortexm.S similarity index 92% rename from src/runtime/scheduler_cortexm.S rename to src/internal/task/task_stack_cortexm.S index fddddfde..afb23eb2 100644 --- a/src/runtime/scheduler_cortexm.S +++ b/src/internal/task/task_stack_cortexm.S @@ -30,17 +30,6 @@ tinygo_startTask: .cfi_endproc .size tinygo_startTask, .-tinygo_startTask -.section .text.tinygo_getSystemStackPointer -.global tinygo_getSystemStackPointer -.type tinygo_getSystemStackPointer, %function -tinygo_getSystemStackPointer: - .cfi_startproc - // The system stack pointer is always stored in the MSP register. - mrs r0, MSP - bx lr - .cfi_endproc -.size tinygo_getSystemStackPointer, .-tinygo_getSystemStackPointer - .section .text.tinygo_switchToScheduler .global tinygo_switchToScheduler .type tinygo_switchToScheduler, %function diff --git a/src/internal/task/task_stack_cortexm.go b/src/internal/task/task_stack_cortexm.go index 0417b922..eb3fe182 100644 --- a/src/internal/task/task_stack_cortexm.go +++ b/src/internal/task/task_stack_cortexm.go @@ -2,10 +2,13 @@ package task -import "unsafe" +import ( + "device/arm" + "unsafe" +) // calleeSavedRegs is the list of registers that must be saved and restored when -// switching between tasks. Also see scheduler_cortexm.S that relies on the +// switching between tasks. Also see task_stack_cortexm.S that relies on the // exact layout of this struct. type calleeSavedRegs struct { r4 uintptr @@ -20,34 +23,15 @@ type calleeSavedRegs struct { pc uintptr } -// registers gets a pointer to the registers stored at the top of the stack. -func (s *state) registers() *calleeSavedRegs { - return (*calleeSavedRegs)(unsafe.Pointer(s.sp)) -} - -// startTask is a small wrapper function that sets up the first (and only) -// argument to the new goroutine and makes sure it is exited when the goroutine -// finishes. -//go:extern tinygo_startTask -var startTask [0]uint8 - // archInit runs architecture-specific setup for the goroutine startup. -func (s *state) archInit(stack []uintptr, fn uintptr, args unsafe.Pointer) { - // Set up the stack canary, a random number that should be checked when - // switching from the task back to the scheduler. The stack canary pointer - // points to the first word of the stack. If it has changed between now and - // the next stack switch, there was a stack overflow. - s.canaryPtr = &stack[0] - *s.canaryPtr = stackCanary - +func (s *state) archInit(r *calleeSavedRegs, fn uintptr, args unsafe.Pointer) { // Store the initial sp for the startTask function (implemented in assembly). - s.sp = uintptr(unsafe.Pointer(&stack[uintptr(len(stack))-(unsafe.Sizeof(calleeSavedRegs{})/unsafe.Sizeof(uintptr(0)))])) + s.sp = uintptr(unsafe.Pointer(r)) // Initialize the registers. // These will be popped off of the stack on the first resume of the goroutine. - r := s.registers() - // Start the function at tinygo_startTask (defined in src/runtime/scheduler_cortexm.S). + // Start the function at tinygo_startTask (defined in src/internal/task/task_stack_cortexm.S). // This assembly code calls a function (passed in r4) with a single argument (passed in r5). // After the function returns, it calls Pause(). r.pc = uintptr(unsafe.Pointer(&startTask)) @@ -75,7 +59,8 @@ func (s *state) pause() { switchToScheduler(&s.sp) } -//export tinygo_pause -func pause() { - Pause() +// SystemStack returns the system stack pointer. On Cortex-M, it is always +// available. +func SystemStack() uintptr { + return arm.AsmFull("mrs {}, MSP", nil) } diff --git a/src/runtime/scheduler_avr.S b/src/runtime/scheduler_avr.S deleted file mode 100644 index 83daf2e2..00000000 --- a/src/runtime/scheduler_avr.S +++ /dev/null @@ -1,189 +0,0 @@ -.section .bss.tinygo_systemStack -.global tinygo_systemStack -.type tinygo_systemStack, %object -tinygo_systemStack: - .short 0 - -.section .text.tinygo_startTask -.global tinygo_startTask -.type tinygo_startTask, %function -tinygo_startTask: - // Small assembly stub for starting a goroutine. This is already run on the - // new stack, with the callee-saved registers already loaded. - // Most importantly, r2r3 contain the pc of the to-be-started function and - // r4r5 contain the only argument it is given. Multiple arguments are packed - // into one by storing them in a new allocation. - - // Set the first argument of the goroutine start wrapper, which contains all - // the arguments. - movw r24, r4 - - // Branch to the "goroutine start" function. Note that the Z register is - // call-clobbered, so does not need to be restored after use. - movw Z, r2 - icall - - // After return, exit this goroutine. This is a tail call. -#if __AVR_ARCH__ == 2 || __AVR_ARCH__ == 25 - // Small memory devices (≤8kB flash) that do not have the long call - // instruction availble will need to use rcall instead. - // Note that they will probably not be able to run more than the main - // goroutine anyway, but this file is compiled for all AVRs so it needs to - // compile at least. - rcall tinygo_pause -#else - // Other devices can (and must) use the regular call instruction. - call tinygo_pause -#endif - -// Get the system stack pointer, independent of whether we're currently on the -// system stack or a task stack. -.global tinygo_getSystemStackPointer -.type tinygo_getSystemStackPointer, %function -tinygo_getSystemStackPointer: - // Load system stack pointer. - lds r24, tinygo_systemStack - lds r25, tinygo_systemStack+1 - - // Compare against 0. - cp r24, r1 - cpc r25, r1 - - // Branch (and then return) if tinygo_systemStack has a non-zero value. - brne 1f - - // tinygo_systemStack is zero, so return the current stack pointer. - in r24, 0x3d; SPL - in r25, 0x3e; SPH - -1: - ret - -.global tinygo_switchToTask -.type tinygo_switchToTask, %function -tinygo_switchToTask: - // The sp parameter is the only parameter, so it will take up r24:r25. - // r24:r25 = sp uintptr - - // Save all call-saved registers: - // https://gcc.gnu.org/wiki/avr-gcc#Call-Saved_Registers - push r29 // Y - push r28 // Y - push r17 - push r16 - push r15 - push r14 - push r13 - push r12 - push r11 - push r10 - push r9 - push r8 - push r7 - push r6 - push r5 - push r4 - push r3 - push r2 - - // Save the system stack pointer in a global. - in r2, 0x3d; SPL - in r3, 0x3e; SPH - sts tinygo_systemStack+0, r2 - sts tinygo_systemStack+1, r3 - - // Switch to the task stack pointer. - out 0x3d, r24; SPL - out 0x3e, r25; SPH - - // Load saved register from the task stack. - pop r2 - pop r3 - pop r4 - pop r5 - pop r6 - pop r7 - pop r8 - pop r9 - pop r10 - pop r11 - pop r12 - pop r13 - pop r14 - pop r15 - pop r16 - pop r17 - pop r28 // Y - pop r29 // Y - - // Return into the new task, as if tinygo_switchToScheduler was a regular - // call. - ret - -.global tinygo_switchToScheduler -.type tinygo_switchToScheduler, %function -tinygo_switchToScheduler: - // The sp parameter is the only parameter, so it will take up r24:r25. - // r24:r25 = sp *uintptr - - // Save all call-saved registers on the task stack: - // https://gcc.gnu.org/wiki/avr-gcc#Call-Saved_Registers - push r29 // Y - push r28 // Y - push r17 - push r16 - push r15 - push r14 - push r13 - push r12 - push r11 - push r10 - push r9 - push r8 - push r7 - push r6 - push r5 - push r4 - push r3 - push r2 - - // Save the task stack. - in r2, 0x3d; SPL - in r3, 0x3e; SPH - movw Y, r24 - std Y+0, r2 - std Y+1, r3 - - // Switch to the system stack. - lds r2, tinygo_systemStack - lds r3, tinygo_systemStack+1 - out 0x3d, r2; SPL - out 0x3e, r3; SPH - - // Clear tinygo_systemStack to make sure tinygo_getSystemStackPointer knows - // which pointer to return. - sts tinygo_systemStack+0, r1 - sts tinygo_systemStack+1, r1 - - // Load saved register from the system stack. - pop r2 - pop r3 - pop r4 - pop r5 - pop r6 - pop r7 - pop r8 - pop r9 - pop r10 - pop r11 - pop r12 - pop r13 - pop r14 - pop r15 - pop r16 - pop r17 - pop r28 // Y - pop r29 // Y - - // Return into the scheduler, as if tinygo_switchToTask was a regular call. - ret diff --git a/src/runtime/scheduler_tasks.go b/src/runtime/scheduler_tasks.go index 9be63d5d..03009b34 100644 --- a/src/runtime/scheduler_tasks.go +++ b/src/runtime/scheduler_tasks.go @@ -2,7 +2,16 @@ package runtime +import "internal/task" + // getSystemStackPointer returns the current stack pointer of the system stack. // This is not necessarily the same as the current stack pointer. -//export tinygo_getSystemStackPointer -func getSystemStackPointer() uintptr +func getSystemStackPointer() uintptr { + // TODO: this always returns the correct stack on Cortex-M, so don't bother + // comparing against 0. + sp := task.SystemStack() + if sp == 0 { + sp = getCurrentStackPointer() + } + return sp +} diff --git a/targets/avr.json b/targets/avr.json index 75f3e175..fdae4b8b 100644 --- a/targets/avr.json +++ b/targets/avr.json @@ -13,7 +13,7 @@ "-Wl,--gc-sections" ], "extra-files": [ - "src/runtime/gc_avr.S", - "src/runtime/scheduler_avr.S" + "src/internal/task/task_stack_avr.S", + "src/runtime/gc_avr.S" ] } diff --git a/targets/cortex-m.json b/targets/cortex-m.json index 15be7d2d..6f031546 100644 --- a/targets/cortex-m.json +++ b/targets/cortex-m.json @@ -25,8 +25,8 @@ ], "extra-files": [ "src/device/arm/cortexm.s", - "src/runtime/gc_arm.S", - "src/runtime/scheduler_cortexm.S" + "src/internal/task/task_stack_cortexm.S", + "src/runtime/gc_arm.S" ], "gdb": "gdb-multiarch" }