
Previously breakpoints with hitcount conditions that became unsatisfiable would become disabled, this was done as an optimization so that the continue loop would no longer need to stop on them and evaluate their conditions. As a side effect this meant that on restart these breakpoints would remain disabled, even though their hit condition returned satisfiable. This commit changes Delve behavior so that breakpoints with unsatisifiable hitcount conditions are no longer disabled but the associated physical breakpoints are removed anyway, preserving the optimization. Some refactoring is done to the way conditions are represented and the enable status is managed so that in the future it will be possible to use hitcount conditions to implement "chained" breakpoints (also known as dependet breakpoints), i.e. breakpoints that become active only after a second breakpoint has been hit.
1859 lines
56 KiB
Go
1859 lines
56 KiB
Go
package proc
|
|
|
|
import (
|
|
"bytes"
|
|
"debug/dwarf"
|
|
"errors"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/constant"
|
|
"go/token"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"golang.org/x/arch/ppc64/ppc64asm"
|
|
|
|
"github.com/go-delve/delve/pkg/astutil"
|
|
"github.com/go-delve/delve/pkg/dwarf/godwarf"
|
|
"github.com/go-delve/delve/pkg/dwarf/reader"
|
|
"github.com/go-delve/delve/pkg/logflags"
|
|
)
|
|
|
|
const maxSkipAutogeneratedWrappers = 5 // maximum recursion depth for skipAutogeneratedWrappers
|
|
|
|
// ErrNoSourceForPC is returned when the given address
|
|
// does not correspond with a source file location.
|
|
type ErrNoSourceForPC struct {
|
|
pc uint64
|
|
}
|
|
|
|
func (err *ErrNoSourceForPC) Error() string {
|
|
return fmt.Sprintf("no source for PC %#x", err.pc)
|
|
}
|
|
|
|
// Next resumes the processes in the group, continuing the selected target
|
|
// until the next source line.
|
|
func (grp *TargetGroup) Next() (err error) {
|
|
if _, err := grp.Valid(); err != nil {
|
|
return err
|
|
}
|
|
if grp.HasSteppingBreakpoints() {
|
|
return errors.New("next while nexting")
|
|
}
|
|
|
|
if err = next(grp.Selected, false, false); err != nil {
|
|
grp.Selected.ClearSteppingBreakpoints()
|
|
return
|
|
}
|
|
|
|
return grp.Continue()
|
|
}
|
|
|
|
// Continue continues execution of the debugged
|
|
// processes. It will continue until it hits a breakpoint
|
|
// or is otherwise stopped.
|
|
func (grp *TargetGroup) Continue() error {
|
|
if grp.numValid() == 0 {
|
|
_, err := grp.targets[0].Valid()
|
|
return err
|
|
}
|
|
for _, dbp := range grp.targets {
|
|
if isvalid, _ := dbp.Valid(); !isvalid {
|
|
continue
|
|
}
|
|
for _, thread := range dbp.ThreadList() {
|
|
thread.Common().CallReturn = false
|
|
thread.Common().returnValues = nil
|
|
}
|
|
dbp.Breakpoints().WatchOutOfScope = nil
|
|
dbp.clearHardcodedBreakpoints()
|
|
}
|
|
grp.cctx.CheckAndClearManualStopRequest()
|
|
defer func() {
|
|
// Make sure we clear internal breakpoints if we simultaneously receive a
|
|
// manual stop request and hit a breakpoint.
|
|
if grp.cctx.CheckAndClearManualStopRequest() {
|
|
grp.finishManualStop()
|
|
}
|
|
}()
|
|
for {
|
|
err := grp.manageUnsatisfiableBreakpoints()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if grp.cctx.CheckAndClearManualStopRequest() {
|
|
grp.finishManualStop()
|
|
return nil
|
|
}
|
|
for _, dbp := range grp.targets {
|
|
dbp.ClearCaches()
|
|
}
|
|
logflags.DebuggerLogger().Debugf("ContinueOnce")
|
|
trapthread, stopReason, contOnceErr := grp.procgrp.ContinueOnce(grp.cctx)
|
|
var traptgt *Target
|
|
if trapthread != nil {
|
|
traptgt = grp.TargetForThread(trapthread.ThreadID())
|
|
if traptgt == nil {
|
|
return fmt.Errorf("could not find target for thread %d", trapthread.ThreadID())
|
|
}
|
|
} else {
|
|
traptgt = grp.targets[0]
|
|
}
|
|
traptgt.StopReason = stopReason
|
|
|
|
it := ValidTargets{Group: grp}
|
|
for it.Next() {
|
|
// Both selectedGoroutine and current thread are stale here, since we can
|
|
// only set their definitive value *after* evaluating breakpoint
|
|
// conditions here we give them temporary non-stale values.
|
|
it.selectedGoroutine = nil
|
|
curthread := it.currentThread
|
|
for _, thread := range it.ThreadList() {
|
|
if thread.Breakpoint().Breakpoint != nil {
|
|
it.currentThread = thread
|
|
thread.Breakpoint().Breakpoint.checkCondition(it.Target, thread, thread.Breakpoint())
|
|
}
|
|
}
|
|
it.currentThread = curthread
|
|
// Clear watchpoints that have gone out of scope
|
|
for _, watchpoint := range it.Breakpoints().WatchOutOfScope {
|
|
err := it.ClearBreakpoint(watchpoint.Addr)
|
|
if err != nil {
|
|
logflags.DebuggerLogger().Errorf("could not clear out-of-scope watchpoint: %v", err)
|
|
}
|
|
delete(it.Breakpoints().Logical, watchpoint.LogicalID())
|
|
}
|
|
// Clear inactivated breakpoints
|
|
err := it.clearInactivatedSteppingBreakpoint()
|
|
if err != nil {
|
|
logflags.DebuggerLogger().Errorf("could not clear inactivated stepping breakpoints: %v", err)
|
|
}
|
|
}
|
|
|
|
if contOnceErr != nil {
|
|
// Attempt to refresh status of current thread/current goroutine, see
|
|
// Issue #2078.
|
|
// Errors are ignored because depending on why ContinueOnce failed this
|
|
// might very well not work.
|
|
_ = grp.setCurrentThreads(traptgt, trapthread)
|
|
if pe, ok := contOnceErr.(ErrProcessExited); ok {
|
|
traptgt.exitStatus = pe.Status
|
|
}
|
|
return contOnceErr
|
|
}
|
|
if stopReason == StopLaunched {
|
|
it.Reset()
|
|
for it.Next() {
|
|
it.Target.ClearSteppingBreakpoints()
|
|
}
|
|
}
|
|
|
|
var callInjectionDone bool
|
|
var callErr error
|
|
var hcbpErr error
|
|
it.Reset()
|
|
for it.Next() {
|
|
dbp := it.Target
|
|
threads := dbp.ThreadList()
|
|
if logflags.Debugger() {
|
|
log := logflags.DebuggerLogger()
|
|
log.Debugf("callInjection protocol on:")
|
|
for _, th := range threads {
|
|
regs, _ := th.Registers()
|
|
log.Debugf("\t%d PC=%#x", th.ThreadID(), regs.PC())
|
|
}
|
|
}
|
|
callInjectionDoneThis, callErrThis := callInjectionProtocol(dbp, trapthread, threads)
|
|
callInjectionDone = callInjectionDone || callInjectionDoneThis
|
|
if callInjectionDoneThis {
|
|
dbp.StopReason = StopCallReturned
|
|
}
|
|
if callErrThis != nil && callErr == nil {
|
|
callErr = callErrThis
|
|
}
|
|
hcbpErrThis := dbp.handleHardcodedBreakpoints(grp, trapthread, threads)
|
|
if hcbpErrThis != nil && hcbpErr == nil {
|
|
hcbpErr = hcbpErrThis
|
|
}
|
|
}
|
|
// callErr and hcbpErr check delayed until after pickCurrentThread, which
|
|
// must always happen, otherwise the debugger could be left in an
|
|
// inconsistent state.
|
|
|
|
it = ValidTargets{Group: grp}
|
|
for it.Next() {
|
|
var th Thread = nil
|
|
if it.Target == traptgt {
|
|
th = trapthread
|
|
}
|
|
err := pickCurrentThread(it.Target, th)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
grp.pickCurrentTarget(traptgt)
|
|
dbp := grp.Selected
|
|
|
|
if callErr != nil {
|
|
return callErr
|
|
}
|
|
if hcbpErr != nil {
|
|
return hcbpErr
|
|
}
|
|
|
|
curthread := dbp.CurrentThread()
|
|
curbp := curthread.Breakpoint()
|
|
|
|
switch {
|
|
case curbp.Active && curbp.Stepping:
|
|
switch {
|
|
case curbp.SteppingInto:
|
|
// See description of proc.(*Process).next for the meaning of StepBreakpoints
|
|
if err := conditionErrors(grp); err != nil {
|
|
return err
|
|
}
|
|
if grp.GetDirection() == Backward {
|
|
if err := dbp.ClearSteppingBreakpoints(); err != nil {
|
|
return err
|
|
}
|
|
return grp.StepInstruction(false)
|
|
}
|
|
case curbp.SteppingIntoRangeOverFuncBody:
|
|
if err := conditionErrors(grp); err != nil {
|
|
return err
|
|
}
|
|
if err := dbp.ClearSteppingBreakpoints(); err != nil {
|
|
return err
|
|
}
|
|
if err := next(dbp, false, false); err != nil {
|
|
return err
|
|
}
|
|
// Target execution continues...
|
|
default:
|
|
curthread.Common().returnValues = curbp.Breakpoint.returnInfo.Collect(dbp, curthread)
|
|
if err := dbp.ClearSteppingBreakpoints(); err != nil {
|
|
return err
|
|
}
|
|
dbp.StopReason = StopNextFinished
|
|
return conditionErrors(grp)
|
|
}
|
|
case curbp.Active:
|
|
onNextGoroutine, err := onNextGoroutine(dbp, curthread, dbp.Breakpoints())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if onNextGoroutine &&
|
|
(!isTraceOrTraceReturn(curbp.Breakpoint) || grp.KeepSteppingBreakpoints&TracepointKeepsSteppingBreakpoints == 0) {
|
|
err := dbp.ClearSteppingBreakpoints()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if curbp.LogicalID() == unrecoveredPanicID {
|
|
dbp.ClearSteppingBreakpoints()
|
|
}
|
|
if curbp.LogicalID() != hardcodedBreakpointID {
|
|
dbp.StopReason = StopBreakpoint
|
|
}
|
|
if curbp.Breakpoint.WatchType != 0 {
|
|
dbp.StopReason = StopWatchpoint
|
|
}
|
|
return conditionErrors(grp)
|
|
case stopReason == StopLaunched:
|
|
return nil
|
|
default:
|
|
// not a manual stop, not on runtime.Breakpoint, not on a breakpoint, just repeat
|
|
}
|
|
if callInjectionDone {
|
|
// a call injection was finished, don't let a breakpoint with a failed
|
|
// condition or a step breakpoint shadow this.
|
|
return conditionErrors(grp)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (grp *TargetGroup) finishManualStop() {
|
|
for _, dbp := range grp.targets {
|
|
if isvalid, _ := dbp.Valid(); !isvalid {
|
|
continue
|
|
}
|
|
dbp.StopReason = StopManual
|
|
dbp.clearHardcodedBreakpoints()
|
|
if grp.KeepSteppingBreakpoints&HaltKeepsSteppingBreakpoints == 0 {
|
|
dbp.ClearSteppingBreakpoints()
|
|
}
|
|
}
|
|
}
|
|
|
|
// setCurrentThreads switches traptgt to trapthread, then for each target in
|
|
// the group if its current thread exists it refreshes the current
|
|
// goroutine, otherwise it switches it to a randomly selected thread.
|
|
func (grp *TargetGroup) setCurrentThreads(traptgt *Target, trapthread Thread) error {
|
|
var err error
|
|
if traptgt != nil && trapthread != nil {
|
|
err = traptgt.SwitchThread(trapthread.ThreadID())
|
|
}
|
|
for _, tgt := range grp.targets {
|
|
if isvalid, _ := tgt.Valid(); !isvalid {
|
|
continue
|
|
}
|
|
if _, ok := tgt.FindThread(tgt.currentThread.ThreadID()); ok {
|
|
tgt.selectedGoroutine, _ = GetG(tgt.currentThread)
|
|
} else {
|
|
threads := tgt.ThreadList()
|
|
if len(threads) > 0 {
|
|
err1 := tgt.SwitchThread(threads[0].ThreadID())
|
|
if err1 != nil && err == nil {
|
|
err = err1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
func isTraceOrTraceReturn(bp *Breakpoint) bool {
|
|
if bp.Logical == nil {
|
|
return false
|
|
}
|
|
return bp.Logical.Tracepoint || bp.Logical.TraceReturn
|
|
}
|
|
|
|
func conditionErrors(grp *TargetGroup) error {
|
|
var condErr error
|
|
for _, dbp := range grp.targets {
|
|
if isvalid, _ := dbp.Valid(); !isvalid {
|
|
continue
|
|
}
|
|
for _, th := range dbp.ThreadList() {
|
|
if bp := th.Breakpoint(); bp.Breakpoint != nil && bp.CondError != nil {
|
|
if condErr == nil {
|
|
condErr = bp.CondError
|
|
} else {
|
|
return errors.New("multiple errors evaluating conditions")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return condErr
|
|
}
|
|
|
|
// pick a new dbp.currentThread, with the following priority:
|
|
//
|
|
// - a thread with an active stepping breakpoint
|
|
// - a thread with an active breakpoint, prioritizing trapthread
|
|
// - trapthread if it is not nil
|
|
// - the previous current thread if it still exists
|
|
// - a randomly selected thread
|
|
func pickCurrentThread(dbp *Target, trapthread Thread) error {
|
|
threads := dbp.ThreadList()
|
|
for _, th := range threads {
|
|
if bp := th.Breakpoint(); bp.Active && bp.Stepping {
|
|
return dbp.SwitchThread(th.ThreadID())
|
|
}
|
|
}
|
|
if trapthread != nil {
|
|
if bp := trapthread.Breakpoint(); bp.Active {
|
|
return dbp.SwitchThread(trapthread.ThreadID())
|
|
}
|
|
}
|
|
for _, th := range threads {
|
|
if bp := th.Breakpoint(); bp.Active {
|
|
return dbp.SwitchThread(th.ThreadID())
|
|
}
|
|
}
|
|
if trapthread != nil {
|
|
return dbp.SwitchThread(trapthread.ThreadID())
|
|
}
|
|
if _, ok := dbp.FindThread(dbp.currentThread.ThreadID()); ok {
|
|
dbp.selectedGoroutine, _ = GetG(dbp.currentThread)
|
|
return nil
|
|
}
|
|
if len(threads) > 0 {
|
|
return dbp.SwitchThread(threads[0].ThreadID())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// pickCurrentTarget picks a new current target, with the following property:
|
|
//
|
|
// - a target with an active stepping breakpoint
|
|
// - a target with StopReason == StopCallReturned
|
|
// - a target with an active breakpoint, prioritizing traptgt
|
|
// - traptgt
|
|
func (grp *TargetGroup) pickCurrentTarget(traptgt *Target) {
|
|
if len(grp.targets) == 1 {
|
|
grp.Selected = grp.targets[0]
|
|
return
|
|
}
|
|
for _, dbp := range grp.targets {
|
|
if isvalid, _ := dbp.Valid(); !isvalid {
|
|
continue
|
|
}
|
|
bp := dbp.currentThread.Breakpoint()
|
|
if bp.Active && bp.Stepping {
|
|
grp.Selected = dbp
|
|
return
|
|
}
|
|
}
|
|
for _, dbp := range grp.targets {
|
|
if isvalid, _ := dbp.Valid(); !isvalid {
|
|
continue
|
|
}
|
|
if dbp.StopReason == StopCallReturned {
|
|
grp.Selected = dbp
|
|
return
|
|
}
|
|
}
|
|
|
|
if traptgt.currentThread.Breakpoint().Active {
|
|
grp.Selected = traptgt
|
|
return
|
|
}
|
|
for _, dbp := range grp.targets {
|
|
if isvalid, _ := dbp.Valid(); !isvalid {
|
|
continue
|
|
}
|
|
bp := dbp.currentThread.Breakpoint()
|
|
if bp.Active {
|
|
grp.Selected = dbp
|
|
return
|
|
}
|
|
}
|
|
grp.Selected = traptgt
|
|
}
|
|
|
|
func disassembleCurrentInstruction(p Process, thread Thread, off int64) ([]AsmInstruction, error) {
|
|
regs, err := thread.Registers()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pc := regs.PC() + uint64(off)
|
|
return disassemble(p.Memory(), regs, p.Breakpoints(), p.BinInfo(), pc, pc+uint64(p.BinInfo().Arch.MaxInstructionLength()), true)
|
|
}
|
|
|
|
// stepInstructionOut repeatedly calls StepInstruction until the current
|
|
// function is neither fnname1 or fnname2.
|
|
// This function is used to step out of runtime.Breakpoint as well as
|
|
// runtime.debugCallV1.
|
|
func stepInstructionOut(grp *TargetGroup, dbp *Target, curthread Thread, fnname1, fnname2 string) error {
|
|
defer dbp.ClearCaches()
|
|
for {
|
|
if err := grp.procgrp.StepInstruction(curthread.ThreadID()); err != nil {
|
|
return err
|
|
}
|
|
loc, err := curthread.Location()
|
|
var locFnName string
|
|
if loc.Fn != nil && !loc.Fn.cu.image.Stripped() {
|
|
locFnName = loc.Fn.Name
|
|
// Calls to runtime.Breakpoint are inlined in some versions of Go when
|
|
// inlining is enabled. Here we attempt to resolve any inlining.
|
|
dwarfTree, _ := loc.Fn.cu.image.getDwarfTree(loc.Fn.offset)
|
|
if dwarfTree != nil {
|
|
inlstack := reader.InlineStack(dwarfTree, loc.PC)
|
|
if len(inlstack) > 0 {
|
|
if locFnName2, ok := inlstack[0].Val(dwarf.AttrName).(string); ok {
|
|
locFnName = locFnName2
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if err != nil || loc.Fn == nil || (locFnName != fnname1 && locFnName != fnname2) {
|
|
g, _ := GetG(curthread)
|
|
selg := dbp.SelectedGoroutine()
|
|
if g != nil && selg != nil && g.ID == selg.ID {
|
|
selg.CurrentLoc = *loc
|
|
}
|
|
return curthread.SetCurrentBreakpoint(true)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Step resumes the processes in the group, continuing the selected target
|
|
// until the next source line. Will step into functions.
|
|
func (grp *TargetGroup) Step() (err error) {
|
|
if _, err := grp.Valid(); err != nil {
|
|
return err
|
|
}
|
|
if grp.HasSteppingBreakpoints() {
|
|
return errors.New("next while nexting")
|
|
}
|
|
|
|
if err = next(grp.Selected, true, false); err != nil {
|
|
_ = grp.Selected.ClearSteppingBreakpoints()
|
|
return err
|
|
}
|
|
|
|
if bpstate := grp.Selected.CurrentThread().Breakpoint(); bpstate.Breakpoint != nil && bpstate.Active && bpstate.SteppingInto && grp.GetDirection() == Backward {
|
|
grp.Selected.ClearSteppingBreakpoints()
|
|
return grp.StepInstruction(false)
|
|
}
|
|
|
|
return grp.Continue()
|
|
}
|
|
|
|
// sameGoroutineCondition returns an expression that evaluates to true when
|
|
// the current goroutine is g.
|
|
func sameGoroutineCondition(bi *BinaryInfo, g *G, threadID int) ast.Expr {
|
|
if g == nil {
|
|
if len(bi.Images[0].compileUnits) == 0 {
|
|
// It's unclear what the right behavior is here. We are probably
|
|
// debugging a process without debug info, this means we can't properly
|
|
// create a same goroutine condition (we don't have a description for the
|
|
// runtime.g type). If we don't set the condition then 'next' (and step,
|
|
// stepout) will work for single-threaded programs (in limited
|
|
// circumstances) but fail in presence of any concurrency.
|
|
// If we set a thread ID condition even single threaded programs can fail
|
|
// due to goroutine migration, but sometimes it will work even with
|
|
// concurrency.
|
|
return nil
|
|
}
|
|
return astutil.Eql(astutil.PkgVar("runtime", "threadid"), astutil.Int(int64(threadID)))
|
|
}
|
|
return astutil.Eql(astutil.Sel(astutil.PkgVar("runtime", "curg"), "goid"), astutil.Int(g.ID))
|
|
}
|
|
|
|
func frameoffCondition(frame *Stackframe) ast.Expr {
|
|
return astutil.Eql(astutil.PkgVar("runtime", "frameoff"), astutil.Int(frame.FrameOffset()))
|
|
}
|
|
|
|
// StepOut resumes the processes in the group, continuing the selected target
|
|
// until the current goroutine exits the function currently being
|
|
// executed or a deferred function is executed
|
|
func (grp *TargetGroup) StepOut() error {
|
|
backward := grp.GetDirection() == Backward
|
|
if _, err := grp.Valid(); err != nil {
|
|
return err
|
|
}
|
|
if grp.HasSteppingBreakpoints() {
|
|
return errors.New("next while nexting")
|
|
}
|
|
|
|
dbp := grp.Selected
|
|
selg := dbp.SelectedGoroutine()
|
|
curthread := dbp.CurrentThread()
|
|
|
|
topframe, retframe, err := topframe(dbp, selg, curthread)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
rangeFrames, err := rangeFuncStackTrace(dbp, selg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if rangeFrames != nil {
|
|
// There are range-over-func body closures skip all of them to the
|
|
// function containing them and its caller function.
|
|
topframe, retframe = rangeFrames[len(rangeFrames)-2], rangeFrames[len(rangeFrames)-1]
|
|
}
|
|
|
|
success := false
|
|
defer func() {
|
|
if !success {
|
|
dbp.ClearSteppingBreakpoints()
|
|
}
|
|
}()
|
|
|
|
if topframe.Inlined {
|
|
if err := next(dbp, false, true); err != nil {
|
|
return err
|
|
}
|
|
|
|
success = true
|
|
return grp.Continue()
|
|
}
|
|
|
|
sameGCond := sameGoroutineCondition(dbp.BinInfo(), selg, curthread.ThreadID())
|
|
|
|
if backward {
|
|
if err := stepOutReverse(dbp, topframe, retframe, sameGCond); err != nil {
|
|
return err
|
|
}
|
|
|
|
success = true
|
|
return grp.Continue()
|
|
}
|
|
|
|
deferpc, err := setDeferBreakpoint(dbp, nil, topframe, sameGCond, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if topframe.Ret == 0 && deferpc == 0 {
|
|
return errors.New("nothing to stepout to")
|
|
}
|
|
|
|
if topframe.Ret != 0 {
|
|
topframe, retframe := skipAutogeneratedWrappersOut(grp.Selected, selg, curthread, &topframe, &retframe)
|
|
retFrameCond := astutil.And(sameGCond, frameoffCondition(retframe))
|
|
bp, err := allowDuplicateBreakpoint(dbp.SetBreakpoint(0, retframe.Current.PC, NextBreakpoint, retFrameCond))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if bp != nil {
|
|
configureReturnBreakpoint(dbp.BinInfo(), bp, topframe, retFrameCond)
|
|
}
|
|
}
|
|
|
|
if bp := curthread.Breakpoint(); bp.Breakpoint == nil {
|
|
curthread.SetCurrentBreakpoint(false)
|
|
}
|
|
|
|
success = true
|
|
return grp.Continue()
|
|
}
|
|
|
|
// StepInstruction will continue the current thread for exactly
|
|
// one instruction. This method affects only the thread
|
|
// associated with the selected goroutine. All other
|
|
// threads will remain stopped.
|
|
func (grp *TargetGroup) StepInstruction(skipCalls bool) (err error) {
|
|
dbp := grp.Selected
|
|
thread := dbp.CurrentThread()
|
|
g := dbp.SelectedGoroutine()
|
|
if g != nil {
|
|
if g.Thread == nil {
|
|
// Step called on parked goroutine
|
|
if _, err := dbp.SetBreakpoint(0, g.PC, NextBreakpoint,
|
|
sameGoroutineCondition(dbp.BinInfo(), dbp.SelectedGoroutine(), thread.ThreadID())); err != nil {
|
|
return err
|
|
}
|
|
return grp.Continue()
|
|
}
|
|
thread = g.Thread
|
|
}
|
|
dbp.ClearCaches()
|
|
if ok, err := dbp.Valid(); !ok {
|
|
return err
|
|
}
|
|
var isCall bool
|
|
instr, err := disassembleCurrentInstruction(dbp, thread, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
isCall = len(instr) > 0 && instr[0].IsCall()
|
|
err = grp.procgrp.StepInstruction(thread.ThreadID())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
thread.Breakpoint().Clear()
|
|
err = thread.SetCurrentBreakpoint(false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if tg, _ := GetG(thread); tg != nil {
|
|
dbp.selectedGoroutine = tg
|
|
}
|
|
dbp.StopReason = StopNextFinished
|
|
|
|
if skipCalls && isCall {
|
|
return grp.StepOut()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Set breakpoints at every line, and the return address. Also look for
|
|
// a deferred function and set a breakpoint there too.
|
|
// If stepInto is true it will also set breakpoints inside all
|
|
// functions called on the current source line, for non-absolute CALLs
|
|
// a breakpoint of kind StepBreakpoint is set on the CALL instruction,
|
|
// Continue will take care of setting a breakpoint to the destination
|
|
// once the CALL is reached.
|
|
//
|
|
// Regardless of stepInto the following breakpoints will be set:
|
|
// - a breakpoint on the first deferred function with NextDeferBreakpoint
|
|
// kind, the list of all the addresses to deferreturn calls in this function
|
|
// and condition checking that we remain on the same goroutine
|
|
// - a breakpoint on each line of the function, with a condition checking
|
|
// that we stay on the same stack frame and goroutine.
|
|
// - a breakpoint on the return address of the function, with a condition
|
|
// checking that we move to the previous stack frame and stay on the same
|
|
// goroutine.
|
|
//
|
|
// The breakpoint on the return address is *not* set if the current frame is
|
|
// an inlined call. For inlined calls topframe.Current.Fn is the function
|
|
// where the inlining happened and the second set of breakpoints will also
|
|
// cover the "return address".
|
|
//
|
|
// If inlinedStepOut is true this function implements the StepOut operation
|
|
// for an inlined function call. Everything works the same as normal except
|
|
// when removing instructions belonging to inlined calls we also remove all
|
|
// instructions belonging to the current inlined call.
|
|
func next(dbp *Target, stepInto, inlinedStepOut bool) error {
|
|
backward := dbp.recman.GetDirection() == Backward
|
|
selg := dbp.SelectedGoroutine()
|
|
curthread := dbp.CurrentThread()
|
|
bi := dbp.BinInfo()
|
|
topframe, retframe, err := topframe(dbp, selg, curthread)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if topframe.Current.Fn == nil {
|
|
return &ErrNoSourceForPC{topframe.Current.PC}
|
|
}
|
|
|
|
if backward && retframe.Current.Fn == nil {
|
|
return &ErrNoSourceForPC{retframe.Current.PC}
|
|
}
|
|
|
|
// sanity check
|
|
if inlinedStepOut && !topframe.Inlined {
|
|
panic("next called with inlinedStepOut but topframe was not inlined")
|
|
}
|
|
|
|
rangeFrames, err := rangeFuncStackTrace(dbp, selg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
success := false
|
|
defer func() {
|
|
if !success {
|
|
dbp.ClearSteppingBreakpoints()
|
|
}
|
|
}()
|
|
|
|
ext := filepath.Ext(topframe.Current.File)
|
|
csource := ext != ".go" && ext != ".s"
|
|
var regs Registers
|
|
if selg != nil && selg.Thread != nil {
|
|
regs, err = selg.Thread.Registers()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
sameGCond := sameGoroutineCondition(bi, selg, curthread.ThreadID())
|
|
|
|
firstPCAfterPrologue, err := FirstPCAfterPrologue(dbp, topframe.Current.Fn, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if backward {
|
|
if firstPCAfterPrologue == topframe.Current.PC {
|
|
// We don't want to step into the prologue so we just execute a reverse step out instead
|
|
if err := stepOutReverse(dbp, topframe, retframe, sameGCond); err != nil {
|
|
return err
|
|
}
|
|
|
|
success = true
|
|
return nil
|
|
}
|
|
|
|
topframe.Ret, err = findCallInstrForRet(dbp, dbp.Memory(), topframe.Ret, retframe.Current.Fn)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
text, err := disassemble(dbp.Memory(), regs, dbp.Breakpoints(), bi, topframe.Current.Fn.Entry, topframe.Current.Fn.End, false)
|
|
if err != nil && stepInto {
|
|
return err
|
|
}
|
|
|
|
sameFrameCond := astutil.And(sameGCond, frameoffCondition(&topframe))
|
|
|
|
if stepInto && !backward {
|
|
err := setStepIntoBreakpoints(dbp, topframe.Current.Fn, text, topframe, sameGCond)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if !backward && !topframe.Current.Fn.cu.image.Stripped() {
|
|
fr := topframe
|
|
if len(rangeFrames) != 0 && !stepInto {
|
|
fr = rangeFrames[len(rangeFrames)-2]
|
|
}
|
|
_, err = setDeferBreakpoint(dbp, text, fr, sameGCond, stepInto)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Add breakpoints on all the lines in the current function
|
|
pcs, err := topframe.Current.Fn.AllPCs(topframe.Current.File, topframe.Current.Line)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if backward {
|
|
// Ensure that pcs contains firstPCAfterPrologue when reverse stepping.
|
|
found := false
|
|
for _, pc := range pcs {
|
|
if pc == firstPCAfterPrologue {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
pcs = append(pcs, firstPCAfterPrologue)
|
|
}
|
|
}
|
|
|
|
if !stepInto {
|
|
// Removing any PC range belonging to an inlined call
|
|
frame := topframe
|
|
if inlinedStepOut {
|
|
frame = retframe
|
|
}
|
|
pcs, err = removeInlinedCalls(pcs, &frame, bi)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if !csource {
|
|
var covered bool
|
|
for i := range pcs {
|
|
if topframe.Current.Fn.Entry <= pcs[i] && pcs[i] < topframe.Current.Fn.End {
|
|
covered = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !covered {
|
|
fn := bi.PCToFunc(topframe.Ret)
|
|
if selg != nil && fn != nil && fn.Name == "runtime.goexit" {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, pc := range pcs {
|
|
if !stepInto && topframe.Call.Fn.extra(bi).rangeParent != nil {
|
|
if pc < firstPCAfterPrologue {
|
|
continue
|
|
}
|
|
}
|
|
if _, err := allowDuplicateBreakpoint(dbp.SetBreakpoint(0, pc, NextBreakpoint, sameFrameCond)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if stepInto && backward {
|
|
err := setStepIntoBreakpointsReverse(dbp, text, topframe, sameGCond)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Stepping into range-over-func-bodies
|
|
if !stepInto && !inlinedStepOut {
|
|
rangeParent := topframe.Call.Fn.extra(bi).rangeParent
|
|
if rangeParent == nil {
|
|
rangeParent = topframe.Call.Fn
|
|
}
|
|
rpoff := topframe.FrameOffset()
|
|
if len(rangeFrames) > 0 {
|
|
rpoff = rangeFrames[len(rangeFrames)-2].FrameOffset()
|
|
}
|
|
rpc := astutil.And(sameGCond, astutil.Eql(astutil.PkgVar("runtime", "rangeParentOffset"), astutil.Int(rpoff)))
|
|
for _, fn := range rangeParent.extra(bi).rangeBodies {
|
|
if fn.Entry != 0 {
|
|
pc, err := FirstPCAfterPrologue(dbp, fn, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
kind := NextBreakpoint
|
|
if f, ln := bi.pcToLine(fn, pc); f == topframe.Current.File && ln == topframe.Current.Line {
|
|
kind = StepIntoRangeOverFuncBodyBreakpoint
|
|
}
|
|
if _, err := allowDuplicateBreakpoint(dbp.SetBreakpoint(0, pc, kind, rpc)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set step-out breakpoints for range-over-func body closures
|
|
if !stepInto && selg != nil && topframe.Current.Fn.extra(bi).rangeParent != nil && len(rangeFrames) > 0 {
|
|
// Set step-out breakpoint for every range-over-func body currently on the stack so that we stop on them.
|
|
for i := 2; i < len(rangeFrames); i += 2 {
|
|
fr := &rangeFrames[i]
|
|
retframecond := astutil.And(sameGCond, frameoffCondition(fr))
|
|
if !fr.hasInlines {
|
|
dbp.SetBreakpoint(0, fr.Current.PC, NextBreakpoint, retframecond)
|
|
} else {
|
|
// fr.Current.PC does not belong to fr.Call.Fn, because there are inlined calls, therefore set a breakpoint on every statement of fr.Call.Fn
|
|
pcs, err := fr.Current.Fn.AllPCs("", 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pcs, err = removeInlinedCalls(pcs, fr, bi)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, pc := range pcs {
|
|
dbp.SetBreakpoint(0, pc, NextBreakpoint, retframecond)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set a step-out breakpoint for the first range-over-func body on the
|
|
// stack, this breakpoint will never cause a stop because the associated
|
|
// callback always returns false.
|
|
// Its purpose is to inactivate all the breakpoints for the current
|
|
// range-over-func body function so that if the iterator re-calls it we
|
|
// don't end up inside the prologue.
|
|
if !rangeFrames[0].Inlined {
|
|
bp, err := dbp.SetBreakpoint(0, rangeFrames[1].Call.PC, NextBreakpoint, astutil.And(sameGCond, frameoffCondition(&rangeFrames[1])))
|
|
if err == nil {
|
|
bplet := bp.Breaklets[len(bp.Breaklets)-1]
|
|
bplet.callback = func(th Thread, p *Target) (bool, error) {
|
|
rangeFrameInactivateNextBreakpoints(p, rangeFrames[0].Call.Fn)
|
|
return false, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
topframe, retframe = rangeFrames[len(rangeFrames)-2], rangeFrames[len(rangeFrames)-1]
|
|
}
|
|
|
|
// Step-out breakpoint
|
|
if !topframe.Inlined {
|
|
topframe, retframe := skipAutogeneratedWrappersOut(dbp, selg, curthread, &topframe, &retframe)
|
|
retFrameCond := astutil.And(sameGCond, frameoffCondition(retframe))
|
|
|
|
// Add a breakpoint on the return address for the current frame.
|
|
// For inlined functions there is no need to do this, the set of PCs
|
|
// returned by the AllPCsBetween call above already cover all instructions
|
|
// of the containing function.
|
|
bp, _ := dbp.SetBreakpoint(0, retframe.Current.PC, NextBreakpoint, retFrameCond)
|
|
// Return address could be wrong, if we are unable to set a breakpoint
|
|
// there it's ok.
|
|
if bp != nil {
|
|
configureReturnBreakpoint(bi, bp, topframe, retFrameCond)
|
|
}
|
|
}
|
|
|
|
if bp := curthread.Breakpoint(); bp.Breakpoint == nil {
|
|
curthread.SetCurrentBreakpoint(false)
|
|
}
|
|
success = true
|
|
return nil
|
|
}
|
|
|
|
func setStepIntoBreakpoints(dbp *Target, curfn *Function, text []AsmInstruction, topframe Stackframe, sameGCond ast.Expr) error {
|
|
gostmt := false
|
|
for _, instr := range text {
|
|
if instr.Loc.File != topframe.Current.File || instr.Loc.Line != topframe.Current.Line || !instr.IsCall() {
|
|
continue
|
|
}
|
|
|
|
if instr.DestLoc != nil {
|
|
if err := setStepIntoBreakpoint(dbp, curfn, []AsmInstruction{instr}, sameGCond); err != nil {
|
|
return err
|
|
}
|
|
if curfn != nil && curfn.Name != "runtime." && instr.DestLoc.Fn != nil && instr.DestLoc.Fn.Name == "runtime.newproc" {
|
|
// The current statement is a go statement, i.e. "go somecall()"
|
|
// We are excluding this check inside the runtime package because
|
|
// functions in the runtime package can call runtime.newproc directly.
|
|
gostmt = true
|
|
}
|
|
} else {
|
|
// Non-absolute call instruction, set a StepBreakpoint here
|
|
bp, err := allowDuplicateBreakpoint(dbp.SetBreakpoint(0, instr.Loc.PC, StepBreakpoint, sameGCond))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
breaklet := bp.Breaklets[len(bp.Breaklets)-1]
|
|
breaklet.callback = stepIntoCallback
|
|
}
|
|
}
|
|
if gostmt {
|
|
setStepIntoNewProcBreakpoint(dbp, sameGCond)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// stepIntoCallback is a callback called when a StepBreakpoint is hit, it
|
|
// disassembles the current instruction to figure out its destination and
|
|
// sets a breakpoint on it.
|
|
func stepIntoCallback(curthread Thread, p *Target) (bool, error) {
|
|
if p.recman.GetDirection() != Forward {
|
|
// This should never happen, step into breakpoints with callbacks are only
|
|
// set when moving forward and direction changes are forbidden while
|
|
// breakpoints are set.
|
|
return true, nil
|
|
}
|
|
|
|
text, err := disassembleCurrentInstruction(p, curthread, 0)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
ok, err := stepIntoCoroutineMaybe(curthread, p, text)
|
|
if ok || err != nil {
|
|
return false, err
|
|
}
|
|
|
|
var fn *Function
|
|
if loc, _ := curthread.Location(); loc != nil {
|
|
fn = loc.Fn
|
|
}
|
|
g, _ := GetG(curthread)
|
|
// here we either set a breakpoint into the destination of the CALL
|
|
// instruction or we determined that the called function is hidden,
|
|
// either way we need to resume execution
|
|
if err = setStepIntoBreakpoint(p, fn, text, sameGoroutineCondition(p.BinInfo(), g, curthread.ThreadID())); err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
func setStepIntoBreakpointsReverse(dbp *Target, text []AsmInstruction, topframe Stackframe, sameGCond ast.Expr) error {
|
|
bpmap := dbp.Breakpoints()
|
|
// Set a breakpoint after every CALL instruction
|
|
for i, instr := range text {
|
|
if instr.Loc.File != topframe.Current.File || !instr.IsCall() || instr.DestLoc == nil || instr.DestLoc.Fn == nil {
|
|
continue
|
|
}
|
|
|
|
if instr.DestLoc.Fn.privateRuntime() {
|
|
continue
|
|
}
|
|
|
|
if nextIdx := i + 1; nextIdx < len(text) {
|
|
_, ok := bpmap.M[text[nextIdx].Loc.PC]
|
|
if !ok {
|
|
if _, err := allowDuplicateBreakpoint(dbp.SetBreakpoint(0, text[nextIdx].Loc.PC, StepBreakpoint, sameGCond)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func FindDeferReturnCalls(text []AsmInstruction) []uint64 {
|
|
const deferreturn = "runtime.deferreturn"
|
|
deferreturns := []uint64{}
|
|
|
|
// Find all runtime.deferreturn locations in the function
|
|
// See documentation of Breakpoint.DeferCond for why this is necessary
|
|
for _, instr := range text {
|
|
if instr.IsCall() && instr.DestLoc != nil && instr.DestLoc.Fn != nil && instr.DestLoc.Fn.Name == deferreturn {
|
|
deferreturns = append(deferreturns, instr.Loc.PC)
|
|
}
|
|
}
|
|
return deferreturns
|
|
}
|
|
|
|
// Removes instructions belonging to inlined calls of topframe from pcs.
|
|
// Inlined calls that belong to range-over-func bodies are not removed.
|
|
func removeInlinedCalls(pcs []uint64, topframe *Stackframe, bi *BinaryInfo) ([]uint64, error) {
|
|
// TODO(derekparker) it should be possible to still use some internal
|
|
// runtime information to do this.
|
|
if topframe.Call.Fn == nil || topframe.Call.Fn.cu.image.Stripped() {
|
|
return pcs, nil
|
|
}
|
|
|
|
topframeRangeParentName := topframe.Call.Fn.Name
|
|
if topframe.Call.Fn.extra(bi).rangeParent != nil {
|
|
topframeRangeParentName = topframe.Call.Fn.extra(bi).rangeParent.Name
|
|
}
|
|
|
|
dwarfTree, err := topframe.Current.Fn.cu.image.getDwarfTree(topframe.Current.Fn.offset)
|
|
if err != nil {
|
|
return pcs, err
|
|
}
|
|
color := make([]removePC, len(pcs))
|
|
removeInlinedCallsColor(topframe, topframeRangeParentName, pcs, color, dwarfTree)
|
|
out := make([]uint64, 0, len(pcs))
|
|
for i := range pcs {
|
|
if color[i] != removePCRemove {
|
|
out = append(out, pcs[i])
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
type removePC uint8
|
|
|
|
const (
|
|
removePCUnknown removePC = iota
|
|
removePCRemove
|
|
removePCKeep
|
|
)
|
|
|
|
// removeInlinedCallsColor sets color[i] to removePCRemove or removePCKeep
|
|
// depending on whether pcs[i] should be removed by removeInlinedCalls.
|
|
// This determination is made by checking, for each PC, what is the topmost
|
|
// inlined call.
|
|
func removeInlinedCallsColor(topframe *Stackframe, topframeRangeParentName string, pcs []uint64, color []removePC, e *godwarf.Tree) {
|
|
switch e.Tag {
|
|
case dwarf.TagSubprogram, dwarf.TagInlinedSubroutine, dwarf.TagLexDwarfBlock:
|
|
// ok
|
|
default:
|
|
return
|
|
}
|
|
|
|
for _, child := range e.Children {
|
|
removeInlinedCallsColor(topframe, topframeRangeParentName, pcs, color, child)
|
|
}
|
|
|
|
switch e.Tag {
|
|
case dwarf.TagInlinedSubroutine:
|
|
c := removePCRemove
|
|
if e.Offset == topframe.Call.Fn.offset {
|
|
c = removePCKeep
|
|
} else {
|
|
fnname, _ := e.Val(dwarf.AttrName).(string)
|
|
ridx := rangeParentName(fnname)
|
|
var rpn string
|
|
if ridx == -1 {
|
|
rpn = fnname
|
|
} else {
|
|
rpn = fnname[:ridx]
|
|
}
|
|
if rpn == topframeRangeParentName {
|
|
c = removePCKeep
|
|
}
|
|
}
|
|
for _, rng := range e.Ranges {
|
|
colorPCsBetween(pcs, color, c, rng[0], rng[1])
|
|
}
|
|
}
|
|
}
|
|
|
|
// colorPCsBetween sets color[i] to c if start <= pcs[i] < end
|
|
func colorPCsBetween(pcs []uint64, color []removePC, c removePC, start, end uint64) {
|
|
for i, pc := range pcs {
|
|
if color[i] == removePCUnknown && pc >= start && pc < end {
|
|
color[i] = c
|
|
}
|
|
}
|
|
}
|
|
|
|
func setStepIntoBreakpoint(dbp *Target, curfn *Function, text []AsmInstruction, cond ast.Expr) error {
|
|
if len(text) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// If the current function is already a runtime function then
|
|
// setStepIntoBreakpoint is allowed to step into unexported runtime
|
|
// functions.
|
|
stepIntoUnexportedRuntime := curfn != nil && strings.HasPrefix(curfn.Name, "runtime.")
|
|
|
|
instr := text[0]
|
|
|
|
if instr.DestLoc == nil {
|
|
// Call destination couldn't be resolved because this was not the
|
|
// current instruction, therefore the step-into breakpoint can not be set.
|
|
return nil
|
|
}
|
|
|
|
pc := instr.DestLoc.PC
|
|
fn := instr.DestLoc.Fn
|
|
if dbp.BinInfo().Arch.Name == "ppc64le" && instr.Inst.OpcodeEquals(uint64(ppc64asm.BCLRL)) {
|
|
regs, err := dbp.CurrentThread().Registers()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
lr := regs.LR()
|
|
fn = dbp.BinInfo().PCToFunc(lr)
|
|
}
|
|
|
|
// Skip unexported runtime functions
|
|
if !stepIntoUnexportedRuntime && fn != nil && fn.privateRuntime() {
|
|
return nil
|
|
}
|
|
|
|
//TODO(aarzilli): if we want to let users hide functions
|
|
// or entire packages from being stepped into with 'step'
|
|
// those extra checks should be done here.
|
|
|
|
// Skip InhibitStepInto functions for different arch.
|
|
if dbp.BinInfo().Arch.inhibitStepInto(dbp.BinInfo(), pc) {
|
|
return nil
|
|
}
|
|
|
|
fn, pc = skipAutogeneratedWrappersIn(dbp, fn, pc, false)
|
|
|
|
// We want to skip the function prologue but we should only do it if the
|
|
// destination address of the CALL instruction is the entry point of the
|
|
// function.
|
|
// Calls to runtime.duffzero and duffcopy inserted by the compiler can
|
|
// sometimes point inside the body of those functions, well after the
|
|
// prologue.
|
|
if fn != nil && fn.Entry == pc {
|
|
pc, _ = FirstPCAfterPrologue(dbp, fn, false)
|
|
}
|
|
|
|
// Set a breakpoint after the function's prologue
|
|
if _, err := allowDuplicateBreakpoint(dbp.SetBreakpoint(0, pc, NextBreakpoint, cond)); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// setStepIntoNewProcBreakpoint sets a temporary breakpoint on
|
|
// runtime.newproc that, when hit, clears all temporary breakpoints and sets
|
|
// a new temporary breakpoint on the starting function for the new
|
|
// goroutine.
|
|
func setStepIntoNewProcBreakpoint(p *Target, sameGCond ast.Expr) {
|
|
const (
|
|
runtimeNewprocFunc1 = "runtime.newproc.func1"
|
|
runtimeRunqput = "runtime.runqput"
|
|
)
|
|
rnf := p.BinInfo().LookupFunc()[runtimeNewprocFunc1]
|
|
if len(rnf) != 1 {
|
|
logflags.DebuggerLogger().Error("could not find " + runtimeNewprocFunc1)
|
|
return
|
|
}
|
|
text, err := Disassemble(p.Memory(), nil, p.Breakpoints(), p.BinInfo(), rnf[0].Entry, rnf[0].End)
|
|
if err != nil {
|
|
logflags.DebuggerLogger().Errorf("could not disassemble "+runtimeNewprocFunc1+": %v", err)
|
|
return
|
|
}
|
|
|
|
callfile, callline := "", 0
|
|
for _, instr := range text {
|
|
if instr.Kind == CallInstruction && instr.DestLoc != nil && instr.DestLoc.Fn != nil && instr.DestLoc.Fn.Name == runtimeRunqput {
|
|
callfile = instr.Loc.File
|
|
callline = instr.Loc.Line
|
|
break
|
|
}
|
|
}
|
|
if callfile == "" {
|
|
logflags.DebuggerLogger().Error("could not find " + runtimeRunqput + " call in " + runtimeNewprocFunc1)
|
|
return
|
|
}
|
|
var pc uint64
|
|
for _, pcstmt := range rnf[0].cu.lineInfo.LineToPCs(callfile, callline) {
|
|
if pcstmt.Stmt {
|
|
pc = pcstmt.PC
|
|
break
|
|
}
|
|
}
|
|
if pc == 0 {
|
|
logflags.DebuggerLogger().Errorf("could not set newproc breakpoint: location not found for " + runtimeRunqput + " call")
|
|
return
|
|
}
|
|
|
|
bp, err := p.SetBreakpoint(0, pc, StepIntoNewProcBreakpoint, sameGCond)
|
|
if err != nil {
|
|
logflags.DebuggerLogger().Errorf("could not set StepIntoNewProcBreakpoint: %v", err)
|
|
return
|
|
}
|
|
blet := bp.Breaklets[len(bp.Breaklets)-1]
|
|
blet.callback = func(th Thread, p *Target) (bool, error) {
|
|
// Clear temp breakpoints that exist and set a new one for goroutine
|
|
// newg.goid on the go statement's target
|
|
scope, err := ThreadScope(p, th)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
v, err := scope.EvalExpression("newg.goid", loadSingleValue)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if v.Unreadable != nil {
|
|
return false, v.Unreadable
|
|
}
|
|
newGGoID, _ := constant.Int64Val(v.Value)
|
|
|
|
v, err = scope.EvalExpression("newg.startpc", loadSingleValue)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if v.Unreadable != nil {
|
|
return false, v.Unreadable
|
|
}
|
|
startpc, _ := constant.Int64Val(v.Value)
|
|
|
|
// Temp breakpoints must be cleared because the current goroutine could
|
|
// hit one of them before the new goroutine manages to start.
|
|
for _, bp := range p.Breakpoints().M {
|
|
for _, bplet := range bp.Breaklets {
|
|
if bplet.Kind&steppingMask != 0 {
|
|
bplet.Kind = NextInactivatedBreakpoint
|
|
}
|
|
}
|
|
}
|
|
|
|
// We don't want to use startpc directly because it will be an
|
|
// autogenerated wrapper on some versions of Go. Additionally, once we
|
|
// have the correct function we must also skip to prologue.
|
|
startfn := p.BinInfo().PCToFunc(uint64(startpc))
|
|
if startfn2, _ := skipAutogeneratedWrappersIn(p, startfn, uint64(startpc), true); startfn2 != nil {
|
|
startfn = startfn2
|
|
}
|
|
if startpc2, err := FirstPCAfterPrologue(p, startfn, false); err == nil {
|
|
startpc = int64(startpc2)
|
|
}
|
|
|
|
// The new breakpoint must have 'NextBreakpoint' kind because we want to
|
|
// stop on it.
|
|
_, err = p.SetBreakpoint(0, uint64(startpc), NextBreakpoint, goroutineCondition(newGGoID))
|
|
return false, err // we don't want to stop at this breakpoint if there is no error
|
|
}
|
|
}
|
|
|
|
func goroutineCondition(goid int64) ast.Expr {
|
|
return astutil.Eql(astutil.Sel(astutil.PkgVar("runtime", "curg"), "goid"), astutil.Int(goid))
|
|
}
|
|
|
|
func allowDuplicateBreakpoint(bp *Breakpoint, err error) (*Breakpoint, error) {
|
|
if err != nil {
|
|
//lint:ignore S1020 this is clearer
|
|
if _, isexists := err.(BreakpointExistsError); isexists {
|
|
return bp, nil
|
|
}
|
|
}
|
|
return bp, err
|
|
}
|
|
|
|
func isAutogenerated(loc Location) bool {
|
|
return (loc.File == "<autogenerated>" && loc.Line == 1) || (loc.Fn != nil && loc.Fn.trampoline)
|
|
}
|
|
|
|
func isAutogeneratedOrDeferReturn(loc Location) bool {
|
|
return isAutogenerated(loc) || (loc.Fn != nil && loc.Fn.Name == "runtime.deferreturn")
|
|
}
|
|
|
|
// skipAutogeneratedWrappersIn skips autogenerated wrappers when setting a
|
|
// step-into breakpoint.
|
|
// If alwaysSkipFirst is set the first function is always skipped if it is
|
|
// autogenerated, even if it isn't a wrapper for the function it is calling.
|
|
// See genwrapper in: $GOROOT/src/cmd/compile/internal/gc/subr.go
|
|
func skipAutogeneratedWrappersIn(p Process, startfn *Function, startpc uint64, alwaysSkipFirst bool) (*Function, uint64) {
|
|
if startfn == nil {
|
|
return nil, startpc
|
|
}
|
|
fn := startfn
|
|
for count := 0; count < maxSkipAutogeneratedWrappers; count++ {
|
|
if !fn.cu.isgo {
|
|
// can't exit Go
|
|
return startfn, startpc
|
|
}
|
|
text, err := Disassemble(p.Memory(), nil, p.Breakpoints(), p.BinInfo(), fn.Entry, fn.End)
|
|
if err != nil {
|
|
break
|
|
}
|
|
if len(text) == 0 {
|
|
break
|
|
}
|
|
if !isAutogenerated(text[0].Loc) {
|
|
return fn, fn.Entry
|
|
}
|
|
tgtfns := []*Function{}
|
|
// collect all functions called by the current destination function
|
|
for _, instr := range text {
|
|
switch {
|
|
case instr.IsCall():
|
|
if instr.DestLoc == nil {
|
|
return startfn, startpc
|
|
}
|
|
if p.BinInfo().Arch.inhibitStepInto(p.BinInfo(), instr.DestLoc.PC) {
|
|
// ignored
|
|
continue
|
|
}
|
|
if instr.DestLoc.Fn == nil {
|
|
return startfn, startpc
|
|
}
|
|
// calls to non private runtime functions
|
|
if !instr.DestLoc.Fn.privateRuntime() {
|
|
tgtfns = append(tgtfns, instr.DestLoc.Fn)
|
|
}
|
|
case instr.IsJmp():
|
|
// unconditional jumps to a different function that isn't a private runtime function
|
|
if instr.DestLoc != nil && instr.DestLoc.Fn != fn && !instr.DestLoc.Fn.privateRuntime() {
|
|
tgtfns = append(tgtfns, instr.DestLoc.Fn)
|
|
}
|
|
}
|
|
}
|
|
if len(tgtfns) != 1 {
|
|
// too many or not enough function calls
|
|
break
|
|
}
|
|
|
|
tgtfn := tgtfns[0]
|
|
if alwaysSkipFirst {
|
|
alwaysSkipFirst = false
|
|
startfn, startpc = tgtfn, tgtfn.Entry
|
|
} else if strings.TrimSuffix(tgtfn.BaseName(), "-fm") != strings.TrimSuffix(fn.BaseName(), "-fm") {
|
|
return startfn, startpc
|
|
}
|
|
fn = tgtfn
|
|
}
|
|
return startfn, startpc
|
|
}
|
|
|
|
// skipAutogeneratedWrappersOut skip autogenerated wrappers when setting a
|
|
// step out breakpoint.
|
|
// See genwrapper in: $GOROOT/src/cmd/compile/internal/gc/subr.go
|
|
// It also skips runtime.deferreturn frames (which are only ever on the stack on Go 1.18 or later)
|
|
func skipAutogeneratedWrappersOut(tgt *Target, g *G, thread Thread, startTopframe, startRetframe *Stackframe) (topframe, retframe *Stackframe) {
|
|
topframe, retframe = startTopframe, startRetframe
|
|
if startTopframe.Ret == 0 {
|
|
return
|
|
}
|
|
if !isAutogeneratedOrDeferReturn(startRetframe.Current) {
|
|
return
|
|
}
|
|
retfn := thread.BinInfo().PCToFunc(startTopframe.Ret)
|
|
if retfn == nil {
|
|
return
|
|
}
|
|
if !retfn.cu.isgo {
|
|
return
|
|
}
|
|
var err error
|
|
var frames []Stackframe
|
|
if g == nil {
|
|
frames, err = ThreadStacktrace(tgt, thread, maxSkipAutogeneratedWrappers)
|
|
} else {
|
|
frames, err = GoroutineStacktrace(tgt, g, maxSkipAutogeneratedWrappers, 0)
|
|
}
|
|
if err != nil {
|
|
return
|
|
}
|
|
bi := thread.BinInfo()
|
|
for i := 1; i < len(frames); i++ {
|
|
frame := frames[i]
|
|
if frame.Current.Fn == nil {
|
|
return
|
|
}
|
|
file, line := bi.EntryLineForFunc(frame.Current.Fn)
|
|
if !isAutogeneratedOrDeferReturn(Location{File: file, Line: line, Fn: frame.Current.Fn}) {
|
|
return &frames[i-1], &frames[i]
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// setDeferBreakpoint is a helper function used by next and StepOut to set a
|
|
// breakpoint on the first deferred function.
|
|
func setDeferBreakpoint(p *Target, text []AsmInstruction, topframe Stackframe, sameGCond ast.Expr, stepInto bool) (uint64, error) {
|
|
// Set breakpoint on the most recently deferred function (if any)
|
|
var deferpc uint64
|
|
if topframe.TopmostDefer != nil && topframe.TopmostDefer.DwrapPC != 0 {
|
|
_, _, deferfn := topframe.TopmostDefer.DeferredFunc(p)
|
|
if deferfn != nil {
|
|
var err error
|
|
deferpc, err = FirstPCAfterPrologue(p, deferfn, false)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
}
|
|
if deferpc != 0 && deferpc != topframe.Current.PC {
|
|
bp, err := allowDuplicateBreakpoint(p.SetBreakpoint(0, deferpc, NextDeferBreakpoint, sameGCond))
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if bp != nil && stepInto {
|
|
// If DeferReturns is set then the breakpoint will also be triggered when
|
|
// called from runtime.deferreturn. We only do this for the step command,
|
|
// not for next or stepout.
|
|
for _, breaklet := range bp.Breaklets {
|
|
if breaklet.Kind == NextDeferBreakpoint {
|
|
breaklet.DeferReturns = FindDeferReturnCalls(text)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return deferpc, nil
|
|
}
|
|
|
|
// findCallInstrForRet returns the PC address of the CALL instruction
|
|
// immediately preceding the instruction at ret.
|
|
func findCallInstrForRet(p Process, mem MemoryReadWriter, ret uint64, fn *Function) (uint64, error) {
|
|
text, err := disassemble(mem, nil, p.Breakpoints(), p.BinInfo(), fn.Entry, fn.End, false)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
var prevInstr AsmInstruction
|
|
for _, instr := range text {
|
|
if instr.Loc.PC == ret {
|
|
return prevInstr.Loc.PC, nil
|
|
}
|
|
prevInstr = instr
|
|
}
|
|
return 0, fmt.Errorf("could not find CALL instruction for address %#x in %s", ret, fn.Name)
|
|
}
|
|
|
|
// stepOutReverse sets a breakpoint on the CALL instruction that created the current frame, this is either:
|
|
// - the CALL instruction immediately preceding the return address of the
|
|
// current frame
|
|
// - the return address of the current frame if the current frame was
|
|
// created by a runtime.deferreturn run
|
|
// - the return address of the runtime.gopanic frame if the current frame
|
|
// was created by a panic
|
|
//
|
|
// This function is used to implement reversed StepOut
|
|
func stepOutReverse(p *Target, topframe, retframe Stackframe, sameGCond ast.Expr) error {
|
|
curthread := p.CurrentThread()
|
|
selg := p.SelectedGoroutine()
|
|
|
|
if selg != nil && selg.Thread != nil {
|
|
curthread = selg.Thread
|
|
}
|
|
|
|
callerText, err := disassemble(p.Memory(), nil, p.Breakpoints(), p.BinInfo(), retframe.Current.Fn.Entry, retframe.Current.Fn.End, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
deferReturns := FindDeferReturnCalls(callerText)
|
|
|
|
var frames []Stackframe
|
|
if selg == nil {
|
|
frames, err = ThreadStacktrace(p, curthread, 3)
|
|
} else {
|
|
frames, err = GoroutineStacktrace(p, selg, 3, 0)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var callpc uint64
|
|
|
|
if ok, panicFrame := isPanicCall(frames); ok {
|
|
if len(frames) < panicFrame+2 || frames[panicFrame+1].Current.Fn == nil {
|
|
if panicFrame < len(frames) {
|
|
return &ErrNoSourceForPC{frames[panicFrame].Current.PC}
|
|
} else {
|
|
return &ErrNoSourceForPC{frames[0].Current.PC}
|
|
}
|
|
}
|
|
callpc, err = findCallInstrForRet(p, p.Memory(), frames[panicFrame].Ret, frames[panicFrame+1].Current.Fn)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
callpc, err = findCallInstrForRet(p, p.Memory(), topframe.Ret, retframe.Current.Fn)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// check if the call instruction to this frame is a call to runtime.deferreturn
|
|
if len(frames) > 0 {
|
|
frames[0].Ret = callpc
|
|
}
|
|
if ok, pc := isDeferReturnCall(frames, deferReturns); ok && pc != 0 {
|
|
callpc = pc
|
|
}
|
|
}
|
|
|
|
_, err = allowDuplicateBreakpoint(p.SetBreakpoint(0, callpc, NextBreakpoint, sameGCond))
|
|
|
|
return err
|
|
}
|
|
|
|
// onNextGoroutine returns true if this thread is on the goroutine requested by the current 'next' command
|
|
func onNextGoroutine(tgt *Target, thread Thread, breakpoints *BreakpointMap) (bool, error) {
|
|
var breaklet *Breaklet
|
|
breakletSearch:
|
|
for i := range breakpoints.M {
|
|
for _, blet := range breakpoints.M[i].Breaklets {
|
|
if blet.Kind&steppingMask != 0 && blet.Cond != nil {
|
|
breaklet = blet
|
|
break breakletSearch
|
|
}
|
|
}
|
|
}
|
|
if breaklet == nil {
|
|
return false, nil
|
|
}
|
|
// Internal breakpoint conditions can take multiple different forms:
|
|
// Step into breakpoints:
|
|
// runtime.curg.goid == X
|
|
// Next or StepOut breakpoints:
|
|
// runtime.curg.goid == X && runtime.frameoff == Y
|
|
// Breakpoints that can be hit either by stepping on a line in the same
|
|
// function or by returning from the function:
|
|
// runtime.curg.goid == X && (runtime.frameoff == Y || runtime.frameoff == Z)
|
|
// Here we are only interested in testing the runtime.curg.goid clause.
|
|
w := onNextGoroutineWalker{tgt: tgt, thread: thread}
|
|
ast.Walk(&w, breaklet.Cond)
|
|
return w.ret, w.err
|
|
}
|
|
|
|
type onNextGoroutineWalker struct {
|
|
tgt *Target
|
|
thread Thread
|
|
ret bool
|
|
err error
|
|
}
|
|
|
|
func (w *onNextGoroutineWalker) Visit(n ast.Node) ast.Visitor {
|
|
if binx, isbin := n.(*ast.BinaryExpr); isbin && binx.Op == token.EQL {
|
|
x := exprToString(binx.X)
|
|
if x == "runtime.curg.goid" || x == "runtime.threadid" {
|
|
w.ret, w.err = evalBreakpointCondition(w.tgt, w.thread, n.(ast.Expr))
|
|
return nil
|
|
}
|
|
}
|
|
return w
|
|
}
|
|
|
|
func (t *Target) clearHardcodedBreakpoints() {
|
|
threads := t.ThreadList()
|
|
for _, thread := range threads {
|
|
if thread.Breakpoint().Breakpoint != nil && thread.Breakpoint().LogicalID() == hardcodedBreakpointID {
|
|
thread.Breakpoint().Active = false
|
|
thread.Breakpoint().Breakpoint = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// handleHardcodedBreakpoints looks for threads stopped at a hardcoded
|
|
// breakpoint (i.e. a breakpoint instruction, like INT 3, hardcoded in the
|
|
// program's text) and sets a fake breakpoint on them with logical id
|
|
// hardcodedBreakpointID.
|
|
// It checks trapthread and all threads that have SoftExc returning true.
|
|
func (t *Target) handleHardcodedBreakpoints(grp *TargetGroup, trapthread Thread, threads []Thread) error {
|
|
mem := t.Memory()
|
|
arch := t.BinInfo().Arch
|
|
recorded, _ := t.recman.Recorded()
|
|
|
|
isHardcodedBreakpoint := func(thread Thread, pc uint64) uint64 {
|
|
for _, bpinstr := range [][]byte{arch.BreakpointInstruction(), arch.AltBreakpointInstruction()} {
|
|
if bpinstr == nil {
|
|
continue
|
|
}
|
|
buf := make([]byte, len(bpinstr))
|
|
pc2 := pc
|
|
if arch.BreakInstrMovesPC() {
|
|
pc2 -= uint64(len(bpinstr))
|
|
}
|
|
_, _ = mem.ReadMemory(buf, pc2)
|
|
if bytes.Equal(buf, bpinstr) {
|
|
return uint64(len(bpinstr))
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
stepOverBreak := func(thread Thread, pc uint64) {
|
|
if arch.BreakInstrMovesPC() {
|
|
return
|
|
}
|
|
if recorded {
|
|
return
|
|
}
|
|
if bpsize := isHardcodedBreakpoint(thread, pc); bpsize > 0 {
|
|
setPC(thread, pc+bpsize)
|
|
}
|
|
}
|
|
|
|
setHardcodedBreakpoint := func(thread Thread, loc *Location) {
|
|
bpstate := thread.Breakpoint()
|
|
hcbp := &Breakpoint{}
|
|
bpstate.Active = true
|
|
bpstate.Breakpoint = hcbp
|
|
hcbp.FunctionName = loc.Fn.Name
|
|
hcbp.File = loc.File
|
|
hcbp.Line = loc.Line
|
|
hcbp.Addr = loc.PC
|
|
hcbp.Logical = &LogicalBreakpoint{}
|
|
hcbp.Logical.Name = HardcodedBreakpoint
|
|
hcbp.Breaklets = []*Breaklet{{Kind: UserBreakpoint, LogicalID: hardcodedBreakpointID}}
|
|
t.StopReason = StopHardcodedBreakpoint
|
|
}
|
|
|
|
for _, thread := range threads {
|
|
if thread.Breakpoint().Breakpoint != nil {
|
|
continue
|
|
}
|
|
if (thread.ThreadID() != trapthread.ThreadID()) && !thread.SoftExc() {
|
|
continue
|
|
}
|
|
if (thread.ThreadID() == trapthread.ThreadID()) && grp.cctx.GetManualStopRequested() {
|
|
continue
|
|
}
|
|
|
|
loc, err := thread.Location()
|
|
if err != nil || loc.Fn == nil {
|
|
continue
|
|
}
|
|
|
|
g, _ := GetG(thread)
|
|
|
|
switch {
|
|
case loc.Fn.Name == "runtime.breakpoint":
|
|
if recorded, _ := t.recman.Recorded(); recorded {
|
|
setHardcodedBreakpoint(thread, loc)
|
|
continue
|
|
}
|
|
stepOverBreak(thread, loc.PC)
|
|
// In linux-arm64, PtraceSingleStep seems cannot step over BRK instruction
|
|
// (linux-arm64 feature or kernel bug maybe).
|
|
// RISC-V use ebreak as hardcoded breakpoint in Go, however we use c.ebreak
|
|
// in delve to support breakpoints in cgo.
|
|
if !arch.BreakInstrMovesPC() && runtime.GOARCH != "riscv64" {
|
|
setPC(thread, loc.PC+uint64(arch.BreakpointSize()))
|
|
}
|
|
// Single-step current thread until we exit runtime.breakpoint and
|
|
// runtime.Breakpoint.
|
|
// On go < 1.8 it was sufficient to single-step twice on go1.8 a change
|
|
// to the compiler requires 4 steps.
|
|
if err := stepInstructionOut(grp, t, thread, "runtime.breakpoint", "runtime.Breakpoint"); err != nil {
|
|
return err
|
|
}
|
|
setHardcodedBreakpoint(thread, loc)
|
|
case g == nil || t.fncallForG[g.ID] == nil:
|
|
// Check that PC is inside a function (not the entry point) and the
|
|
// preceding instruction is a hardcoded breakpoint.
|
|
// We explicitly check for entry points of functions because the space
|
|
// between functions is usually filled with hardcoded breakpoints.
|
|
if (loc.Fn == nil || loc.Fn.Entry != loc.PC) && isHardcodedBreakpoint(thread, loc.PC) > 0 {
|
|
stepOverBreak(thread, loc.PC)
|
|
setHardcodedBreakpoint(thread, loc)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func rangeFrameInactivateNextBreakpoints(p *Target, fn *Function) {
|
|
pc, err := FirstPCAfterPrologue(p, fn, false)
|
|
if err != nil {
|
|
logflags.DebuggerLogger().Errorf("Error inactivating next breakpoints after exiting a range-over-func body: %v", err)
|
|
return
|
|
}
|
|
|
|
for _, bp := range p.Breakpoints().M {
|
|
if bp.Addr < fn.Entry || bp.Addr >= fn.End || bp.Addr == pc {
|
|
continue
|
|
}
|
|
for _, bplet := range bp.Breaklets {
|
|
if bplet.Kind != NextBreakpoint {
|
|
continue
|
|
}
|
|
// We set to NextInactivatedBreakpoint instead of deleting them because
|
|
// we can't delete breakpoints (or breakpointlets) while breakpoint
|
|
// conditions are being evaluated.
|
|
bplet.Kind = NextInactivatedBreakpoint
|
|
}
|
|
}
|
|
}
|
|
|
|
// stepIntoCoroutineMaybe: if the current instruction is a call to a closure
|
|
// defined into iter.Pull (i.e. next, yield and stop) stepIntoCoroutineMaybe
|
|
// will set up a new breakpoint to step into the associated coroutine code
|
|
// and returns true.
|
|
// In every other case it returns false.
|
|
func stepIntoCoroutineMaybe(curthread Thread, p *Target, text []AsmInstruction) (bool, error) {
|
|
if len(text) == 0 || !text[0].IsCall() || text[0].DestLoc == nil || text[0].DestLoc.Fn == nil || !strings.HasPrefix(text[0].DestLoc.Fn.Name, "iter.Pull") || !strings.Contains(text[0].DestLoc.Fn.Name, ".func") {
|
|
return false, nil
|
|
}
|
|
bi := p.BinInfo()
|
|
|
|
// Read the closure that we are going to call currently
|
|
|
|
regs, err := curthread.Registers()
|
|
if err != nil {
|
|
return false, fmt.Errorf("could not get registers trying to step into coroutine: %v", err)
|
|
}
|
|
dregs := bi.Arch.RegistersToDwarfRegisters(0, regs)
|
|
cst := text[0].DestLoc.Fn.extra(bi).closureStructType
|
|
clos := newVariable("", dregs.Uint64Val(bi.Arch.ContextRegNum), cst, p.BinInfo(), p.Memory())
|
|
|
|
// Get variable 'c' from the current closure, change its type to
|
|
// runtime.coro (it is normally iter.coro, which is an internal
|
|
// placeholder).
|
|
|
|
cvar, err := clos.structMember("c")
|
|
if err != nil {
|
|
logflags.DebuggerLogger().Errorf("iter.Pull problems accessing captured 'c' variable in closure: %v", err)
|
|
return false, nil
|
|
}
|
|
cvar = cvar.maybeDereference()
|
|
if cvar.Unreadable != nil {
|
|
return false, fmt.Errorf("could not read coroutine: %v", cvar.Unreadable)
|
|
}
|
|
typRuntimeCoro, err := bi.findType("runtime.coro")
|
|
if err != nil {
|
|
logflags.DebuggerLogger().Errorf("could not find runtime.coro type: %v", err)
|
|
return false, nil
|
|
}
|
|
cvar = newVariable("", cvar.Addr, typRuntimeCoro, p.BinInfo(), p.Memory())
|
|
|
|
// Set a breakpoint on the first user frame of goroutine c.gp (but
|
|
// something special needs to happen if c.gp is executing
|
|
// runtime.corostart).
|
|
|
|
gp := cvar.loadFieldNamed("gp")
|
|
if gp == nil {
|
|
logflags.DebuggerLogger().Errorf("could not load runtime.coro.gp field (unreadable: %v)", cvar.Unreadable)
|
|
return false, nil
|
|
}
|
|
gaddr, _ := constant.Uint64Val(gp.Value)
|
|
|
|
gvar, err := newGVariable(curthread, gaddr, false)
|
|
if err != nil {
|
|
logflags.DebuggerLogger().Errorf("could not load runtime.coro.gp: %v", err)
|
|
return false, nil
|
|
}
|
|
g, err := gvar.parseG()
|
|
if err != nil {
|
|
logflags.DebuggerLogger().Errorf("could not load runtime.coro.gp: %v", err)
|
|
return false, nil
|
|
}
|
|
|
|
if g.CurrentLoc.Fn == nil {
|
|
logflags.DebuggerLogger().Errorf("could not determine target location of coroutine")
|
|
return false, nil
|
|
}
|
|
|
|
var bploc Location
|
|
|
|
if g.CurrentLoc.Fn.Name == "runtime.corostart" {
|
|
// If the associated goroutine is on runtime.corostart that means the
|
|
// coroutine hasn't started yet, to give a smooth user experience we
|
|
// shouldn't just switch to the goroutine, but instead put a breakpoint on
|
|
// the entry point of the sequence function.
|
|
//
|
|
// This function is stored in the captured variable 'seq' in 'c.f'
|
|
|
|
f := cvar.loadFieldNamed("f")
|
|
if f == nil || f.Unreadable != nil {
|
|
logflags.DebuggerLogger().Errorf("could not determine target location of coroutine (corostart)")
|
|
return false, nil
|
|
}
|
|
seq := f.fieldVariable("seq")
|
|
if seq == nil || seq.Unreadable != nil {
|
|
logflags.DebuggerLogger().Errorf("could not determine target location of coroutine (corostart -- seq)")
|
|
return false, nil
|
|
}
|
|
fn := bi.PCToFunc(seq.Base)
|
|
if fn == nil {
|
|
logflags.DebuggerLogger().Errorf("could not determine target location of coroutine (corostart), no function for PC: %#x", seq.Base)
|
|
return false, nil
|
|
}
|
|
pc, err := FirstPCAfterPrologue(p, fn, false)
|
|
if err != nil {
|
|
logflags.DebuggerLogger().Errorf("FirstPCAfterPrologue error: %v", err)
|
|
pc = fn.Entry
|
|
}
|
|
bploc = Location{PC: pc, Fn: fn}
|
|
} else {
|
|
bploc = g.UserCurrent()
|
|
}
|
|
|
|
// Invalidate all current temp breakpoints
|
|
for _, bp := range p.Breakpoints().M {
|
|
for _, bplet := range bp.Breaklets {
|
|
if bplet.Kind&steppingMask != 0 {
|
|
bplet.Kind = NextInactivatedBreakpoint
|
|
}
|
|
}
|
|
}
|
|
|
|
_, err = p.SetBreakpoint(0, bploc.PC, NextBreakpoint, goroutineCondition(g.ID))
|
|
return true, err
|
|
}
|