1527 lines
45 KiB
Go
1527 lines
45 KiB
Go
package proc
|
|
|
|
import (
|
|
"bytes"
|
|
"debug/dwarf"
|
|
"errors"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/constant"
|
|
"go/token"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"golang.org/x/arch/ppc64/ppc64asm"
|
|
|
|
"github.com/go-delve/delve/pkg/astutil"
|
|
"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 fmt.Errorf("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 {
|
|
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
|
|
}
|
|
|
|
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, 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:
|
|
if 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)
|
|
}
|
|
} else {
|
|
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 fmt.Errorf("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 fmt.Errorf("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 fmt.Errorf("next while nexting")
|
|
}
|
|
|
|
dbp := grp.Selected
|
|
selg := dbp.SelectedGoroutine()
|
|
curthread := dbp.CurrentThread()
|
|
|
|
topframe, retframe, err := topframe(dbp, selg, curthread)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
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()
|
|
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")
|
|
}
|
|
|
|
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(dbp.BinInfo(), selg, curthread.ThreadID())
|
|
|
|
var firstPCAfterPrologue uint64
|
|
|
|
if backward {
|
|
firstPCAfterPrologue, err = FirstPCAfterPrologue(dbp, topframe.Current.Fn, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
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(), dbp.BinInfo(), 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() {
|
|
_, err = setDeferBreakpoint(dbp, text, topframe, 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)
|
|
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 := dbp.BinInfo().PCToFunc(topframe.Ret)
|
|
if selg != nil && fn != nil && fn.Name == "runtime.goexit" {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, pc := range pcs {
|
|
if _, err := allowDuplicateBreakpoint(dbp.SetBreakpoint(0, pc, NextBreakpoint, sameFrameCond)); err != nil {
|
|
dbp.ClearSteppingBreakpoints()
|
|
return err
|
|
}
|
|
}
|
|
|
|
if stepInto && backward {
|
|
err := setStepIntoBreakpointsReverse(dbp, text, topframe, sameGCond)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
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(dbp.BinInfo(), 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
|
|
}
|
|
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.
|
|
// If includeCurrentFn is true it will also remove all instructions
|
|
// belonging to the current function.
|
|
func removeInlinedCalls(pcs []uint64, topframe Stackframe) ([]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
|
|
}
|
|
dwarfTree, err := topframe.Call.Fn.cu.image.getDwarfTree(topframe.Call.Fn.offset)
|
|
if err != nil {
|
|
return pcs, err
|
|
}
|
|
for _, e := range reader.InlineStack(dwarfTree, 0) {
|
|
if e.Offset == topframe.Call.Fn.offset {
|
|
continue
|
|
}
|
|
for _, rng := range e.Ranges {
|
|
pcs = removePCsBetween(pcs, rng[0], rng[1])
|
|
}
|
|
}
|
|
return pcs, nil
|
|
}
|
|
|
|
func removePCsBetween(pcs []uint64, start, end uint64) []uint64 {
|
|
out := pcs[:0]
|
|
for _, pc := range pcs {
|
|
if pc < start || pc >= end {
|
|
out = append(out, pc)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
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.
|
|
err = p.ClearSteppingBreakpoints()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
newGCond := astutil.Eql(astutil.Sel(astutil.PkgVar("runtime", "curg"), "goid"), astutil.Int(newGGoID))
|
|
|
|
// We don't want to use startpc directly because it will be an
|
|
// autogenerated wrapper on some versions of Go. Addditionally, 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, newGCond)
|
|
return false, err // we don't want to stop at this breakpoint if there is no error
|
|
}
|
|
}
|
|
|
|
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).
|
|
if !arch.BreakInstrMovesPC() {
|
|
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
|
|
}
|