Skip to content

Commit

Permalink
runtime: support for debugger function calls
Browse files Browse the repository at this point in the history
This adds a mechanism for debuggers to safely inject calls to Go
functions on amd64. Debuggers must participate in a protocol with the
runtime, and need to know how to lay out a call frame, but the runtime
support takes care of the details of handling live pointers in
registers, stack growth, and detecting the trickier conditions when it
is unsafe to inject a user function call.

Fixes #21678.
Updates derekparker/delve#119.

Change-Id: I56d8ca67700f1f77e19d89e7fc92ab337b228834
Reviewed-on: https://go-review.googlesource.com/109699
Run-TryBot: Austin Clements <austin@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Keith Randall <khr@golang.org>
  • Loading branch information
aclements committed May 22, 2018
1 parent 9f95c9d commit c5ed10f
Show file tree
Hide file tree
Showing 13 changed files with 706 additions and 7 deletions.
1 change: 1 addition & 0 deletions src/cmd/internal/objabi/funcid.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,5 @@ const (
FuncID_cgocallback_gofunc
FuncID_gogo
FuncID_externalthreadhandler
FuncID_debugCallV1
)
2 changes: 2 additions & 0 deletions src/cmd/link/internal/ld/pcln.go
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,8 @@ func (ctxt *Link) pclntab() {
funcID = objabi.FuncID_gogo
case "runtime.externalthreadhandler":
funcID = objabi.FuncID_externalthreadhandler
case "runtime.debugCallV1":
funcID = objabi.FuncID_debugCallV1
}
off = int32(ftab.SetUint32(ctxt.Arch, int64(off), uint32(funcID)))

Expand Down
197 changes: 197 additions & 0 deletions src/runtime/asm_amd64.s
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,11 @@ ok:
CALL runtime·abort(SB) // mstart should never return
RET

// Prevent dead-code elimination of debugCallV1, which is
// intended to be called by debuggers.
MOVQ $runtime·debugCallV1(SB), AX
RET

DATA runtime·mainPC+0(SB)/8,$runtime·main(SB)
GLOBL runtime·mainPC(SB),RODATA,$8

Expand Down Expand Up @@ -1460,3 +1465,195 @@ flush:
MOVQ 88(SP), R12
MOVQ 96(SP), R15
JMP ret

DATA debugCallFrameTooLarge<>+0x00(SB)/8, $"call fra"
DATA debugCallFrameTooLarge<>+0x08(SB)/8, $"me too l"
DATA debugCallFrameTooLarge<>+0x10(SB)/4, $"arge"
GLOBL debugCallFrameTooLarge<>(SB), RODATA, $0x14 // Size duplicated below

// debugCallV1 is the entry point for debugger-injected function
// calls on running goroutines. It informs the runtime that a
// debug call has been injected and creates a call frame for the
// debugger to fill in.
//
// To inject a function call, a debugger should:
// 1. Check that the goroutine is in state _Grunning and that
// there are at least 256 bytes free on the stack.
// 2. Push the current PC on the stack (updating SP).
// 3. Write the desired argument frame size at SP-16 (using the SP
// after step 2).
// 4. Save all machine registers (including flags and XMM reigsters)
// so they can be restored later by the debugger.
// 5. Set the PC to debugCallV1 and resume execution.
//
// If the goroutine is in state _Grunnable, then it's not generally
// safe to inject a call because it may return out via other runtime
// operations. Instead, the debugger should unwind the stack to find
// the return to non-runtime code, add a temporary breakpoint there,
// and inject the call once that breakpoint is hit.
//
// If the goroutine is in any other state, it's not safe to inject a call.
//
// This function communicates back to the debugger by setting RAX and
// invoking INT3 to raise a breakpoint signal. See the comments in the
// implementation for the protocol the debugger is expected to
// follow. InjectDebugCall in the runtime tests demonstates this protocol.
//
// The debugger must ensure that any pointers passed to the function
// obey escape analysis requirements. Specifically, it must not pass
// a stack pointer to an escaping argument. debugCallV1 cannot check
// this invariant.
TEXT runtime·debugCallV1(SB),NOSPLIT,$152-0
// Save all registers that may contain pointers in GC register
// map order (see ssa.registersAMD64). This makes it possible
// to copy the stack while updating pointers currently held in
// registers, and for the GC to find roots in registers.
//
// We can't do anything that might clobber any of these
// registers before this.
MOVQ R15, r15-(14*8+8)(SP)
MOVQ R14, r14-(13*8+8)(SP)
MOVQ R13, r13-(12*8+8)(SP)
MOVQ R12, r12-(11*8+8)(SP)
MOVQ R11, r11-(10*8+8)(SP)
MOVQ R10, r10-(9*8+8)(SP)
MOVQ R9, r9-(8*8+8)(SP)
MOVQ R8, r8-(7*8+8)(SP)
MOVQ DI, di-(6*8+8)(SP)
MOVQ SI, si-(5*8+8)(SP)
MOVQ BP, bp-(4*8+8)(SP)
MOVQ BX, bx-(3*8+8)(SP)
MOVQ DX, dx-(2*8+8)(SP)
// Save the frame size before we clobber it. Either of the last
// saves could clobber this depending on whether there's a saved BP.
MOVQ frameSize-24(FP), DX // aka -16(RSP) before prologue
MOVQ CX, cx-(1*8+8)(SP)
MOVQ AX, ax-(0*8+8)(SP)

// Save the argument frame size.
MOVQ DX, frameSize-128(SP)

// Perform a safe-point check.
MOVQ retpc-8(FP), AX // Caller's PC
MOVQ AX, 0(SP)
CALL runtime·debugCallCheck(SB)
MOVQ 8(SP), AX
TESTQ AX, AX
JZ good
// The safety check failed. Put the reason string at the top
// of the stack.
MOVQ AX, 0(SP)
MOVQ 16(SP), AX
MOVQ AX, 8(SP)
// Set AX to 8 and invoke INT3. The debugger should get the
// reason a call can't be injected from the top of the stack
// and resume execution.
MOVQ $8, AX
BYTE $0xcc
JMP restore

good:
// Registers are saved and it's safe to make a call.
// Open up a call frame, moving the stack if necessary.
//
// Once the frame is allocated, this will set AX to 0 and
// invoke INT3. The debugger should write the argument
// frame for the call at SP, push the trapping PC on the
// stack, set the PC to the function to call, set RCX to point
// to the closure (if a closure call), and resume execution.
//
// If the function returns, this will set AX to 1 and invoke
// INT3. The debugger can then inspect any return value saved
// on the stack at SP and resume execution again.
//
// If the function panics, this will set AX to 2 and invoke INT3.
// The interface{} value of the panic will be at SP. The debugger
// can inspect the panic value and resume execution again.
#define DEBUG_CALL_DISPATCH(NAME,MAXSIZE) \
CMPQ AX, $MAXSIZE; \
JA 5(PC); \
MOVQ $NAME(SB), AX; \
MOVQ AX, 0(SP); \
CALL runtime·debugCallWrap(SB); \
JMP restore

MOVQ frameSize-128(SP), AX
DEBUG_CALL_DISPATCH(debugCall32<>, 32)
DEBUG_CALL_DISPATCH(debugCall64<>, 64)
DEBUG_CALL_DISPATCH(debugCall128<>, 128)
DEBUG_CALL_DISPATCH(debugCall256<>, 256)
DEBUG_CALL_DISPATCH(debugCall512<>, 512)
DEBUG_CALL_DISPATCH(debugCall1024<>, 1024)
DEBUG_CALL_DISPATCH(debugCall2048<>, 2048)
DEBUG_CALL_DISPATCH(debugCall4096<>, 4096)
DEBUG_CALL_DISPATCH(debugCall8192<>, 8192)
DEBUG_CALL_DISPATCH(debugCall16384<>, 16384)
DEBUG_CALL_DISPATCH(debugCall32768<>, 32768)
DEBUG_CALL_DISPATCH(debugCall65536<>, 65536)
// The frame size is too large. Report the error.
MOVQ $debugCallFrameTooLarge<>(SB), AX
MOVQ AX, 0(SP)
MOVQ $0x14, 8(SP)
MOVQ $8, AX
BYTE $0xcc
JMP restore

restore:
// Calls and failures resume here.
//
// Set AX to 16 and invoke INT3. The debugger should restore
// all registers except RIP and RSP and resume execution.
MOVQ $16, AX
BYTE $0xcc
// We must not modify flags after this point.

// Restore pointer-containing registers, which may have been
// modified from the debugger's copy by stack copying.
MOVQ ax-(0*8+8)(SP), AX
MOVQ cx-(1*8+8)(SP), CX
MOVQ dx-(2*8+8)(SP), DX
MOVQ bx-(3*8+8)(SP), BX
MOVQ bp-(4*8+8)(SP), BP
MOVQ si-(5*8+8)(SP), SI
MOVQ di-(6*8+8)(SP), DI
MOVQ r8-(7*8+8)(SP), R8
MOVQ r9-(8*8+8)(SP), R9
MOVQ r10-(9*8+8)(SP), R10
MOVQ r11-(10*8+8)(SP), R11
MOVQ r12-(11*8+8)(SP), R12
MOVQ r13-(12*8+8)(SP), R13
MOVQ r14-(13*8+8)(SP), R14
MOVQ r15-(14*8+8)(SP), R15

RET

#define DEBUG_CALL_FN(NAME,MAXSIZE) \
TEXT NAME(SB),WRAPPER,$MAXSIZE-0; \
NO_LOCAL_POINTERS; \
MOVQ $0, AX; \
BYTE $0xcc; \
MOVQ $1, AX; \
BYTE $0xcc; \
RET
DEBUG_CALL_FN(debugCall32<>, 32)
DEBUG_CALL_FN(debugCall64<>, 64)
DEBUG_CALL_FN(debugCall128<>, 128)
DEBUG_CALL_FN(debugCall256<>, 256)
DEBUG_CALL_FN(debugCall512<>, 512)
DEBUG_CALL_FN(debugCall1024<>, 1024)
DEBUG_CALL_FN(debugCall2048<>, 2048)
DEBUG_CALL_FN(debugCall4096<>, 4096)
DEBUG_CALL_FN(debugCall8192<>, 8192)
DEBUG_CALL_FN(debugCall16384<>, 16384)
DEBUG_CALL_FN(debugCall32768<>, 32768)
DEBUG_CALL_FN(debugCall65536<>, 65536)

TEXT runtime·debugCallPanicked(SB),NOSPLIT,$16-16
// Copy the panic value to the top of stack.
MOVQ val_type+0(FP), AX
MOVQ AX, 0(SP)
MOVQ val_data+8(FP), AX
MOVQ AX, 8(SP)
MOVQ $2, AX
BYTE $0xcc
RET
Loading

0 comments on commit c5ed10f

Please sign in to comment.