Skip to content

Commit ee12282

Browse files
committed
runtime/interrupt: add Checkpoint type
This type can be used to jump back to a previous position in a program from inside an interrupt. This is useful for baremetal systems that implement wfi but not wfe, and therefore have no easy (race-free) way to wait until a flag gets changed inside an interrupt. This is an issue on RISC-V, where this is racy (the interrupt might happen after the check but before the wfi instruction): configureInterrupt() for flag.Load() != 0 { riscv.Asm("wfi") }
1 parent 28f70fc commit ee12282

File tree

5 files changed

+101
-6
lines changed

5 files changed

+101
-6
lines changed

compiler/compiler.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1895,6 +1895,8 @@ func (b *builder) createFunctionCall(instr *ssa.CallCommon) (llvm.Value, error)
18951895
case name == "runtime.exportedFuncPtr":
18961896
_, ptr := b.getFunction(instr.Args[0].(*ssa.Function))
18971897
return b.CreatePtrToInt(ptr, b.uintptrType, ""), nil
1898+
case name == "(*runtime/interrupt.Checkpoint).Save":
1899+
return b.createInterruptCheckpoint(instr.Args[0]), nil
18981900
case name == "internal/abi.FuncPCABI0":
18991901
retval := b.createDarwinFuncPCABI0Call(instr)
19001902
if !retval.IsNil() {

compiler/defer.go

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,11 @@ func (b *builder) createLandingPad() {
103103
b.CreateBr(b.blockEntries[b.fn.Recover])
104104
}
105105

106-
// createInvokeCheckpoint saves the function state at the given point, to
107-
// continue at the landing pad if a panic happened. This is implemented using a
108-
// setjmp-like construct.
109-
func (b *builder) createInvokeCheckpoint() {
106+
// Create a checkpoint (similar to setjmp). This emits inline assembly that
107+
// stores the current program counter inside the ptr address (actually
108+
// ptr+sizeof(ptr)) and then returns a boolean indicating whether this is the
109+
// normal flow (false) or we jumped here from somewhere else (true).
110+
func (b *builder) createCheckpoint(ptr llvm.Value) llvm.Value {
110111
// Construct inline assembly equivalents of setjmp.
111112
// The assembly works as follows:
112113
// * All registers (both callee-saved and caller saved) are clobbered
@@ -217,11 +218,19 @@ li a0, 0
217218
// This case should have been handled by b.supportsRecover().
218219
b.addError(b.fn.Pos(), "unknown architecture for defer: "+b.archFamily())
219220
}
220-
asmType := llvm.FunctionType(resultType, []llvm.Type{b.deferFrame.Type()}, false)
221+
asmType := llvm.FunctionType(resultType, []llvm.Type{b.dataPtrType}, false)
221222
asm := llvm.InlineAsm(asmType, asmString, constraints, false, false, 0, false)
222-
result := b.CreateCall(asmType, asm, []llvm.Value{b.deferFrame}, "setjmp")
223+
result := b.CreateCall(asmType, asm, []llvm.Value{ptr}, "setjmp")
223224
result.AddCallSiteAttribute(-1, b.ctx.CreateEnumAttribute(llvm.AttributeKindID("returns_twice"), 0))
224225
isZero := b.CreateICmp(llvm.IntEQ, result, llvm.ConstInt(resultType, 0, false), "setjmp.result")
226+
return isZero
227+
}
228+
229+
// createInvokeCheckpoint saves the function state at the given point, to
230+
// continue at the landing pad if a panic happened. This is implemented using a
231+
// setjmp-like construct.
232+
func (b *builder) createInvokeCheckpoint() {
233+
isZero := b.createCheckpoint(b.deferFrame)
225234
continueBB := b.insertBasicBlock("")
226235
b.CreateCondBr(isZero, continueBB, b.landingpad)
227236
b.SetInsertPointAtEnd(continueBB)

compiler/inlineasm.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,3 +249,15 @@ func (b *builder) emitCSROperation(call *ssa.CallCommon) (llvm.Value, error) {
249249
return llvm.Value{}, b.makeError(call.Pos(), "unknown CSR operation: "+name)
250250
}
251251
}
252+
253+
// Implement runtime/interrupt.Checkpoint.Save. It needs to be implemented
254+
// directly at the call site. If it isn't implemented directly at the call site
255+
// (but instead through a function call), it might result in an overwritten
256+
// stack in the non-jump return case.
257+
func (b *builder) createInterruptCheckpoint(ptr ssa.Value) llvm.Value {
258+
addr := b.getValue(ptr, ptr.Pos())
259+
b.createNilCheck(ptr, addr, "deref")
260+
stackPointer := b.readStackPointer()
261+
b.CreateStore(stackPointer, addr)
262+
return b.createCheckpoint(addr)
263+
}

src/runtime/asm_riscv.S

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,12 @@ tinygo_longjmp:
5050
lw sp, 0(a0) // jumpSP
5151
lw a1, REGSIZE(a0) // jumpPC
5252
jr a1
53+
54+
.section .text.tinygo_checkpointJump
55+
.global tinygo_checkpointJump
56+
tinygo_checkpointJump:
57+
// Note: the code we jump to assumes a0 is non-zero, which is already the
58+
// case because that's the stack pointer.
59+
mv sp, a0 // jumpSP
60+
csrw mepc, a1 // update MEPC value, so we resume there after the mret
61+
mret // jump to jumpPC

src/runtime/interrupt/checkpoint.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package interrupt
2+
3+
// A checkpoint is a setjmp like buffer, that can be used as a flag for
4+
// interrupts.
5+
//
6+
// It can be used as follows:
7+
//
8+
// // global var
9+
// var c Checkpoint
10+
//
11+
// // to set up the checkpoint and wait for it
12+
// if c.Save() {
13+
// setupInterrupt()
14+
// for {
15+
// waitForInterrupt()
16+
// }
17+
// }
18+
//
19+
// // Inside the interrupt handler:
20+
// if c.Saved() {
21+
// c.Jump()
22+
// }
23+
type Checkpoint struct {
24+
jumpSP uintptr
25+
jumpPC uintptr
26+
}
27+
28+
// Save the execution state in the given checkpoint, overwriting a previous
29+
// saved checkpoint.
30+
//
31+
// This function returns twice: once the normal way after saving (returning
32+
// true) and once after jumping (returning false).
33+
//
34+
// This function is a compiler intrinsic, it is not implemented in Go.
35+
func (c *Checkpoint) Save() bool
36+
37+
// Returns whether a jump point was saved (and not erased due to a jump).
38+
func (c *Checkpoint) Saved() bool {
39+
return c.jumpPC != 0
40+
}
41+
42+
// Jump to the point where the execution state was saved, and erase the saved
43+
// jump point. This works across interrupts.
44+
//
45+
// This method does not return in the conventional way, it resumes execution at
46+
// the last point a checkpoint was saved.
47+
func (c *Checkpoint) Jump() {
48+
if !c.Saved() {
49+
panic("runtime/interrupt: no checkpoint was saved")
50+
}
51+
// TODO: check whether we are indeed inside an interrupt
52+
jumpPC := c.jumpPC
53+
jumpSP := c.jumpSP
54+
c.jumpPC = 0
55+
c.jumpSP = 0
56+
if jumpPC == 0 {
57+
panic("jumping to 0")
58+
}
59+
checkpointJump(jumpSP, jumpPC)
60+
}
61+
62+
//export tinygo_checkpointJump
63+
func checkpointJump(jumpSP, jumpPC uintptr)

0 commit comments

Comments
 (0)