delve/pkg/proc/stackwatch.go
Alessandro Arzilli 348c722981
proc/gdbserver: support watchpoints (#2659)
Adds watchpoint support to gdbserver backend for rr debugger and
debugserver on macOS/amd64 and macOS/arm64.

Also changes stack watchpoints to support reverse execution.
2021-10-04 14:45:05 -07:00

193 lines
5.9 KiB
Go

package proc
import (
"errors"
"github.com/go-delve/delve/pkg/astutil"
"github.com/go-delve/delve/pkg/logflags"
)
// This file implements most of the details needed to support stack
// watchpoints. Some of the remaining details are in breakpoints, along with
// the code to support non-stack allocated watchpoints.
//
// In Go goroutine stacks start small and are frequently resized by the
// runtime according to the needs of the goroutine.
// To support this behavior we create a StackResizeBreakpoint, deep inside
// the Go runtime, when this breakpoint is hit all the stack watchpoints on
// the goroutine being resized are adjusted for the new stack.
// Furthermore, we need to detect when a goroutine leaves the stack frame
// where the variable we are watching was declared, so that we can notify
// the user that the variable went out of scope and clear the watchpoint.
//
// These breakpoints are created by setStackWatchBreakpoints and cleared by
// clearStackWatchBreakpoints.
// setStackWatchBreakpoints sets the out of scope sentinel breakpoints for
// watchpoint and a stack resize breakpoint.
func (t *Target) setStackWatchBreakpoints(scope *EvalScope, watchpoint *Breakpoint) error {
// Watchpoint Out-of-scope Sentinel
woos := func(_ Thread) bool {
watchpointOutOfScope(t, watchpoint)
return true
}
topframe, retframe, err := topframe(scope.g, nil)
if err != nil {
return err
}
sameGCond := sameGoroutineCondition(scope.g)
retFrameCond := astutil.And(sameGCond, frameoffCondition(&retframe))
var deferpc uint64
if topframe.TopmostDefer != nil {
_, _, deferfn := topframe.TopmostDefer.DeferredFunc(t)
if deferfn != nil {
var err error
deferpc, err = FirstPCAfterPrologue(t, deferfn, false)
if err != nil {
return err
}
}
}
if deferpc != 0 && deferpc != topframe.Current.PC {
deferbp, err := t.SetBreakpoint(deferpc, WatchOutOfScopeBreakpoint, sameGCond)
if err != nil {
return err
}
deferbreaklet := deferbp.Breaklets[len(deferbp.Breaklets)-1]
deferbreaklet.checkPanicCall = true
deferbreaklet.watchpoint = watchpoint
deferbreaklet.callback = woos
}
retbp, err := t.SetBreakpoint(retframe.Current.PC, WatchOutOfScopeBreakpoint, retFrameCond)
if err != nil {
return err
}
retbreaklet := retbp.Breaklets[len(retbp.Breaklets)-1]
retbreaklet.watchpoint = watchpoint
retbreaklet.callback = woos
if recorded, _ := t.Recorded(); recorded && retframe.Current.Fn != nil {
// Must also set a breakpoint on the call instruction immediately
// preceding retframe.Current.PC, because the watchpoint could also go out
// of scope while we are running backwards.
callerText, err := disassemble(t.Memory(), nil, t.Breakpoints(), t.BinInfo(), retframe.Current.Fn.Entry, retframe.Current.Fn.End, false)
if err != nil {
return err
}
for i, instr := range callerText {
if instr.Loc.PC == retframe.Current.PC && i > 0 {
retbp2, err := t.SetBreakpoint(callerText[i-1].Loc.PC, WatchOutOfScopeBreakpoint, retFrameCond)
if err != nil {
return err
}
retbreaklet2 := retbp2.Breaklets[len(retbp.Breaklets)-1]
retbreaklet2.watchpoint = watchpoint
retbreaklet2.callback = woos
break
}
}
}
// Stack Resize Sentinel
fn := t.BinInfo().LookupFunc["runtime.copystack"]
if fn == nil {
return errors.New("could not find runtime.copystack")
}
text, err := Disassemble(t.Memory(), nil, t.Breakpoints(), t.BinInfo(), fn.Entry, fn.End)
if err != nil {
return err
}
var retpc uint64
for _, instr := range text {
if instr.IsRet() {
if retpc != 0 {
return errors.New("runtime.copystack has too many return instructions")
}
retpc = instr.Loc.PC
}
}
if retpc == 0 {
return errors.New("could not find return instruction in runtime.copystack")
}
rszbp, err := t.SetBreakpoint(retpc, StackResizeBreakpoint, sameGCond)
if err != nil {
return err
}
rszbreaklet := rszbp.Breaklets[len(rszbp.Breaklets)-1]
rszbreaklet.watchpoint = watchpoint
rszbreaklet.callback = func(th Thread) bool {
adjustStackWatchpoint(t, th, watchpoint)
return false // we never want this breakpoint to be shown to the user
}
return nil
}
// clearStackWatchBreakpoints clears all accessory breakpoints for
// watchpoint.
func (t *Target) clearStackWatchBreakpoints(watchpoint *Breakpoint) error {
bpmap := t.Breakpoints()
for _, bp := range bpmap.M {
changed := false
for i, breaklet := range bp.Breaklets {
if breaklet.watchpoint == watchpoint {
bp.Breaklets[i] = nil
changed = true
}
}
if changed {
_, err := t.finishClearBreakpoint(bp)
if err != nil {
return err
}
}
}
return nil
}
// watchpointOutOfScope is called when the watchpoint goes out of scope. It
// is used as a breaklet callback function.
// Its responsibility is to delete the watchpoint and make sure that the
// user is notified of the watchpoint going out of scope.
func watchpointOutOfScope(t *Target, watchpoint *Breakpoint) {
t.Breakpoints().WatchOutOfScope = append(t.Breakpoints().WatchOutOfScope, watchpoint)
err := t.ClearBreakpoint(watchpoint.Addr)
if err != nil {
log := logflags.DebuggerLogger()
log.Errorf("could not clear out-of-scope watchpoint: %v", err)
}
}
// adjustStackWatchpoint is called when the goroutine of watchpoint resizes
// its stack. It is used as a breaklet callback function.
// Its responsibility is to move the watchpoint to a its new address.
func adjustStackWatchpoint(t *Target, th Thread, watchpoint *Breakpoint) {
g, _ := GetG(th)
if g == nil {
return
}
err := t.proc.EraseBreakpoint(watchpoint)
if err != nil {
log := logflags.DebuggerLogger()
log.Errorf("could not adjust watchpoint at %#x: %v", watchpoint.Addr, err)
return
}
delete(t.Breakpoints().M, watchpoint.Addr)
watchpoint.Addr = uint64(int64(g.stack.hi) + watchpoint.watchStackOff)
err = t.proc.WriteBreakpoint(watchpoint)
if err != nil {
log := logflags.DebuggerLogger()
log.Errorf("could not adjust watchpoint at %#x: %v", watchpoint.Addr, err)
return
}
t.Breakpoints().M[watchpoint.Addr] = watchpoint
}