
Cache module data so that we don't reload it every time we look up a variable with a generic type.
646 lines
20 KiB
Go
646 lines
20 KiB
Go
package proc
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"go/constant"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/go-delve/delve/pkg/dwarf/op"
|
|
"github.com/go-delve/delve/pkg/goversion"
|
|
"github.com/go-delve/delve/pkg/logflags"
|
|
"github.com/go-delve/delve/pkg/proc/internal/ebpf"
|
|
)
|
|
|
|
var (
|
|
// ErrNotRecorded is returned when an action is requested that is
|
|
// only possible on recorded (traced) programs.
|
|
ErrNotRecorded = errors.New("not a recording")
|
|
|
|
// ErrNoRuntimeAllG is returned when the runtime.allg list could
|
|
// not be found.
|
|
ErrNoRuntimeAllG = errors.New("could not find goroutine array")
|
|
|
|
// ErrProcessDetached indicates that we detached from the target process.
|
|
ErrProcessDetached = errors.New("detached from the process")
|
|
)
|
|
|
|
type LaunchFlags uint8
|
|
|
|
const (
|
|
LaunchForeground LaunchFlags = 1 << iota
|
|
LaunchDisableASLR
|
|
)
|
|
|
|
// Target represents the process being debugged.
|
|
type Target struct {
|
|
Process
|
|
|
|
proc ProcessInternal
|
|
recman RecordingManipulationInternal
|
|
|
|
pid int
|
|
CmdLine string
|
|
|
|
// StopReason describes the reason why the target process is stopped.
|
|
// A process could be stopped for multiple simultaneous reasons, in which
|
|
// case only one will be reported.
|
|
StopReason StopReason
|
|
|
|
// currentThread is the thread that will be used by next/step/stepout and to evaluate variables if no goroutine is selected.
|
|
currentThread Thread
|
|
|
|
// Goroutine that will be used by default to set breakpoint, eval variables, etc...
|
|
// Normally selectedGoroutine is currentThread.GetG, it will not be only if SwitchGoroutine is called with a goroutine that isn't attached to a thread
|
|
selectedGoroutine *G
|
|
|
|
// fncallForG stores a mapping of current active function calls.
|
|
fncallForG map[int64]*callInjection
|
|
|
|
asyncPreemptChanged bool // runtime/debug.asyncpreemptoff was changed
|
|
asyncPreemptOff int64 // cached value of runtime/debug.asyncpreemptoff
|
|
|
|
// gcache is a cache for Goroutines that we
|
|
// have read and parsed from the targets memory.
|
|
// This must be cleared whenever the target is resumed.
|
|
gcache goroutineCache
|
|
iscgo *bool
|
|
|
|
// exitStatus is the exit status of the process we are debugging.
|
|
// Saved here to relay to any future commands.
|
|
exitStatus int
|
|
|
|
// fakeMemoryRegistry contains the list of all compositeMemory objects
|
|
// created since the last restart, it exists so that registerized variables
|
|
// can be given a unique address.
|
|
fakeMemoryRegistry []*compositeMemory
|
|
fakeMemoryRegistryMap map[string]*compositeMemory
|
|
|
|
partOfGroup bool
|
|
}
|
|
|
|
type KeepSteppingBreakpoints uint8
|
|
|
|
const (
|
|
HaltKeepsSteppingBreakpoints KeepSteppingBreakpoints = 1 << iota
|
|
TracepointKeepsSteppingBreakpoints
|
|
)
|
|
|
|
// ErrProcessExited indicates that the process has exited and contains both
|
|
// process id and exit status.
|
|
type ErrProcessExited struct {
|
|
Pid int
|
|
Status int
|
|
}
|
|
|
|
func (pe ErrProcessExited) Error() string {
|
|
return fmt.Sprintf("Process %d has exited with status %d", pe.Pid, pe.Status)
|
|
}
|
|
|
|
// StopReason describes the reason why the target process is stopped.
|
|
// A process could be stopped for multiple simultaneous reasons, in which
|
|
// case only one will be reported.
|
|
type StopReason uint8
|
|
|
|
// String maps StopReason to string representation.
|
|
func (sr StopReason) String() string {
|
|
switch sr {
|
|
case StopUnknown:
|
|
return "unknown"
|
|
case StopLaunched:
|
|
return "launched"
|
|
case StopAttached:
|
|
return "attached"
|
|
case StopExited:
|
|
return "exited"
|
|
case StopBreakpoint:
|
|
return "breakpoint"
|
|
case StopHardcodedBreakpoint:
|
|
return "hardcoded breakpoint"
|
|
case StopManual:
|
|
return "manual"
|
|
case StopNextFinished:
|
|
return "next finished"
|
|
case StopCallReturned:
|
|
return "call returned"
|
|
case StopWatchpoint:
|
|
return "watchpoint"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
const (
|
|
StopUnknown StopReason = iota
|
|
StopLaunched // The process was just launched
|
|
StopAttached // The debugger stopped the process after attaching
|
|
StopExited // The target process terminated
|
|
StopBreakpoint // The target process hit one or more software breakpoints
|
|
StopHardcodedBreakpoint // The target process hit a hardcoded breakpoint (for example runtime.Breakpoint())
|
|
StopManual // A manual stop was requested
|
|
StopNextFinished // The next/step/stepout/stepInstruction command terminated
|
|
StopCallReturned // An injected call completed
|
|
StopWatchpoint // The target process hit one or more watchpoints
|
|
)
|
|
|
|
// DisableAsyncPreemptEnv returns a process environment (like os.Environ)
|
|
// where asyncpreemptoff is set to 1.
|
|
func DisableAsyncPreemptEnv() []string {
|
|
env := os.Environ()
|
|
for i := range env {
|
|
if strings.HasPrefix(env[i], "GODEBUG=") {
|
|
// Go 1.14 asynchronous preemption mechanism is incompatible with
|
|
// debuggers, see: https://github.com/golang/go/issues/36494
|
|
env[i] += ",asyncpreemptoff=1"
|
|
}
|
|
}
|
|
return env
|
|
}
|
|
|
|
// newTarget returns an initialized Target object.
|
|
// The p argument can optionally implement the RecordingManipulation interface.
|
|
func (grp *TargetGroup) newTarget(p ProcessInternal, pid int, currentThread Thread, path, cmdline string) (*Target, error) {
|
|
entryPoint, err := p.EntryPoint()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = p.BinInfo().LoadBinaryInfo(path, entryPoint, grp.cfg.DebugInfoDirs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, image := range p.BinInfo().Images {
|
|
if image.loadErr != nil {
|
|
return nil, image.loadErr
|
|
}
|
|
}
|
|
|
|
t := &Target{
|
|
Process: p,
|
|
proc: p,
|
|
fncallForG: make(map[int64]*callInjection),
|
|
currentThread: currentThread,
|
|
pid: pid,
|
|
CmdLine: cmdline,
|
|
}
|
|
|
|
if recman, ok := p.(RecordingManipulationInternal); ok {
|
|
t.recman = recman
|
|
} else {
|
|
t.recman = &dummyRecordingManipulation{}
|
|
}
|
|
|
|
g, _ := GetG(currentThread)
|
|
t.selectedGoroutine = g
|
|
|
|
t.Breakpoints().Logical = grp.LogicalBreakpoints
|
|
t.createUnrecoveredPanicBreakpoint()
|
|
t.createFatalThrowBreakpoint()
|
|
t.createPluginOpenBreakpoint()
|
|
|
|
t.gcache.init(p.BinInfo())
|
|
t.fakeMemoryRegistryMap = make(map[string]*compositeMemory)
|
|
|
|
if grp.cfg.DisableAsyncPreempt {
|
|
setAsyncPreemptOff(t, 1)
|
|
}
|
|
|
|
return t, nil
|
|
}
|
|
|
|
// Pid returns the pid of the target process.
|
|
func (t *Target) Pid() int {
|
|
return t.pid
|
|
}
|
|
|
|
// IsCgo returns the value of runtime.iscgo
|
|
func (t *Target) IsCgo() bool {
|
|
if t.iscgo != nil {
|
|
return *t.iscgo
|
|
}
|
|
scope := globalScope(t, t.BinInfo(), t.BinInfo().Images[0], t.Memory())
|
|
iscgov, err := scope.findGlobal("runtime", "iscgo")
|
|
if err == nil {
|
|
iscgov.loadValue(loadFullValue)
|
|
if iscgov.Unreadable == nil {
|
|
t.iscgo = new(bool)
|
|
*t.iscgo = constant.BoolVal(iscgov.Value)
|
|
return constant.BoolVal(iscgov.Value)
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Valid returns true if this Process can be used. When it returns false it
|
|
// also returns an error describing why the Process is invalid (either
|
|
// ErrProcessExited or ErrProcessDetached).
|
|
func (t *Target) Valid() (bool, error) {
|
|
ok, err := t.proc.Valid()
|
|
if !ok && err != nil {
|
|
if pe, ok := err.(ErrProcessExited); ok {
|
|
pe.Status = t.exitStatus
|
|
err = pe
|
|
}
|
|
}
|
|
return ok, err
|
|
}
|
|
|
|
// SupportsFunctionCalls returns whether or not the backend supports
|
|
// calling functions during a debug session.
|
|
// Currently only non-recorded processes running on AMD64 support
|
|
// function calls.
|
|
func (t *Target) SupportsFunctionCalls() bool {
|
|
return t.Process.BinInfo().Arch.Name == "amd64" || (t.Process.BinInfo().Arch.Name == "arm64" && t.Process.BinInfo().GOOS != "windows") || t.Process.BinInfo().Arch.Name == "ppc64le"
|
|
}
|
|
|
|
// ClearCaches clears internal caches that should not survive a restart.
|
|
// This should be called anytime the target process executes instructions.
|
|
func (t *Target) ClearCaches() {
|
|
t.clearFakeMemory()
|
|
t.gcache.Clear()
|
|
t.BinInfo().moduleDataCache = nil
|
|
for _, thread := range t.ThreadList() {
|
|
thread.Common().g = nil
|
|
}
|
|
}
|
|
|
|
// Restart will start the process group over from the location specified by the "from" locspec.
|
|
// This is only useful for recorded targets.
|
|
// Restarting of a normal process happens at a higher level (debugger.Restart).
|
|
func (grp *TargetGroup) Restart(from string) error {
|
|
if len(grp.targets) != 1 {
|
|
panic("multiple targets not implemented")
|
|
}
|
|
for _, t := range grp.targets {
|
|
t.ClearCaches()
|
|
}
|
|
t := grp.Selected
|
|
currentThread, err := t.recman.Restart(grp.cctx, from)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
t.currentThread = currentThread
|
|
t.selectedGoroutine, _ = GetG(t.CurrentThread())
|
|
if from != "" {
|
|
t.StopReason = StopManual
|
|
} else {
|
|
t.StopReason = StopLaunched
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SelectedGoroutine returns the currently selected goroutine.
|
|
func (t *Target) SelectedGoroutine() *G {
|
|
return t.selectedGoroutine
|
|
}
|
|
|
|
// SwitchGoroutine will change the selected and active goroutine.
|
|
func (t *Target) SwitchGoroutine(g *G) error {
|
|
if ok, err := t.Valid(); !ok {
|
|
return err
|
|
}
|
|
if g == nil {
|
|
return nil
|
|
}
|
|
if g.Thread != nil {
|
|
return t.SwitchThread(g.Thread.ThreadID())
|
|
}
|
|
t.selectedGoroutine = g
|
|
return nil
|
|
}
|
|
|
|
// SwitchThread will change the selected and active thread.
|
|
func (t *Target) SwitchThread(tid int) error {
|
|
if ok, err := t.Valid(); !ok {
|
|
return err
|
|
}
|
|
if th, ok := t.FindThread(tid); ok {
|
|
t.currentThread = th
|
|
t.selectedGoroutine, _ = GetG(t.CurrentThread())
|
|
return nil
|
|
}
|
|
return fmt.Errorf("thread %d does not exist", tid)
|
|
}
|
|
|
|
// setAsyncPreemptOff enables or disables async goroutine preemption by
|
|
// writing the value 'v' to runtime.debug.asyncpreemptoff.
|
|
// A value of '1' means off, a value of '0' means on.
|
|
func setAsyncPreemptOff(p *Target, v int64) {
|
|
if producer := p.BinInfo().Producer(); producer == "" || !goversion.ProducerAfterOrEqual(producer, 1, 14) {
|
|
return
|
|
}
|
|
logger := p.BinInfo().logger
|
|
scope := globalScope(p, p.BinInfo(), p.BinInfo().Images[0], p.Memory())
|
|
// +rtype -var debug anytype
|
|
debugv, err := scope.findGlobal("runtime", "debug")
|
|
if err != nil {
|
|
logger.Warnf("could not find runtime/debug variable (or unreadable): %v", err)
|
|
return
|
|
}
|
|
if debugv.Unreadable != nil {
|
|
logger.Warnf("runtime/debug variable unreadable: %v", err, debugv.Unreadable)
|
|
return
|
|
}
|
|
asyncpreemptoffv, err := debugv.structMember("asyncpreemptoff") // +rtype int32
|
|
if err != nil {
|
|
logger.Warnf("could not find asyncpreemptoff field: %v", err)
|
|
return
|
|
}
|
|
asyncpreemptoffv.loadValue(loadFullValue)
|
|
if asyncpreemptoffv.Unreadable != nil {
|
|
logger.Warnf("asyncpreemptoff field unreadable: %v", asyncpreemptoffv.Unreadable)
|
|
return
|
|
}
|
|
p.asyncPreemptChanged = true
|
|
p.asyncPreemptOff, _ = constant.Int64Val(asyncpreemptoffv.Value)
|
|
|
|
err = scope.setValue(asyncpreemptoffv, newConstant(constant.MakeInt64(v), scope.Mem), "")
|
|
if err != nil {
|
|
logger.Warnf("could not set asyncpreemptoff %v", err)
|
|
}
|
|
}
|
|
|
|
// createUnrecoveredPanicBreakpoint creates the unrecoverable-panic breakpoint.
|
|
func (t *Target) createUnrecoveredPanicBreakpoint() {
|
|
panicpcs, err := FindFunctionLocation(t.Process, "runtime.startpanic", 0)
|
|
if _, isFnNotFound := err.(*ErrFunctionNotFound); isFnNotFound {
|
|
panicpcs, err = FindFunctionLocation(t.Process, "runtime.fatalpanic", 0)
|
|
}
|
|
if err == nil {
|
|
bp, err := t.SetBreakpoint(unrecoveredPanicID, panicpcs[0], UserBreakpoint, nil)
|
|
if err == nil {
|
|
bp.Logical.Name = UnrecoveredPanic
|
|
bp.Logical.Variables = []string{"runtime.curg._panic.arg"}
|
|
}
|
|
}
|
|
}
|
|
|
|
// createFatalThrowBreakpoint creates the a breakpoint as runtime.fatalthrow.
|
|
func (t *Target) createFatalThrowBreakpoint() {
|
|
setFatalThrow := func(pcs []uint64, err error) {
|
|
if err == nil {
|
|
bp, err := t.SetBreakpoint(fatalThrowID, pcs[0], UserBreakpoint, nil)
|
|
if err == nil {
|
|
bp.Logical.Name = FatalThrow
|
|
}
|
|
}
|
|
}
|
|
setFatalThrow(FindFunctionLocation(t.Process, "runtime.throw", 0))
|
|
setFatalThrow(FindFunctionLocation(t.Process, "runtime.fatal", 0))
|
|
setFatalThrow(FindFunctionLocation(t.Process, "runtime.winthrow", 0))
|
|
setFatalThrow(FindFunctionLocation(t.Process, "runtime.fatalsignal", 0))
|
|
}
|
|
|
|
// createPluginOpenBreakpoint creates a breakpoint at the return instruction
|
|
// of plugin.Open (if it exists) that will try to enable suspended
|
|
// breakpoints.
|
|
func (t *Target) createPluginOpenBreakpoint() {
|
|
retpcs, _ := findRetPC(t, "plugin.Open")
|
|
for _, retpc := range retpcs {
|
|
bp, err := t.SetBreakpoint(0, retpc, PluginOpenBreakpoint, nil)
|
|
if err != nil {
|
|
t.BinInfo().logger.Errorf("could not set plugin.Open breakpoint: %v", err)
|
|
} else {
|
|
bp.Breaklets[len(bp.Breaklets)-1].callback = t.pluginOpenCallback
|
|
}
|
|
}
|
|
}
|
|
|
|
// CurrentThread returns the currently selected thread which will be used
|
|
// for next/step/stepout and for reading variables, unless a goroutine is
|
|
// selected.
|
|
func (t *Target) CurrentThread() Thread {
|
|
return t.currentThread
|
|
}
|
|
|
|
type UProbeTraceResult struct {
|
|
FnAddr int
|
|
GoroutineID int
|
|
IsRet bool
|
|
InputParams []*Variable
|
|
ReturnParams []*Variable
|
|
}
|
|
|
|
func (t *Target) GetBufferedTracepoints() []*UProbeTraceResult {
|
|
var results []*UProbeTraceResult
|
|
tracepoints := t.proc.GetBufferedTracepoints()
|
|
convertInputParamToVariable := func(ip *ebpf.RawUProbeParam) *Variable {
|
|
v := &Variable{}
|
|
v.RealType = ip.RealType
|
|
v.Len = ip.Len
|
|
v.Base = ip.Base
|
|
v.Addr = ip.Addr
|
|
v.Kind = ip.Kind
|
|
|
|
if v.RealType == nil {
|
|
v.Unreadable = errors.New("type not supported by ebpf")
|
|
return v
|
|
}
|
|
|
|
cachedMem := CreateLoadedCachedMemory(ip.Data)
|
|
compMem, _ := CreateCompositeMemory(cachedMem, t.BinInfo().Arch, op.DwarfRegisters{}, ip.Pieces, ip.RealType.Common().ByteSize)
|
|
v.mem = compMem
|
|
|
|
// Load the value here so that we don't have to export
|
|
// loadValue outside of proc.
|
|
v.loadValue(loadFullValue)
|
|
|
|
return v
|
|
}
|
|
for _, tp := range tracepoints {
|
|
r := &UProbeTraceResult{}
|
|
r.FnAddr = tp.FnAddr
|
|
r.GoroutineID = tp.GoroutineID
|
|
r.IsRet = tp.IsRet
|
|
for _, ip := range tp.InputParams {
|
|
v := convertInputParamToVariable(ip)
|
|
r.InputParams = append(r.InputParams, v)
|
|
}
|
|
for _, ip := range tp.ReturnParams {
|
|
v := convertInputParamToVariable(ip)
|
|
r.ReturnParams = append(r.ReturnParams, v)
|
|
}
|
|
results = append(results, r)
|
|
}
|
|
return results
|
|
}
|
|
|
|
// ResumeNotify specifies a channel that will be closed the next time
|
|
// Continue finishes resuming the targets.
|
|
func (grp *TargetGroup) ResumeNotify(ch chan<- struct{}) {
|
|
grp.cctx.ResumeChan = ch
|
|
}
|
|
|
|
// RequestManualStop attempts to stop all the processes' threads.
|
|
func (grp *TargetGroup) RequestManualStop() error {
|
|
grp.cctx.StopMu.Lock()
|
|
defer grp.cctx.StopMu.Unlock()
|
|
grp.cctx.manualStopRequested = true
|
|
return grp.Selected.proc.RequestManualStop(grp.cctx)
|
|
}
|
|
|
|
const (
|
|
FakeAddressBase = 0xbeef000000000000
|
|
fakeAddressUnresolv = 0xbeed000000000000 // this address never resolves to memory
|
|
)
|
|
|
|
// newCompositeMemory creates a new compositeMemory object and registers it.
|
|
// If the same composite memory has been created before it will return a
|
|
// cached object.
|
|
// This caching is primarily done so that registerized variables don't get a
|
|
// different address every time they are evaluated, which would be confusing
|
|
// and leak memory.
|
|
func (t *Target) newCompositeMemory(mem MemoryReadWriter, regs op.DwarfRegisters, pieces []op.Piece, descr *locationExpr, size int64) (int64, *compositeMemory, error) {
|
|
var key string
|
|
if regs.CFA != 0 && len(pieces) > 0 {
|
|
// key is created by concatenating the location expression with the CFA,
|
|
// this combination is guaranteed to be unique between resumes.
|
|
buf := new(strings.Builder)
|
|
fmt.Fprintf(buf, "%#x ", regs.CFA)
|
|
op.PrettyPrint(buf, descr.instr, t.BinInfo().Arch.RegnumToString)
|
|
key = buf.String()
|
|
|
|
if cmem := t.fakeMemoryRegistryMap[key]; cmem != nil {
|
|
return int64(cmem.base), cmem, nil
|
|
}
|
|
}
|
|
|
|
cmem, err := newCompositeMemory(mem, t.BinInfo().Arch, regs, pieces, size)
|
|
if err != nil {
|
|
return 0, cmem, err
|
|
}
|
|
t.registerFakeMemory(cmem)
|
|
if key != "" {
|
|
t.fakeMemoryRegistryMap[key] = cmem
|
|
}
|
|
return int64(cmem.base), cmem, nil
|
|
}
|
|
|
|
func (t *Target) registerFakeMemory(mem *compositeMemory) (addr uint64) {
|
|
t.fakeMemoryRegistry = append(t.fakeMemoryRegistry, mem)
|
|
addr = FakeAddressBase
|
|
if len(t.fakeMemoryRegistry) > 1 {
|
|
prevMem := t.fakeMemoryRegistry[len(t.fakeMemoryRegistry)-2]
|
|
addr = uint64(alignAddr(int64(prevMem.base+uint64(len(prevMem.data))), 0x100)) // the call to alignAddr just makes the address look nicer, it is not necessary
|
|
}
|
|
mem.base = addr
|
|
return addr
|
|
}
|
|
|
|
func (t *Target) findFakeMemory(addr uint64) *compositeMemory {
|
|
i := sort.Search(len(t.fakeMemoryRegistry), func(i int) bool {
|
|
mem := t.fakeMemoryRegistry[i]
|
|
return addr <= mem.base || (mem.base <= addr && addr < (mem.base+uint64(len(mem.data))))
|
|
})
|
|
if i != len(t.fakeMemoryRegistry) {
|
|
mem := t.fakeMemoryRegistry[i]
|
|
if mem.base <= addr && addr < (mem.base+uint64(len(mem.data))) {
|
|
return mem
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (t *Target) clearFakeMemory() {
|
|
for i := range t.fakeMemoryRegistry {
|
|
t.fakeMemoryRegistry[i] = nil
|
|
}
|
|
t.fakeMemoryRegistry = t.fakeMemoryRegistry[:0]
|
|
t.fakeMemoryRegistryMap = make(map[string]*compositeMemory)
|
|
}
|
|
|
|
// dwrapUnwrap checks if fn is a dwrap wrapper function and unwraps it if it is.
|
|
func (t *Target) dwrapUnwrap(fn *Function) *Function {
|
|
if fn == nil {
|
|
return nil
|
|
}
|
|
if !strings.Contains(fn.Name, "·dwrap·") && !fn.trampoline {
|
|
return fn
|
|
}
|
|
if unwrap := t.BinInfo().dwrapUnwrapCache[fn.Entry]; unwrap != nil {
|
|
return unwrap
|
|
}
|
|
text, err := disassemble(t.Memory(), nil, t.Breakpoints(), t.BinInfo(), fn.Entry, fn.End, false)
|
|
if err != nil {
|
|
return fn
|
|
}
|
|
for _, instr := range text {
|
|
if instr.IsCall() && instr.DestLoc != nil && instr.DestLoc.Fn != nil && !instr.DestLoc.Fn.privateRuntime() {
|
|
t.BinInfo().dwrapUnwrapCache[fn.Entry] = instr.DestLoc.Fn
|
|
return instr.DestLoc.Fn
|
|
}
|
|
}
|
|
return fn
|
|
}
|
|
|
|
func (t *Target) pluginOpenCallback(Thread, *Target) (bool, error) {
|
|
logger := logflags.DebuggerLogger()
|
|
for _, lbp := range t.Breakpoints().Logical {
|
|
if isSuspended(t, lbp) {
|
|
err := enableBreakpointOnTarget(t, lbp)
|
|
if err != nil {
|
|
logger.Debugf("could not enable breakpoint %d: %v", lbp.LogicalID, err)
|
|
} else {
|
|
logger.Debugf("suspended breakpoint %d enabled", lbp.LogicalID)
|
|
}
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func isSuspended(t *Target, lbp *LogicalBreakpoint) bool {
|
|
for _, bp := range t.Breakpoints().M {
|
|
if bp.LogicalID() == lbp.LogicalID {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
type dummyRecordingManipulation struct {
|
|
}
|
|
|
|
// Recorded always returns false for the native proc backend.
|
|
func (*dummyRecordingManipulation) Recorded() (bool, string) { return false, "" }
|
|
|
|
// ChangeDirection will always return an error in the native proc backend, only for
|
|
// recorded traces.
|
|
func (*dummyRecordingManipulation) ChangeDirection(dir Direction) error {
|
|
if dir != Forward {
|
|
return ErrNotRecorded
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetDirection will always return Forward.
|
|
func (*dummyRecordingManipulation) GetDirection() Direction { return Forward }
|
|
|
|
// When will always return an empty string and nil, not supported on native proc backend.
|
|
func (*dummyRecordingManipulation) When() (string, error) { return "", nil }
|
|
|
|
// Checkpoint will always return an error on the native proc backend,
|
|
// only supported for recorded traces.
|
|
func (*dummyRecordingManipulation) Checkpoint(string) (int, error) { return -1, ErrNotRecorded }
|
|
|
|
// Checkpoints will always return an error on the native proc backend,
|
|
// only supported for recorded traces.
|
|
func (*dummyRecordingManipulation) Checkpoints() ([]Checkpoint, error) { return nil, ErrNotRecorded }
|
|
|
|
// ClearCheckpoint will always return an error on the native proc backend,
|
|
// only supported in recorded traces.
|
|
func (*dummyRecordingManipulation) ClearCheckpoint(int) error { return ErrNotRecorded }
|
|
|
|
// Restart will always return an error in the native proc backend, only for
|
|
// recorded traces.
|
|
func (*dummyRecordingManipulation) Restart(*ContinueOnceContext, string) (Thread, error) {
|
|
return nil, ErrNotRecorded
|
|
}
|
|
|
|
var ErrWaitForNotImplemented = errors.New("waitfor not implemented")
|
|
|
|
func (waitFor *WaitFor) Valid() bool {
|
|
return waitFor != nil && waitFor.Name != ""
|
|
}
|