diff --git a/pkg/proc/breakpoints.go b/pkg/proc/breakpoints.go index 9e5d27b9..ae4cbce9 100644 --- a/pkg/proc/breakpoints.go +++ b/pkg/proc/breakpoints.go @@ -639,7 +639,7 @@ func (t *Target) SetWatchpoint(logicalID int, scope *EvalScope, expr string, wty func (t *Target) setBreakpointInternal(logicalID int, addr uint64, kind BreakpointKind, wtype WatchType, cond ast.Expr) (*Breakpoint, error) { if valid, err := t.Valid(); !valid { - recorded, _ := t.Recorded() + recorded, _ := t.recman.Recorded() if !recorded { return nil, err } @@ -744,7 +744,7 @@ func (bp *Breakpoint) canOverlap(kind BreakpointKind) bool { // ClearBreakpoint clears the breakpoint at addr. func (t *Target) ClearBreakpoint(addr uint64) error { if valid, err := t.Valid(); !valid { - recorded, _ := t.Recorded() + recorded, _ := t.recman.Recorded() if !recorded { return err } diff --git a/pkg/proc/core/core.go b/pkg/proc/core/core.go index 62a1fdeb..561b0096 100644 --- a/pkg/proc/core/core.go +++ b/pkg/proc/core/core.go @@ -227,7 +227,9 @@ func OpenCore(corePath, exePath string, debugInfoDirs []string) (*proc.Target, e DebugInfoDirs: debugInfoDirs, DisableAsyncPreempt: false, StopReason: proc.StopAttached, - CanDump: false}) + CanDump: false, + ContinueOnce: continueOnce, + }) } // BinInfo will return the binary info. @@ -417,9 +419,7 @@ func (p *process) ClearInternalBreakpoints() error { return nil } -// ContinueOnce will always return an error because you -// cannot control execution of a core file. -func (p *process) ContinueOnce(cctx *proc.ContinueOnceContext) (proc.Thread, proc.StopReason, error) { +func continueOnce(procs []proc.ProcessInternal, cctx *proc.ContinueOnceContext) (proc.Thread, proc.StopReason, error) { return nil, proc.StopUnknown, ErrContinueCore } diff --git a/pkg/proc/core/core_test.go b/pkg/proc/core/core_test.go index 3a2bfd23..efb8e208 100644 --- a/pkg/proc/core/core_test.go +++ b/pkg/proc/core/core_test.go @@ -256,8 +256,9 @@ func TestCore(t *testing.T) { t.Skip("disabled on linux, Github Actions, with PIE buildmode") } p := withCoreFile(t, "panic", "") + grp := proc.NewGroup(p) - recorded, _ := p.Recorded() + recorded, _ := grp.Recorded() if !recorded { t.Fatalf("expecting recorded to be true") } diff --git a/pkg/proc/fncall.go b/pkg/proc/fncall.go index 7fdb65f2..dfe260c2 100644 --- a/pkg/proc/fncall.go +++ b/pkg/proc/fncall.go @@ -146,7 +146,8 @@ func (callCtx *callContext) doReturn(ret *Variable, err error) { // EvalExpressionWithCalls is like EvalExpression but allows function calls in 'expr'. // Because this can only be done in the current goroutine, unlike // EvalExpression, EvalExpressionWithCalls is not a method of EvalScope. -func EvalExpressionWithCalls(t *Target, g *G, expr string, retLoadCfg LoadConfig, checkEscape bool) error { +func EvalExpressionWithCalls(grp *TargetGroup, g *G, expr string, retLoadCfg LoadConfig, checkEscape bool) error { + t := grp.Selected bi := t.BinInfo() if !t.SupportsFunctionCalls() { return errFuncCallUnsupportedBackend @@ -201,7 +202,7 @@ func EvalExpressionWithCalls(t *Target, g *G, expr string, retLoadCfg LoadConfig contReq, ok := <-continueRequest if contReq.cont { - return t.Continue() + return grp.Continue() } return finishEvalExpressionWithCalls(t, g, contReq, ok) diff --git a/pkg/proc/gdbserial/gdbserver.go b/pkg/proc/gdbserial/gdbserver.go index 38be0be8..29f2aeae 100644 --- a/pkg/proc/gdbserial/gdbserver.go +++ b/pkg/proc/gdbserial/gdbserver.go @@ -709,7 +709,9 @@ func (p *gdbProcess) initialize(path string, debugInfoDirs []string, stopReason DebugInfoDirs: debugInfoDirs, DisableAsyncPreempt: runtime.GOOS == "darwin", StopReason: stopReason, - CanDump: runtime.GOOS == "darwin"}) + CanDump: runtime.GOOS == "darwin", + ContinueOnce: continueOnce, + }) if err != nil { p.Detach(true) return nil, err @@ -799,9 +801,11 @@ const ( debugServerTargetExcBreakpoint = 0x96 ) -// ContinueOnce will continue execution of the process until -// a breakpoint is hit or signal is received. -func (p *gdbProcess) ContinueOnce(cctx *proc.ContinueOnceContext) (proc.Thread, proc.StopReason, error) { +func continueOnce(procs []proc.ProcessInternal, cctx *proc.ContinueOnceContext) (proc.Thread, proc.StopReason, error) { + if len(procs) != 1 { + panic("not implemented") + } + p := procs[0].(*gdbProcess) if p.exited { return nil, proc.StopExited, proc.ErrProcessExited{Pid: p.conn.pid} } diff --git a/pkg/proc/gdbserial/rr_test.go b/pkg/proc/gdbserial/rr_test.go index 7b4a53f6..a83e249f 100644 --- a/pkg/proc/gdbserial/rr_test.go +++ b/pkg/proc/gdbserial/rr_test.go @@ -23,7 +23,7 @@ func TestMain(m *testing.M) { os.Exit(protest.RunTestsWithFixtures(m)) } -func withTestRecording(name string, t testing.TB, fn func(t *proc.Target, fixture protest.Fixture)) { +func withTestRecording(name string, t testing.TB, fn func(grp *proc.TargetGroup, fixture protest.Fixture)) { fixture := protest.BuildFixture(name, 0) protest.MustHaveRecordingAllowed(t) if path, _ := exec.LookPath("rr"); path == "" { @@ -36,9 +36,11 @@ func withTestRecording(name string, t testing.TB, fn func(t *proc.Target, fixtur } t.Logf("replaying %q", tracedir) - defer p.Detach(true) + grp := proc.NewGroup(p) - fn(p, fixture) + defer grp.Detach(true) + + fn(grp, fixture) } func assertNoError(err error, t testing.TB, s string) { @@ -69,25 +71,26 @@ func setFunctionBreakpoint(p *proc.Target, t *testing.T, fname string) *proc.Bre func TestRestartAfterExit(t *testing.T) { protest.AllowRecording(t) - withTestRecording("testnextprog", t, func(p *proc.Target, fixture protest.Fixture) { + withTestRecording("testnextprog", t, func(grp *proc.TargetGroup, fixture protest.Fixture) { + p := grp.Selected setFunctionBreakpoint(p, t, "main.main") - assertNoError(p.Continue(), t, "Continue") + assertNoError(grp.Continue(), t, "Continue") loc, err := p.CurrentThread().Location() assertNoError(err, t, "CurrentThread().Location()") - err = p.Continue() + err = grp.Continue() if _, isexited := err.(proc.ErrProcessExited); err == nil || !isexited { t.Fatalf("program did not exit: %v", err) } - assertNoError(p.Restart(""), t, "Restart") + assertNoError(grp.Restart(""), t, "Restart") - assertNoError(p.Continue(), t, "Continue (after restart)") + assertNoError(grp.Continue(), t, "Continue (after restart)") loc2, err := p.CurrentThread().Location() assertNoError(err, t, "CurrentThread().Location() (after restart)") if loc2.Line != loc.Line { t.Fatalf("stopped at %d (expected %d)", loc2.Line, loc.Line) } - err = p.Continue() + err = grp.Continue() if _, isexited := err.(proc.ErrProcessExited); err == nil || !isexited { t.Fatalf("program did not exit (after exit): %v", err) } @@ -96,21 +99,22 @@ func TestRestartAfterExit(t *testing.T) { func TestRestartDuringStop(t *testing.T) { protest.AllowRecording(t) - withTestRecording("testnextprog", t, func(p *proc.Target, fixture protest.Fixture) { + withTestRecording("testnextprog", t, func(grp *proc.TargetGroup, fixture protest.Fixture) { + p := grp.Selected setFunctionBreakpoint(p, t, "main.main") - assertNoError(p.Continue(), t, "Continue") + assertNoError(grp.Continue(), t, "Continue") loc, err := p.CurrentThread().Location() assertNoError(err, t, "CurrentThread().Location()") - assertNoError(p.Restart(""), t, "Restart") + assertNoError(grp.Restart(""), t, "Restart") - assertNoError(p.Continue(), t, "Continue (after restart)") + assertNoError(grp.Continue(), t, "Continue (after restart)") loc2, err := p.CurrentThread().Location() assertNoError(err, t, "CurrentThread().Location() (after restart)") if loc2.Line != loc.Line { t.Fatalf("stopped at %d (expected %d)", loc2.Line, loc.Line) } - err = p.Continue() + err = grp.Continue() if _, isexited := err.(proc.ErrProcessExited); err == nil || !isexited { t.Fatalf("program did not exit (after exit): %v", err) } @@ -137,22 +141,23 @@ func setFileBreakpoint(p *proc.Target, t *testing.T, fixture protest.Fixture, li func TestReverseBreakpointCounts(t *testing.T) { protest.AllowRecording(t) - withTestRecording("bpcountstest", t, func(p *proc.Target, fixture protest.Fixture) { + withTestRecording("bpcountstest", t, func(grp *proc.TargetGroup, fixture protest.Fixture) { + p := grp.Selected endbp := setFileBreakpoint(p, t, fixture, 28) - assertNoError(p.Continue(), t, "Continue()") + assertNoError(grp.Continue(), t, "Continue()") loc, _ := p.CurrentThread().Location() if loc.PC != endbp.Addr { t.Fatalf("did not reach end of main.main function: %s:%d (%#x)", loc.File, loc.Line, loc.PC) } p.ClearBreakpoint(endbp.Addr) - assertNoError(p.ChangeDirection(proc.Backward), t, "Switching to backward direction") + assertNoError(grp.ChangeDirection(proc.Backward), t, "Switching to backward direction") bp := setFileBreakpoint(p, t, fixture, 12) startbp := setFileBreakpoint(p, t, fixture, 20) countLoop: for { - assertNoError(p.Continue(), t, "Continue()") + assertNoError(grp.Continue(), t, "Continue()") loc, _ := p.CurrentThread().Location() switch loc.PC { case startbp.Addr: @@ -181,40 +186,41 @@ func TestReverseBreakpointCounts(t *testing.T) { }) } -func getPosition(p *proc.Target, t *testing.T) (when string, loc *proc.Location) { +func getPosition(grp *proc.TargetGroup, t *testing.T) (when string, loc *proc.Location) { var err error - when, err = p.When() + when, err = grp.When() assertNoError(err, t, "When") - loc, err = p.CurrentThread().Location() + loc, err = grp.Selected.CurrentThread().Location() assertNoError(err, t, "Location") return } func TestCheckpoints(t *testing.T) { protest.AllowRecording(t) - withTestRecording("continuetestprog", t, func(p *proc.Target, fixture protest.Fixture) { + withTestRecording("continuetestprog", t, func(grp *proc.TargetGroup, fixture protest.Fixture) { + p := grp.Selected // Continues until start of main.main, record output of 'when' bp := setFunctionBreakpoint(p, t, "main.main") - assertNoError(p.Continue(), t, "Continue") - when0, loc0 := getPosition(p, t) + assertNoError(grp.Continue(), t, "Continue") + when0, loc0 := getPosition(grp, t) t.Logf("when0: %q (%#x) %x", when0, loc0.PC, p.CurrentThread().ThreadID()) // Create a checkpoint and check that the list of checkpoints reflects this - cpid, err := p.Checkpoint("checkpoint1") + cpid, err := grp.Checkpoint("checkpoint1") if cpid != 1 { t.Errorf("unexpected checkpoint id %d", cpid) } assertNoError(err, t, "Checkpoint") - checkpoints, err := p.Checkpoints() + checkpoints, err := grp.Checkpoints() assertNoError(err, t, "Checkpoints") if len(checkpoints) != 1 { t.Fatalf("wrong number of checkpoints %v (one expected)", checkpoints) } // Move forward with next, check that the output of 'when' changes - assertNoError(p.Next(), t, "First Next") - assertNoError(p.Next(), t, "Second Next") - when1, loc1 := getPosition(p, t) + assertNoError(grp.Next(), t, "First Next") + assertNoError(grp.Next(), t, "Second Next") + when1, loc1 := getPosition(grp, t) t.Logf("when1: %q (%#x) %x", when1, loc1.PC, p.CurrentThread().ThreadID()) if loc0.PC == loc1.PC { t.Fatalf("next did not move process %#x", loc0.PC) @@ -225,10 +231,10 @@ func TestCheckpoints(t *testing.T) { // Move back to checkpoint, check that the output of 'when' is the same as // what it was when we set the breakpoint - p.Restart(fmt.Sprintf("c%d", cpid)) + grp.Restart(fmt.Sprintf("c%d", cpid)) g, _ := proc.FindGoroutine(p, 1) p.SwitchGoroutine(g) - when2, loc2 := getPosition(p, t) + when2, loc2 := getPosition(grp, t) t.Logf("when2: %q (%#x) %x", when2, loc2.PC, p.CurrentThread().ThreadID()) if loc2.PC != loc0.PC { t.Fatalf("PC address mismatch %#x != %#x", loc0.PC, loc2.PC) @@ -238,9 +244,9 @@ func TestCheckpoints(t *testing.T) { } // Move forward with next again, check that the output of 'when' matches - assertNoError(p.Next(), t, "First Next") - assertNoError(p.Next(), t, "Second Next") - when3, loc3 := getPosition(p, t) + assertNoError(grp.Next(), t, "First Next") + assertNoError(grp.Next(), t, "Second Next") + when3, loc3 := getPosition(grp, t) t.Logf("when3: %q (%#x)", when3, loc3.PC) if loc3.PC != loc1.PC { t.Fatalf("PC address mismatch %#x != %#x", loc1.PC, loc3.PC) @@ -253,12 +259,12 @@ func TestCheckpoints(t *testing.T) { // output of 'when' again err = p.ClearBreakpoint(bp.Addr) assertNoError(err, t, "ClearBreakpoint") - p.Restart(fmt.Sprintf("c%d", cpid)) + grp.Restart(fmt.Sprintf("c%d", cpid)) g, _ = proc.FindGoroutine(p, 1) p.SwitchGoroutine(g) - assertNoError(p.Next(), t, "First Next") - assertNoError(p.Next(), t, "Second Next") - when4, loc4 := getPosition(p, t) + assertNoError(grp.Next(), t, "First Next") + assertNoError(grp.Next(), t, "Second Next") + when4, loc4 := getPosition(grp, t) t.Logf("when4: %q (%#x)", when4, loc4.PC) if loc4.PC != loc1.PC { t.Fatalf("PC address mismatch %#x != %#x", loc1.PC, loc4.PC) @@ -268,8 +274,8 @@ func TestCheckpoints(t *testing.T) { } // Delete checkpoint, check that the list of checkpoints is updated - assertNoError(p.ClearCheckpoint(cpid), t, "ClearCheckpoint") - checkpoints, err = p.Checkpoints() + assertNoError(grp.ClearCheckpoint(cpid), t, "ClearCheckpoint") + checkpoints, err = grp.Checkpoints() assertNoError(err, t, "Checkpoints") if len(checkpoints) != 0 { t.Fatalf("wrong number of checkpoints %v (zero expected)", checkpoints) @@ -280,12 +286,13 @@ func TestCheckpoints(t *testing.T) { func TestIssue1376(t *testing.T) { // Backward Continue should terminate when it encounters the start of the process. protest.AllowRecording(t) - withTestRecording("continuetestprog", t, func(p *proc.Target, fixture protest.Fixture) { + withTestRecording("continuetestprog", t, func(grp *proc.TargetGroup, fixture protest.Fixture) { + p := grp.Selected bp := setFunctionBreakpoint(p, t, "main.main") - assertNoError(p.Continue(), t, "Continue (forward)") + assertNoError(grp.Continue(), t, "Continue (forward)") err := p.ClearBreakpoint(bp.Addr) assertNoError(err, t, "ClearBreakpoint") - assertNoError(p.ChangeDirection(proc.Backward), t, "Switching to backward direction") - assertNoError(p.Continue(), t, "Continue (backward)") + assertNoError(grp.ChangeDirection(proc.Backward), t, "Switching to backward direction") + assertNoError(grp.Continue(), t, "Continue (backward)") }) } diff --git a/pkg/proc/interface.go b/pkg/proc/interface.go index ef9e8a60..2f845211 100644 --- a/pkg/proc/interface.go +++ b/pkg/proc/interface.go @@ -38,7 +38,6 @@ type ProcessInternal interface { // ErrProcessExited or ErrProcessDetached). Valid() (bool, error) Detach(bool) error - ContinueOnce(*ContinueOnceContext) (trapthread Thread, stopReason StopReason, err error) // RequestManualStop attempts to stop all the process' threads. RequestManualStop(cctx *ContinueOnceContext) error diff --git a/pkg/proc/native/proc.go b/pkg/proc/native/proc.go index 500fc46a..6f25861d 100644 --- a/pkg/proc/native/proc.go +++ b/pkg/proc/native/proc.go @@ -172,9 +172,11 @@ func (dbp *nativeProcess) EraseBreakpoint(bp *proc.Breakpoint) error { return dbp.memthread.clearSoftwareBreakpoint(bp) } -// ContinueOnce will continue the target until it stops. -// This could be the result of a breakpoint or signal. -func (dbp *nativeProcess) ContinueOnce(cctx *proc.ContinueOnceContext) (proc.Thread, proc.StopReason, error) { +func continueOnce(procs []proc.ProcessInternal, cctx *proc.ContinueOnceContext) (proc.Thread, proc.StopReason, error) { + if len(procs) != 1 { + panic("not implemented") + } + dbp := procs[0].(*nativeProcess) if dbp.exited { return nil, proc.StopExited, proc.ErrProcessExited{Pid: dbp.pid} } @@ -253,8 +255,9 @@ func (dbp *nativeProcess) initialize(path string, debugInfoDirs []string) (*proc // See: https://go-review.googlesource.com/c/go/+/208126 DisableAsyncPreempt: runtime.GOOS == "windows" || runtime.GOOS == "freebsd" || (runtime.GOOS == "linux" && runtime.GOARCH == "arm64"), - StopReason: stopReason, - CanDump: runtime.GOOS == "linux" || runtime.GOOS == "windows", + StopReason: stopReason, + CanDump: runtime.GOOS == "linux" || runtime.GOOS == "windows", + ContinueOnce: continueOnce, }) if err != nil { return nil, err diff --git a/pkg/proc/proc_export_test.go b/pkg/proc/proc_export_test.go new file mode 100644 index 00000000..45b67e5f --- /dev/null +++ b/pkg/proc/proc_export_test.go @@ -0,0 +1,42 @@ +package proc + +// Wrapper functions so that most tests proc_test.go don't need to worry +// about TargetGroup when they just need to resume a single process. + +func newGroupTransient(tgt *Target) *TargetGroup { + grp := NewGroup(tgt) + tgt.partOfGroup = false + return grp +} + +func (tgt *Target) Detach(kill bool) error { + return tgt.detach(kill) +} + +func (tgt *Target) Continue() error { + return newGroupTransient(tgt).Continue() +} + +func (tgt *Target) Next() error { + return newGroupTransient(tgt).Next() +} + +func (tgt *Target) Step() error { + return newGroupTransient(tgt).Step() +} + +func (tgt *Target) StepOut() error { + return newGroupTransient(tgt).StepOut() +} + +func (tgt *Target) ChangeDirection(dir Direction) error { + return tgt.recman.ChangeDirection(dir) +} + +func (tgt *Target) StepInstruction() error { + return newGroupTransient(tgt).StepInstruction() +} + +func (tgt *Target) Recorded() (bool, string) { + return tgt.recman.Recorded() +} diff --git a/pkg/proc/proc_test.go b/pkg/proc/proc_test.go index e0fa81c3..c09bcb16 100644 --- a/pkg/proc/proc_test.go +++ b/pkg/proc/proc_test.go @@ -272,16 +272,17 @@ func findFileLocation(p *proc.Target, t *testing.T, file string, lineno int) uin func TestHalt(t *testing.T) { stopChan := make(chan interface{}, 1) withTestProcess("loopprog", t, func(p *proc.Target, fixture protest.Fixture) { + grp := proc.NewGroup(p) setFunctionBreakpoint(p, t, "main.loop") - assertNoError(p.Continue(), t, "Continue") + assertNoError(grp.Continue(), t, "Continue") resumeChan := make(chan struct{}, 1) go func() { <-resumeChan time.Sleep(100 * time.Millisecond) - stopChan <- p.RequestManualStop() + stopChan <- grp.RequestManualStop() }() - p.ResumeNotify(resumeChan) - assertNoError(p.Continue(), t, "Continue") + grp.ResumeNotify(resumeChan) + assertNoError(grp.Continue(), t, "Continue") retVal := <-stopChan if err, ok := retVal.(error); ok && err != nil { @@ -2064,6 +2065,7 @@ func TestCmdLineArgs(t *testing.T) { func TestIssue462(t *testing.T) { skipOn(t, "broken", "windows") // Stacktrace of Goroutine 0 fails with an error withTestProcess("testnextnethttp", t, func(p *proc.Target, fixture protest.Fixture) { + grp := proc.NewGroup(p) go func() { // Wait for program to start listening. for { @@ -2075,10 +2077,10 @@ func TestIssue462(t *testing.T) { time.Sleep(50 * time.Millisecond) } - p.RequestManualStop() + grp.RequestManualStop() }() - assertNoError(p.Continue(), t, "Continue()") + assertNoError(grp.Continue(), t, "Continue()") _, err := proc.ThreadStacktrace(p.CurrentThread(), 40) assertNoError(err, t, "Stacktrace()") }) @@ -2638,22 +2640,23 @@ func TestNextBreakpoint(t *testing.T) { func TestNextBreakpointKeepsSteppingBreakpoints(t *testing.T) { protest.AllowRecording(t) withTestProcess("testnextprog", t, func(p *proc.Target, fixture protest.Fixture) { - p.KeepSteppingBreakpoints = proc.TracepointKeepsSteppingBreakpoints + grp := proc.NewGroup(p) + grp.KeepSteppingBreakpoints = proc.TracepointKeepsSteppingBreakpoints bp := setFileBreakpoint(p, t, fixture.Source, 34) - assertNoError(p.Continue(), t, "Continue()") + assertNoError(grp.Continue(), t, "Continue()") p.ClearBreakpoint(bp.Addr) // Next should be interrupted by a tracepoint on the same goroutine. bp = setFileBreakpoint(p, t, fixture.Source, 14) bp.Logical.Tracepoint = true - assertNoError(p.Next(), t, "Next()") + assertNoError(grp.Next(), t, "Next()") assertLineNumber(p, t, 14, "wrong line number") if !p.Breakpoints().HasSteppingBreakpoints() { t.Fatal("does not have internal breakpoints after hitting tracepoint on same goroutine") } // Continue to complete next. - assertNoError(p.Continue(), t, "Continue()") + assertNoError(grp.Continue(), t, "Continue()") assertLineNumber(p, t, 35, "wrong line number") if p.Breakpoints().HasSteppingBreakpoints() { t.Fatal("has internal breakpoints after completing next") @@ -3713,17 +3716,18 @@ func TestIssue1101(t *testing.T) { func TestIssue1145(t *testing.T) { withTestProcess("sleep", t, func(p *proc.Target, fixture protest.Fixture) { + grp := proc.NewGroup(p) setFileBreakpoint(p, t, fixture.Source, 18) - assertNoError(p.Continue(), t, "Continue()") + assertNoError(grp.Continue(), t, "Continue()") resumeChan := make(chan struct{}, 1) - p.ResumeNotify(resumeChan) + grp.ResumeNotify(resumeChan) go func() { <-resumeChan time.Sleep(100 * time.Millisecond) - p.RequestManualStop() + grp.RequestManualStop() }() - assertNoError(p.Next(), t, "Next()") + assertNoError(grp.Next(), t, "Next()") if p.Breakpoints().HasSteppingBreakpoints() { t.Fatal("has internal breakpoints after manual stop request") } @@ -3732,18 +3736,19 @@ func TestIssue1145(t *testing.T) { func TestHaltKeepsSteppingBreakpoints(t *testing.T) { withTestProcess("sleep", t, func(p *proc.Target, fixture protest.Fixture) { - p.KeepSteppingBreakpoints = proc.HaltKeepsSteppingBreakpoints + grp := proc.NewGroup(p) + grp.KeepSteppingBreakpoints = proc.HaltKeepsSteppingBreakpoints setFileBreakpoint(p, t, fixture.Source, 18) - assertNoError(p.Continue(), t, "Continue()") + assertNoError(grp.Continue(), t, "Continue()") resumeChan := make(chan struct{}, 1) - p.ResumeNotify(resumeChan) + grp.ResumeNotify(resumeChan) go func() { <-resumeChan time.Sleep(100 * time.Millisecond) - p.RequestManualStop() + grp.RequestManualStop() }() - assertNoError(p.Next(), t, "Next()") + assertNoError(grp.Next(), t, "Next()") if !p.Breakpoints().HasSteppingBreakpoints() { t.Fatal("does not have internal breakpoints after manual stop request") } @@ -4346,11 +4351,12 @@ func TestIssue1374(t *testing.T) { // Continue did not work when stopped at a breakpoint immediately after calling CallFunction. protest.MustSupportFunctionCalls(t, testBackend) withTestProcess("issue1374", t, func(p *proc.Target, fixture protest.Fixture) { + grp := proc.NewGroup(p) setFileBreakpoint(p, t, fixture.Source, 7) - assertNoError(p.Continue(), t, "First Continue") + assertNoError(grp.Continue(), t, "First Continue") assertLineNumber(p, t, 7, "Did not continue to correct location (first continue),") - assertNoError(proc.EvalExpressionWithCalls(p, p.SelectedGoroutine(), "getNum()", normalLoadConfig, true), t, "Call") - err := p.Continue() + assertNoError(proc.EvalExpressionWithCalls(grp, p.SelectedGoroutine(), "getNum()", normalLoadConfig, true), t, "Call") + err := grp.Continue() if _, isexited := err.(proc.ErrProcessExited); !isexited { regs, _ := p.CurrentThread().Registers() f, l, _ := p.BinInfo().PCToLine(regs.PC()) @@ -4577,14 +4583,15 @@ func TestCallConcurrent(t *testing.T) { skipOn(t, "broken", "freebsd") protest.MustSupportFunctionCalls(t, testBackend) withTestProcess("teststepconcurrent", t, func(p *proc.Target, fixture protest.Fixture) { + grp := proc.NewGroup(p) bp := setFileBreakpoint(p, t, fixture.Source, 24) - assertNoError(p.Continue(), t, "Continue()") + assertNoError(grp.Continue(), t, "Continue()") //_, err := p.ClearBreakpoint(bp.Addr) //assertNoError(err, t, "ClearBreakpoint() returned an error") gid1 := p.SelectedGoroutine().ID t.Logf("starting injection in %d / %d", p.SelectedGoroutine().ID, p.CurrentThread().ThreadID()) - assertNoError(proc.EvalExpressionWithCalls(p, p.SelectedGoroutine(), "Foo(10, 1)", normalLoadConfig, false), t, "EvalExpressionWithCalls()") + assertNoError(proc.EvalExpressionWithCalls(grp, p.SelectedGoroutine(), "Foo(10, 1)", normalLoadConfig, false), t, "EvalExpressionWithCalls()") returned := testCallConcurrentCheckReturns(p, t, gid1, -1) @@ -4599,7 +4606,7 @@ func TestCallConcurrent(t *testing.T) { gid2 := p.SelectedGoroutine().ID t.Logf("starting second injection in %d / %d", p.SelectedGoroutine().ID, p.CurrentThread().ThreadID()) - assertNoError(proc.EvalExpressionWithCalls(p, p.SelectedGoroutine(), "Foo(10, 2)", normalLoadConfig, false), t, "EvalExpressioniWithCalls") + assertNoError(proc.EvalExpressionWithCalls(grp, p.SelectedGoroutine(), "Foo(10, 2)", normalLoadConfig, false), t, "EvalExpressioniWithCalls") for { returned += testCallConcurrentCheckReturns(p, t, gid1, gid2) @@ -4607,10 +4614,10 @@ func TestCallConcurrent(t *testing.T) { break } t.Logf("Continuing... %d", returned) - assertNoError(p.Continue(), t, "Continue()") + assertNoError(grp.Continue(), t, "Continue()") } - p.Continue() + grp.Continue() }) } @@ -4964,8 +4971,9 @@ func TestIssue1925(t *testing.T) { // altering the state of the target process. protest.MustSupportFunctionCalls(t, testBackend) withTestProcess("testvariables2", t, func(p *proc.Target, fixture protest.Fixture) { - assertNoError(p.Continue(), t, "Continue()") - assertNoError(proc.EvalExpressionWithCalls(p, p.SelectedGoroutine(), "afunc(2)", normalLoadConfig, true), t, "Call") + grp := proc.NewGroup(p) + assertNoError(grp.Continue(), t, "Continue()") + assertNoError(proc.EvalExpressionWithCalls(grp, p.SelectedGoroutine(), "afunc(2)", normalLoadConfig, true), t, "Call") t.Logf("%v\n", p.SelectedGoroutine().CurrentLoc) if loc := p.SelectedGoroutine().CurrentLoc; loc.File != fixture.Source { t.Errorf("wrong location for selected goroutine after call: %s:%d", loc.File, loc.Line) @@ -5059,31 +5067,32 @@ func TestStepoutOneliner(t *testing.T) { func TestRequestManualStopWhileStopped(t *testing.T) { // Requesting a manual stop while stopped shouldn't cause problems (issue #2138). withTestProcess("issue2138", t, func(p *proc.Target, fixture protest.Fixture) { + grp := proc.NewGroup(p) resumed := make(chan struct{}) setFileBreakpoint(p, t, fixture.Source, 8) - assertNoError(p.Continue(), t, "Continue() 1") - p.ResumeNotify(resumed) + assertNoError(grp.Continue(), t, "Continue() 1") + grp.ResumeNotify(resumed) go func() { <-resumed time.Sleep(1 * time.Second) - p.RequestManualStop() + grp.RequestManualStop() }() t.Logf("at time.Sleep call") - assertNoError(p.Continue(), t, "Continue() 2") + assertNoError(grp.Continue(), t, "Continue() 2") t.Logf("manually stopped") - p.RequestManualStop() - p.RequestManualStop() - p.RequestManualStop() + grp.RequestManualStop() + grp.RequestManualStop() + grp.RequestManualStop() resumed = make(chan struct{}) - p.ResumeNotify(resumed) + grp.ResumeNotify(resumed) go func() { <-resumed time.Sleep(1 * time.Second) - p.RequestManualStop() + grp.RequestManualStop() }() t.Logf("resuming sleep") - assertNoError(p.Continue(), t, "Continue() 3") + assertNoError(grp.Continue(), t, "Continue() 3") t.Logf("done") }) } @@ -5546,9 +5555,10 @@ func TestWatchpointCounts(t *testing.T) { func TestManualStopWhileStopped(t *testing.T) { // Checks that RequestManualStop sent to a stopped thread does not cause the target process to die. withTestProcess("loopprog", t, func(p *proc.Target, fixture protest.Fixture) { + grp := proc.NewGroup(p) asyncCont := func(done chan struct{}) { defer close(done) - err := p.Continue() + err := grp.Continue() t.Logf("%v\n", err) if err != nil { panic(err) @@ -5571,9 +5581,9 @@ func TestManualStopWhileStopped(t *testing.T) { done := make(chan struct{}) go asyncCont(done) time.Sleep(1 * time.Second) - p.RequestManualStop() + grp.RequestManualStop() time.Sleep(1 * time.Second) - p.RequestManualStop() + grp.RequestManualStop() time.Sleep(1 * time.Second) <-done } @@ -5581,11 +5591,11 @@ func TestManualStopWhileStopped(t *testing.T) { t.Logf("Continue %d (fast)", i) rch := make(chan struct{}) done := make(chan struct{}) - p.ResumeNotify(rch) + grp.ResumeNotify(rch) go asyncCont(done) <-rch - p.RequestManualStop() - p.RequestManualStop() + grp.RequestManualStop() + grp.RequestManualStop() <-done } }) @@ -5865,6 +5875,8 @@ func TestCallInjectionFlagCorruption(t *testing.T) { withTestProcessArgs("badflags", t, ".", []string{"0"}, 0, func(p *proc.Target, fixture protest.Fixture) { mainfn := p.BinInfo().LookupFunc["main.main"] + grp := proc.NewGroup(p) + // Find JNZ instruction on line :14 var addr uint64 text, err := proc.Disassemble(p.Memory(), nil, p.Breakpoints(), p.BinInfo(), mainfn.Entry, mainfn.End) @@ -5886,7 +5898,7 @@ func TestCallInjectionFlagCorruption(t *testing.T) { assertNoError(err, t, "SetBreakpoint") // Continue to breakpoint - assertNoError(p.Continue(), t, "Continue()") + assertNoError(grp.Continue(), t, "Continue()") assertLineNumber(p, t, 14, "expected line :14") // Save RFLAGS register @@ -5894,7 +5906,7 @@ func TestCallInjectionFlagCorruption(t *testing.T) { t.Logf("rflags before = %#x", rflagsBeforeCall) // Inject call to main.g() - assertNoError(proc.EvalExpressionWithCalls(p, p.SelectedGoroutine(), "g()", normalLoadConfig, true), t, "Call") + assertNoError(proc.EvalExpressionWithCalls(grp, p.SelectedGoroutine(), "g()", normalLoadConfig, true), t, "Call") // Check RFLAGS register after the call rflagsAfterCall := p.BinInfo().Arch.RegistersToDwarfRegisters(0, getRegisters(p, t)).Uint64Val(regnum.AMD64_Rflags) @@ -5905,7 +5917,7 @@ func TestCallInjectionFlagCorruption(t *testing.T) { } // Single step and check where we end up - assertNoError(p.Step(), t, "Step()") + assertNoError(grp.Step(), t, "Step()") assertLineNumber(p, t, 17, "expected line :17") // since we passed "0" as argument we should be going into the false branch at line :17 }) } diff --git a/pkg/proc/proc_unix_test.go b/pkg/proc/proc_unix_test.go index 74a39cc6..fa9ac0ae 100644 --- a/pkg/proc/proc_unix_test.go +++ b/pkg/proc/proc_unix_test.go @@ -37,24 +37,25 @@ func TestIssue419(t *testing.T) { // SIGINT directed at the inferior should be passed along not swallowed by delve withTestProcess("issue419", t, func(p *proc.Target, fixture protest.Fixture) { + grp := proc.NewGroup(p) setFunctionBreakpoint(p, t, "main.main") - assertNoError(p.Continue(), t, "Continue()") + assertNoError(grp.Continue(), t, "Continue()") resumeChan := make(chan struct{}, 1) go func() { time.Sleep(500 * time.Millisecond) <-resumeChan if p.Pid() <= 0 { // if we don't stop the inferior the test will never finish - p.RequestManualStop() - err := p.Detach(true) + grp.RequestManualStop() + err := grp.Detach(true) errChan <- errIssue419{pid: p.Pid(), err: err} return } err := syscall.Kill(p.Pid(), syscall.SIGINT) errChan <- errIssue419{pid: p.Pid(), err: err} }() - p.ResumeNotify(resumeChan) - errChan <- p.Continue() + grp.ResumeNotify(resumeChan) + errChan <- grp.Continue() }) for i := 0; i < 2; i++ { diff --git a/pkg/proc/stackwatch.go b/pkg/proc/stackwatch.go index 3ab57274..8d48289b 100644 --- a/pkg/proc/stackwatch.go +++ b/pkg/proc/stackwatch.go @@ -72,7 +72,7 @@ func (t *Target) setStackWatchBreakpoints(scope *EvalScope, watchpoint *Breakpoi retbreaklet.watchpoint = watchpoint retbreaklet.callback = woos - if recorded, _ := t.Recorded(); recorded && retframe.Current.Fn != nil { + if recorded, _ := t.recman.Recorded(); recorded && retframe.Current.Fn != nil { // Must also set a breakpoint on the call instruction immediately // preceding retframe.Current.PC, because the watchpoint could also go out // of scope while we are running backwards. diff --git a/pkg/proc/target.go b/pkg/proc/target.go index 6837d0ba..278e0905 100644 --- a/pkg/proc/target.go +++ b/pkg/proc/target.go @@ -36,10 +36,10 @@ const ( // Target represents the process being debugged. type Target struct { Process - RecordingManipulation - proc ProcessInternal - recman RecordingManipulationInternal + proc ProcessInternal + recman RecordingManipulationInternal + continueOnce ContinueOnceFunc pid int @@ -51,10 +51,6 @@ type Target struct { // CanDump is true if core dumping is supported. CanDump bool - // KeepSteppingBreakpoints determines whether certain stop reasons (e.g. manual halts) - // will keep the stepping breakpoints instead of clearing them. - KeepSteppingBreakpoints KeepSteppingBreakpoints - // currentThread is the thread that will be used by next/step/stepout and to evaluate variables if no goroutine is selected. currentThread Thread @@ -84,7 +80,7 @@ type Target struct { fakeMemoryRegistry []*compositeMemory fakeMemoryRegistryMap map[string]*compositeMemory - cctx *ContinueOnceContext + partOfGroup bool } type KeepSteppingBreakpoints uint8 @@ -151,6 +147,8 @@ const ( StopWatchpoint // The target process hit one or more watchpoints ) +type ContinueOnceFunc func([]ProcessInternal, *ContinueOnceContext) (trapthread Thread, stopReason StopReason, err error) + // NewTargetConfig contains the configuration for a new Target object, type NewTargetConfig struct { Path string // path of the main executable @@ -158,6 +156,7 @@ type NewTargetConfig struct { DisableAsyncPreempt bool // Go 1.14 asynchronous preemption should be disabled StopReason StopReason // Initial stop reason CanDump bool // Can create core dumps (must implement ProcessInternal.MemoryMap) + ContinueOnce ContinueOnceFunc } // DisableAsyncPreemptEnv returns a process environment (like os.Environ) @@ -200,7 +199,7 @@ func NewTarget(p ProcessInternal, pid int, currentThread Thread, cfg NewTargetCo currentThread: currentThread, CanDump: cfg.CanDump, pid: pid, - cctx: &ContinueOnceContext{}, + continueOnce: cfg.ContinueOnce, } if recman, ok := p.(RecordingManipulationInternal); ok { @@ -208,7 +207,6 @@ func NewTarget(p ProcessInternal, pid int, currentThread Thread, cfg NewTargetCo } else { t.recman = &dummyRecordingManipulation{} } - t.RecordingManipulation = t.recman g, _ := GetG(currentThread) t.selectedGoroutine = g @@ -281,12 +279,18 @@ func (t *Target) ClearCaches() { } } -// Restart will start the process over from the location specified by the "from" locspec. +// 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 (t *Target) Restart(from string) error { - t.ClearCaches() - currentThread, err := t.recman.Restart(t.cctx, from) +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 } @@ -333,11 +337,11 @@ func (p *Target) SwitchThread(tid int) error { return fmt.Errorf("thread %d does not exist", tid) } -// Detach will detach the target from the underylying process. +// detach will detach the target from the underylying process. // This means the debugger will no longer receive events from the process // we were previously debugging. // If kill is true then the process will be killed when we detach. -func (t *Target) Detach(kill bool) error { +func (t *Target) detach(kill bool) error { if !kill { if t.asyncPreemptChanged { setAsyncPreemptOff(t, t.asyncPreemptOff) @@ -475,17 +479,17 @@ func (t *Target) GetBufferedTracepoints() []*UProbeTraceResult { } // ResumeNotify specifies a channel that will be closed the next time -// Continue finishes resuming the target. -func (t *Target) ResumeNotify(ch chan<- struct{}) { +// Continue finishes resuming the targets. +func (t *TargetGroup) ResumeNotify(ch chan<- struct{}) { t.cctx.ResumeChan = ch } -// RequestManualStop attempts to stop all the process' threads. -func (t *Target) RequestManualStop() error { +// RequestManualStop attempts to stop all the processes' threads. +func (t *TargetGroup) RequestManualStop() error { t.cctx.StopMu.Lock() defer t.cctx.StopMu.Unlock() t.cctx.manualStopRequested = true - return t.proc.RequestManualStop(t.cctx) + return t.Selected.proc.RequestManualStop(t.cctx) } const ( diff --git a/pkg/proc/target_exec.go b/pkg/proc/target_exec.go index e5bd28aa..78bf4e85 100644 --- a/pkg/proc/target_exec.go +++ b/pkg/proc/target_exec.go @@ -26,27 +26,32 @@ func (err *ErrNoSourceForPC) Error() string { return fmt.Sprintf("no source for PC %#x", err.pc) } -// Next continues execution until the next source line. -func (dbp *Target) Next() (err error) { - if _, err := dbp.Valid(); err != nil { +// 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 dbp.Breakpoints().HasSteppingBreakpoints() { + if grp.HasSteppingBreakpoints() { return fmt.Errorf("next while nexting") } - if err = next(dbp, false, false); err != nil { - dbp.ClearSteppingBreakpoints() + if err = next(grp.Selected, false, false); err != nil { + grp.Selected.ClearSteppingBreakpoints() return } - return dbp.Continue() + return grp.Continue() } // Continue continues execution of the debugged -// process. It will continue until it hits a breakpoint +// processes. It will continue until it hits a breakpoint // or is otherwise stopped. -func (dbp *Target) Continue() error { +func (grp *TargetGroup) Continue() error { + if len(grp.targets) != 1 { + panic("multiple targets not implemented") + } + dbp := grp.Selected if _, err := dbp.Valid(); err != nil { return err } @@ -56,29 +61,29 @@ func (dbp *Target) Continue() error { } dbp.Breakpoints().WatchOutOfScope = nil dbp.clearHardcodedBreakpoints() - dbp.cctx.CheckAndClearManualStopRequest() + grp.cctx.CheckAndClearManualStopRequest() defer func() { // Make sure we clear internal breakpoints if we simultaneously receive a // manual stop request and hit a breakpoint. - if dbp.cctx.CheckAndClearManualStopRequest() { + if grp.cctx.CheckAndClearManualStopRequest() { dbp.StopReason = StopManual dbp.clearHardcodedBreakpoints() - if dbp.KeepSteppingBreakpoints&HaltKeepsSteppingBreakpoints == 0 { + if grp.KeepSteppingBreakpoints&HaltKeepsSteppingBreakpoints == 0 { dbp.ClearSteppingBreakpoints() } } }() for { - if dbp.cctx.CheckAndClearManualStopRequest() { + if grp.cctx.CheckAndClearManualStopRequest() { dbp.StopReason = StopManual dbp.clearHardcodedBreakpoints() - if dbp.KeepSteppingBreakpoints&HaltKeepsSteppingBreakpoints == 0 { + if grp.KeepSteppingBreakpoints&HaltKeepsSteppingBreakpoints == 0 { dbp.ClearSteppingBreakpoints() } return nil } dbp.ClearCaches() - trapthread, stopReason, contOnceErr := dbp.proc.ContinueOnce(dbp.cctx) + trapthread, stopReason, contOnceErr := grp.continueOnce([]ProcessInternal{grp.targets[0].proc}, grp.cctx) dbp.StopReason = stopReason threads := dbp.ThreadList() @@ -136,7 +141,7 @@ func (dbp *Target) Continue() error { if err := conditionErrors(threads); err != nil { return err } - if dbp.GetDirection() == Forward { + if grp.GetDirection() == Forward { text, err := disassembleCurrentInstruction(dbp, curthread, 0) if err != nil { return err @@ -155,7 +160,7 @@ func (dbp *Target) Continue() error { if err := dbp.ClearSteppingBreakpoints(); err != nil { return err } - return dbp.StepInstruction() + return grp.StepInstruction() } } else { curthread.Common().returnValues = curbp.Breakpoint.returnInfo.Collect(dbp, curthread) @@ -171,7 +176,7 @@ func (dbp *Target) Continue() error { return err } if onNextGoroutine && - (!isTraceOrTraceReturn(curbp.Breakpoint) || dbp.KeepSteppingBreakpoints&TracepointKeepsSteppingBreakpoints == 0) { + (!isTraceOrTraceReturn(curbp.Breakpoint) || grp.KeepSteppingBreakpoints&TracepointKeepsSteppingBreakpoints == 0) { err := dbp.ClearSteppingBreakpoints() if err != nil { return err @@ -287,27 +292,27 @@ func stepInstructionOut(dbp *Target, curthread Thread, fnname1, fnname2 string) } } -// Step will continue until another source line is reached. -// Will step into functions. -func (dbp *Target) Step() (err error) { - if _, err := dbp.Valid(); err != nil { +// 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 dbp.Breakpoints().HasSteppingBreakpoints() { + if grp.HasSteppingBreakpoints() { return fmt.Errorf("next while nexting") } - if err = next(dbp, true, false); err != nil { - _ = dbp.ClearSteppingBreakpoints() + if err = next(grp.Selected, true, false); err != nil { + _ = grp.Selected.ClearSteppingBreakpoints() return err } - if bpstate := dbp.CurrentThread().Breakpoint(); bpstate.Breakpoint != nil && bpstate.Active && bpstate.SteppingInto && dbp.GetDirection() == Backward { - dbp.ClearSteppingBreakpoints() - return dbp.StepInstruction() + if bpstate := grp.Selected.CurrentThread().Breakpoint(); bpstate.Breakpoint != nil && bpstate.Active && bpstate.SteppingInto && grp.GetDirection() == Backward { + grp.Selected.ClearSteppingBreakpoints() + return grp.StepInstruction() } - return dbp.Continue() + return grp.Continue() } // sameGoroutineCondition returns an expression that evaluates to true when @@ -323,17 +328,19 @@ func frameoffCondition(frame *Stackframe) ast.Expr { return astutil.Eql(astutil.PkgVar("runtime", "frameoff"), astutil.Int(frame.FrameOffset())) } -// StepOut will continue until the current goroutine exits the -// function currently being executed or a deferred function is executed -func (dbp *Target) StepOut() error { - backward := dbp.GetDirection() == Backward - if _, err := dbp.Valid(); err != nil { +// StepOut resumes the processes in the group, continuing the selected target +// until 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 dbp.Breakpoints().HasSteppingBreakpoints() { + if grp.HasSteppingBreakpoints() { return fmt.Errorf("next while nexting") } + dbp := grp.Selected selg := dbp.SelectedGoroutine() curthread := dbp.CurrentThread() @@ -355,7 +362,7 @@ func (dbp *Target) StepOut() error { } success = true - return dbp.Continue() + return grp.Continue() } sameGCond := sameGoroutineCondition(selg) @@ -366,7 +373,7 @@ func (dbp *Target) StepOut() error { } success = true - return dbp.Continue() + return grp.Continue() } deferpc, err := setDeferBreakpoint(dbp, nil, topframe, sameGCond, false) @@ -395,14 +402,15 @@ func (dbp *Target) StepOut() error { } success = true - return dbp.Continue() + 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 (dbp *Target) StepInstruction() (err error) { +func (grp *TargetGroup) StepInstruction() (err error) { + dbp := grp.Selected thread := dbp.CurrentThread() g := dbp.SelectedGoroutine() if g != nil { @@ -412,7 +420,7 @@ func (dbp *Target) StepInstruction() (err error) { sameGoroutineCondition(dbp.SelectedGoroutine())); err != nil { return err } - return dbp.Continue() + return grp.Continue() } thread = g.Thread } @@ -464,7 +472,7 @@ func (dbp *Target) StepInstruction() (err error) { // 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.GetDirection() == Backward + backward := dbp.recman.GetDirection() == Backward selg := dbp.SelectedGoroutine() curthread := dbp.CurrentThread() topframe, retframe, err := topframe(selg, curthread) @@ -1086,7 +1094,7 @@ func (tgt *Target) clearHardcodedBreakpoints() { func (tgt *Target) handleHardcodedBreakpoints(trapthread Thread, threads []Thread) error { mem := tgt.Memory() arch := tgt.BinInfo().Arch - recorded, _ := tgt.Recorded() + recorded, _ := tgt.recman.Recorded() isHardcodedBreakpoint := func(thread Thread, pc uint64) uint64 { for _, bpinstr := range [][]byte{arch.BreakpointInstruction(), arch.AltBreakpointInstruction()} { @@ -1150,7 +1158,7 @@ func (tgt *Target) handleHardcodedBreakpoints(trapthread Thread, threads []Threa switch { case loc.Fn.Name == "runtime.breakpoint": - if recorded, _ := tgt.Recorded(); recorded { + if recorded, _ := tgt.recman.Recorded(); recorded { setHardcodedBreakpoint(thread, loc) continue } diff --git a/pkg/proc/target_group.go b/pkg/proc/target_group.go new file mode 100644 index 00000000..b7976fa5 --- /dev/null +++ b/pkg/proc/target_group.go @@ -0,0 +1,123 @@ +package proc + +import ( + "fmt" + "strings" +) + +// TargetGroup reperesents a group of target processes being debugged that +// will be resumed and stopped simultaneously. +// New targets are automatically added to the group if exec catching is +// enabled and the backend supports it, otherwise the group will always +// contain a single target process. +type TargetGroup struct { + targets []*Target + Selected *Target + + RecordingManipulation + recman RecordingManipulationInternal + + // KeepSteppingBreakpoints determines whether certain stop reasons (e.g. manual halts) + // will keep the stepping breakpoints instead of clearing them. + KeepSteppingBreakpoints KeepSteppingBreakpoints + + LogicalBreakpoints map[int]*LogicalBreakpoint + + continueOnce ContinueOnceFunc + cctx *ContinueOnceContext +} + +// NewGroup creates a TargetGroup containing the specified Target. +func NewGroup(t *Target) *TargetGroup { + if t.partOfGroup { + panic("internal error: target is already part of a group") + } + t.partOfGroup = true + return &TargetGroup{ + RecordingManipulation: t.recman, + targets: []*Target{t}, + Selected: t, + cctx: &ContinueOnceContext{}, + recman: t.recman, + LogicalBreakpoints: t.Breakpoints().Logical, + continueOnce: t.continueOnce, + } +} + +// Targets returns a slice of targets in the group. +func (grp *TargetGroup) Targets() []*Target { + return grp.targets +} + +// Valid returns true if any target in the target group is valid. +func (grp *TargetGroup) Valid() (bool, error) { + var err0 error + for _, t := range grp.targets { + ok, err := t.Valid() + if ok { + return true, nil + } + err0 = err + } + return false, err0 +} + +// Detach detaches all targets in the group. +func (grp *TargetGroup) Detach(kill bool) error { + var errs []string + for _, t := range grp.targets { + isvalid, _ := t.Valid() + if !isvalid { + continue + } + err := t.detach(kill) + if err != nil { + errs = append(errs, fmt.Sprintf("could not detach process %d: %v", t.Pid(), err)) + } + } + if len(errs) > 0 { + return fmt.Errorf("%s", strings.Join(errs, "\n")) + } + return nil +} + +// HasSteppingBreakpoints returns true if any of the targets has stepping breakpoints set. +func (grp *TargetGroup) HasSteppingBreakpoints() bool { + for _, t := range grp.targets { + if t.Breakpoints().HasSteppingBreakpoints() { + return true + } + } + return false +} + +// ClearSteppingBreakpoints removes all stepping breakpoints. +func (grp *TargetGroup) ClearSteppingBreakpoints() error { + for _, t := range grp.targets { + if t.Breakpoints().HasSteppingBreakpoints() { + return t.ClearSteppingBreakpoints() + } + } + return nil +} + +// ThreadList returns a list of all threads in all target processes. +func (grp *TargetGroup) ThreadList() []Thread { + r := []Thread{} + for _, t := range grp.targets { + r = append(r, t.ThreadList()...) + } + return r +} + +// TargetForThread returns the target containing the given thread. +func (grp *TargetGroup) TargetForThread(thread Thread) *Target { + for _, t := range grp.targets { + for _, th := range t.ThreadList() { + if th == thread { + return t + } + } + } + return nil +} diff --git a/pkg/proc/variables_test.go b/pkg/proc/variables_test.go index 145364e8..43403851 100644 --- a/pkg/proc/variables_test.go +++ b/pkg/proc/variables_test.go @@ -1232,44 +1232,45 @@ func TestCallFunction(t *testing.T) { } withTestProcessArgs("fncall", t, ".", nil, protest.AllNonOptimized, func(p *proc.Target, fixture protest.Fixture) { + grp := proc.NewGroup(p) testCallFunctionSetBreakpoint(t, p, fixture) - assertNoError(p.Continue(), t, "Continue()") + assertNoError(grp.Continue(), t, "Continue()") for _, tc := range testcases { - testCallFunction(t, p, tc) + testCallFunction(t, grp, p, tc) } if goversion.VersionAfterOrEqual(runtime.Version(), 1, 12) { for _, tc := range testcases112 { - testCallFunction(t, p, tc) + testCallFunction(t, grp, p, tc) } if !goversion.VersionAfterOrEqual(runtime.Version(), 1, 14) { for _, tc := range testcasesBefore114After112 { - testCallFunction(t, p, tc) + testCallFunction(t, grp, p, tc) } } } if goversion.VersionAfterOrEqual(runtime.Version(), 1, 13) { for _, tc := range testcases113 { - testCallFunction(t, p, tc) + testCallFunction(t, grp, p, tc) } } if goversion.VersionAfterOrEqual(runtime.Version(), 1, 14) { for _, tc := range testcases114 { - testCallFunction(t, p, tc) + testCallFunction(t, grp, p, tc) } } if goversion.VersionAfterOrEqual(runtime.Version(), 1, 17) { for _, tc := range testcases117 { - testCallFunction(t, p, tc) + testCallFunction(t, grp, p, tc) } } // LEAVE THIS AS THE LAST ITEM, IT BREAKS THE TARGET PROCESS!!! - testCallFunction(t, p, testCaseCallFunction{"-unsafe escapeArg(&a2)", nil, nil}) + testCallFunction(t, grp, p, testCaseCallFunction{"-unsafe escapeArg(&a2)", nil, nil}) }) } @@ -1284,7 +1285,7 @@ func testCallFunctionSetBreakpoint(t *testing.T, p *proc.Target, fixture protest } } -func testCallFunction(t *testing.T, p *proc.Target, tc testCaseCallFunction) { +func testCallFunction(t *testing.T, grp *proc.TargetGroup, p *proc.Target, tc testCaseCallFunction) { const unsafePrefix = "-unsafe " var callExpr, varExpr string @@ -1302,7 +1303,7 @@ func testCallFunction(t *testing.T, p *proc.Target, tc testCaseCallFunction) { checkEscape = false } t.Logf("call %q", tc.expr) - err := proc.EvalExpressionWithCalls(p, p.SelectedGoroutine(), callExpr, pnormalLoadConfig, checkEscape) + err := proc.EvalExpressionWithCalls(grp, p.SelectedGoroutine(), callExpr, pnormalLoadConfig, checkEscape) if tc.err != nil { t.Logf("\terr = %v\n", err) if err == nil { diff --git a/service/dap/server.go b/service/dap/server.go index 640e2950..fc97f718 100644 --- a/service/dap/server.go +++ b/service/dap/server.go @@ -1558,7 +1558,7 @@ func (s *Session) onConfigurationDoneRequest(request *dap.ConfigurationDoneReque } s.send(e) } - s.debugger.Target().KeepSteppingBreakpoints = proc.HaltKeepsSteppingBreakpoints | proc.TracepointKeepsSteppingBreakpoints + s.debugger.TargetGroup().KeepSteppingBreakpoints = proc.HaltKeepsSteppingBreakpoints | proc.TracepointKeepsSteppingBreakpoints s.logToConsole("Type 'dlv help' for list of commands.") s.send(&dap.ConfigurationDoneResponse{Response: *newResponse(request.Request)}) diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go index 886bdbd0..85d8b83d 100644 --- a/service/debugger/debugger.go +++ b/service/debugger/debugger.go @@ -61,7 +61,7 @@ type Debugger struct { processArgs []string targetMutex sync.Mutex - target *proc.Target + target *proc.TargetGroup log *logrus.Entry @@ -161,7 +161,7 @@ func New(config *Config, processArgs []string) (*Debugger, error) { err = noDebugErrorWarning(err) return nil, attachErrorMessage(d.config.AttachPid, err) } - d.target = p + d.target = proc.NewGroup(p) case d.config.CoreFile != "": var p *proc.Target @@ -178,7 +178,7 @@ func New(config *Config, processArgs []string) (*Debugger, error) { err = go11DecodeErrorCheck(err) return nil, err } - d.target = p + d.target = proc.NewGroup(p) if err := d.checkGoVersion(); err != nil { d.target.Detach(true) return nil, err @@ -197,7 +197,7 @@ func New(config *Config, processArgs []string) (*Debugger, error) { } if p != nil { // if p == nil and err == nil then we are doing a recording, don't touch d.target - d.target = p + d.target = proc.NewGroup(p) } if err := d.checkGoVersion(); err != nil { d.target.Detach(true) @@ -225,7 +225,7 @@ func (d *Debugger) checkGoVersion() error { // do not do anything if we are still recording return nil } - producer := d.target.BinInfo().Producer() + producer := d.target.Selected.BinInfo().Producer() if producer == "" { return nil } @@ -235,7 +235,7 @@ func (d *Debugger) checkGoVersion() error { func (d *Debugger) TargetGoVersion() string { d.targetMutex.Lock() defer d.targetMutex.Unlock() - return d.target.BinInfo().Producer() + return d.target.Selected.BinInfo().Producer() } // Launch will start a process with the given args and working directory. @@ -285,7 +285,7 @@ func (d *Debugger) Launch(processArgs []string, wd string) (*proc.Target, error) os.Exit(1) } d.recordingDone() - d.target = p + d.target = proc.NewGroup(p) if err := d.checkGoVersion(); err != nil { d.log.Error(err) err := d.target.Detach(true) @@ -368,7 +368,7 @@ func betterGdbserialLaunchError(p *proc.Target, err error) (*proc.Target, error) func (d *Debugger) ProcessPid() int { d.targetMutex.Lock() defer d.targetMutex.Unlock() - return d.target.Pid() + return d.target.Selected.Pid() } // LastModified returns the time that the process' executable was last @@ -376,7 +376,7 @@ func (d *Debugger) ProcessPid() int { func (d *Debugger) LastModified() time.Time { d.targetMutex.Lock() defer d.targetMutex.Unlock() - return d.target.BinInfo().LastModified() + return d.target.Selected.BinInfo().LastModified() } // FunctionReturnLocations returns all return locations @@ -387,7 +387,7 @@ func (d *Debugger) FunctionReturnLocations(fnName string) ([]uint64, error) { defer d.targetMutex.Unlock() var ( - p = d.target + p = d.target.Selected g = p.SelectedGoroutine() ) @@ -463,12 +463,6 @@ func (d *Debugger) Restart(rerecord bool, pos string, resetArgs bool, newArgs [] return nil, ErrCanNotRestart } - if valid, _ := d.target.Valid(); valid && !recorded { - // Ensure the process is in a PTRACE_STOP. - if err := stopProcess(d.target.Pid()); err != nil { - return nil, err - } - } if err := d.detach(true); err != nil { return nil, err } @@ -514,9 +508,9 @@ func (d *Debugger) Restart(rerecord bool, pos string, resetArgs bool, newArgs [] } discarded := []api.DiscardedBreakpoint{} - p.Breakpoints().Logical = d.target.Breakpoints().Logical - d.target = p - for _, oldBp := range d.target.Breakpoints().Logical { + p.Breakpoints().Logical = d.target.LogicalBreakpoints + d.target = proc.NewGroup(p) + for _, oldBp := range d.target.LogicalBreakpoints { if oldBp.LogicalID < 0 || !oldBp.Enabled { continue } @@ -566,16 +560,19 @@ func (d *Debugger) state(retLoadCfg *proc.LoadConfig) (*api.DebuggerState, error goroutine *api.Goroutine ) - if d.target.SelectedGoroutine() != nil { - goroutine = api.ConvertGoroutine(d.target, d.target.SelectedGoroutine()) + tgt := d.target.Selected + + if tgt.SelectedGoroutine() != nil { + goroutine = api.ConvertGoroutine(tgt, tgt.SelectedGoroutine()) } exited := false - if _, err := d.target.Valid(); err != nil { + if _, err := tgt.Valid(); err != nil { _, exited = err.(proc.ErrProcessExited) } state = &api.DebuggerState{ + Pid: tgt.Pid(), SelectedGoroutine: goroutine, Exited: exited, } @@ -589,22 +586,23 @@ func (d *Debugger) state(retLoadCfg *proc.LoadConfig) (*api.DebuggerState, error } state.Threads = append(state.Threads, th) - if thread.ThreadID() == d.target.CurrentThread().ThreadID() { + if thread.ThreadID() == tgt.CurrentThread().ThreadID() { state.CurrentThread = th } } - state.NextInProgress = d.target.Breakpoints().HasSteppingBreakpoints() + state.NextInProgress = d.target.HasSteppingBreakpoints() if recorded, _ := d.target.Recorded(); recorded { state.When, _ = d.target.When() } - state.WatchOutOfScope = make([]*api.Breakpoint, 0, len(d.target.Breakpoints().WatchOutOfScope)) - for _, bp := range d.target.Breakpoints().WatchOutOfScope { - abp := api.ConvertLogicalBreakpoint(bp.Logical) - api.ConvertPhysicalBreakpoints(abp, []*proc.Breakpoint{bp}) - state.WatchOutOfScope = append(state.WatchOutOfScope, abp) + for _, t := range d.target.Targets() { + for _, bp := range t.Breakpoints().WatchOutOfScope { + abp := api.ConvertLogicalBreakpoint(bp.Logical) + api.ConvertPhysicalBreakpoints(abp, []*proc.Breakpoint{bp}) + state.WatchOutOfScope = append(state.WatchOutOfScope, abp) + } } return state, nil @@ -641,6 +639,15 @@ func (d *Debugger) CreateBreakpoint(requestedBp *api.Breakpoint) (*api.Breakpoin d.targetMutex.Lock() defer d.targetMutex.Unlock() + if len(d.target.Targets()) != 1 { + //TODO(aarzilli): + // - the calls to FindFileLocation and FindFunctionLocation need to be done on all targets + // - the Addrs slice and the Addr field need to be converted to a format + // that can specify to which target an address belongs when there are + // multiple targets (but this must happen in a backwards compatible way) + panic("multiple targets not implemented") + } + var ( addrs []uint64 err error @@ -660,16 +667,19 @@ func (d *Debugger) CreateBreakpoint(requestedBp *api.Breakpoint) (*api.Breakpoin if runtime.GOOS == "windows" { // Accept fileName which is case-insensitive and slash-insensitive match fileNameNormalized := strings.ToLower(filepath.ToSlash(fileName)) - for _, symFile := range d.target.BinInfo().Sources { - if fileNameNormalized == strings.ToLower(filepath.ToSlash(symFile)) { - fileName = symFile - break + caseInsensitiveSearch: + for _, t := range d.target.Targets() { + for _, symFile := range t.BinInfo().Sources { + if fileNameNormalized == strings.ToLower(filepath.ToSlash(symFile)) { + fileName = symFile + break caseInsensitiveSearch + } } } } - addrs, err = proc.FindFileLocation(d.target, fileName, requestedBp.Line) + addrs, err = proc.FindFileLocation(d.target.Selected, fileName, requestedBp.Line) case len(requestedBp.FunctionName) > 0: - addrs, err = proc.FindFunctionLocation(d.target, requestedBp.FunctionName, requestedBp.Line) + addrs, err = proc.FindFunctionLocation(d.target.Selected, requestedBp.FunctionName, requestedBp.Line) case len(requestedBp.Addrs) > 0: addrs = requestedBp.Addrs default: @@ -704,7 +714,10 @@ func (d *Debugger) ConvertThreadBreakpoint(thread proc.Thread) *api.Breakpoint { // createLogicalBreakpoint creates one physical breakpoint for each address // in addrs and associates all of them with the same logical breakpoint. func createLogicalBreakpoint(d *Debugger, addrs []uint64, requestedBp *api.Breakpoint, id int) (*api.Breakpoint, error) { - p := d.target + if len(d.target.Targets()) != 1 { + panic("multiple targets not implemented") + } + p := d.target.Selected if lbp := p.Breakpoints().Logical[requestedBp.ID]; lbp != nil { abp := d.convertBreakpoint(lbp) @@ -752,13 +765,17 @@ func createLogicalBreakpoint(d *Debugger, addrs []uint64, requestedBp *api.Break } func (d *Debugger) createPhysicalBreakpoints(lbp *proc.LogicalBreakpoint) error { - addrs, err := proc.FindFileLocation(d.target, lbp.File, lbp.Line) + if len(d.target.Targets()) != 1 { + panic("multiple targets not implemented") + } + p := d.target.Selected + addrs, err := proc.FindFileLocation(p, lbp.File, lbp.Line) if err != nil { return err } bps := make([]*proc.Breakpoint, len(addrs)) for i := range addrs { - bps[i], err = d.target.SetBreakpoint(lbp.LogicalID, addrs[i], proc.UserBreakpoint, nil) + bps[i], err = p.SetBreakpoint(lbp.LogicalID, addrs[i], proc.UserBreakpoint, nil) if err != nil { break } @@ -771,7 +788,7 @@ func (d *Debugger) createPhysicalBreakpoints(lbp *proc.LogicalBreakpoint) error if bp == nil { continue } - if err1 := d.target.ClearBreakpoint(bp.Addr); err1 != nil { + if err1 := p.ClearBreakpoint(bp.Addr); err1 != nil { return fmt.Errorf("error while creating breakpoint: %v, additionally the breakpoint could not be properly rolled back: %v", err, err1) } } @@ -781,12 +798,16 @@ func (d *Debugger) createPhysicalBreakpoints(lbp *proc.LogicalBreakpoint) error } func (d *Debugger) clearPhysicalBreakpoints(id int) error { + if len(d.target.Targets()) != 1 { + panic("multiple targets not implemented") + } + p := d.target.Selected var errs []error n := 0 - for _, bp := range d.target.Breakpoints().M { + for _, bp := range p.Breakpoints().M { if bp.LogicalID() == id { n++ - err := d.target.ClearBreakpoint(bp.Addr) + err := p.ClearBreakpoint(bp.Addr) if err != nil { errs = append(errs, err) } @@ -817,15 +838,21 @@ func isBreakpointExistsErr(err error) bool { func (d *Debugger) CreateEBPFTracepoint(fnName string) error { d.targetMutex.Lock() defer d.targetMutex.Unlock() - - return d.target.SetEBPFTracepoint(fnName) + if len(d.target.Targets()) != 1 { + panic("multiple targets not implemented") + } + p := d.target.Selected + return p.SetEBPFTracepoint(fnName) } // amendBreakpoint will update the breakpoint with the matching ID. // It also enables or disables the breakpoint. // We can consume this function to avoid locking a goroutine. func (d *Debugger) amendBreakpoint(amend *api.Breakpoint) error { - original := d.target.Breakpoints().Logical[amend.ID] + if len(d.target.Targets()) != 1 { + panic("multiple targets not implemented") + } + original := d.target.LogicalBreakpoints[amend.ID] if original == nil { return fmt.Errorf("no breakpoint with ID %d", amend.ID) } @@ -956,15 +983,20 @@ func (d *Debugger) ClearBreakpoint(requestedBp *api.Breakpoint) (*api.Breakpoint // clearBreakpoint clears a breakpoint, we can consume this function to avoid locking a goroutine func (d *Debugger) clearBreakpoint(requestedBp *api.Breakpoint) (*api.Breakpoint, error) { + if len(d.target.Targets()) != 1 { + panic("multiple targets not implemented") + } + p := d.target.Selected + if requestedBp.ID <= 0 { - bp := d.target.Breakpoints().M[requestedBp.Addr] + bp := p.Breakpoints().M[requestedBp.Addr] requestedBp.ID = bp.LogicalID() } - lbp := d.target.Breakpoints().Logical[requestedBp.ID] + lbp := d.target.LogicalBreakpoints[requestedBp.ID] clearedBp := d.convertBreakpoint(lbp) - delete(d.target.Breakpoints().Logical, requestedBp.ID) + delete(d.target.LogicalBreakpoints, requestedBp.ID) err := d.clearPhysicalBreakpoints(requestedBp.ID) if err != nil { @@ -1006,10 +1038,14 @@ func isBpHitCondNotSatisfiable(bp *api.Breakpoint) bool { func (d *Debugger) Breakpoints(all bool) []*api.Breakpoint { d.targetMutex.Lock() defer d.targetMutex.Unlock() + if len(d.target.Targets()) != 1 { + panic("multiple targets not implemented") + } + p := d.target.Selected abps := []*api.Breakpoint{} if all { - for _, bp := range d.target.Breakpoints().M { + for _, bp := range p.Breakpoints().M { var abp *api.Breakpoint if bp.Logical != nil { abp = api.ConvertLogicalBreakpoint(bp.Logical) @@ -1021,7 +1057,7 @@ func (d *Debugger) Breakpoints(all bool) []*api.Breakpoint { abps = append(abps, abp) } } else { - for _, lbp := range d.target.Breakpoints().Logical { + for _, lbp := range d.target.LogicalBreakpoints { abps = append(abps, d.convertBreakpoint(lbp)) } } @@ -1032,7 +1068,7 @@ func (d *Debugger) Breakpoints(all bool) []*api.Breakpoint { func (d *Debugger) FindBreakpoint(id int) *api.Breakpoint { d.targetMutex.Lock() defer d.targetMutex.Unlock() - lbp := d.target.Breakpoints().Logical[id] + lbp := d.target.LogicalBreakpoints[id] if lbp == nil { return nil } @@ -1040,8 +1076,13 @@ func (d *Debugger) FindBreakpoint(id int) *api.Breakpoint { } func (d *Debugger) findBreakpoint(id int) []*proc.Breakpoint { + if len(d.target.Targets()) != 1 { + panic("multiple targets not implemented") + } + p := d.target.Selected + var bps []*proc.Breakpoint - for _, bp := range d.target.Breakpoints().M { + for _, bp := range p.Breakpoints().M { if bp.LogicalID() == id { bps = append(bps, bp) } @@ -1057,7 +1098,7 @@ func (d *Debugger) FindBreakpointByName(name string) *api.Breakpoint { } func (d *Debugger) findBreakpointByName(name string) *api.Breakpoint { - for _, lbp := range d.target.Breakpoints().Logical { + for _, lbp := range d.target.LogicalBreakpoints { if lbp.Name == name { return d.convertBreakpoint(lbp) } @@ -1067,12 +1108,17 @@ func (d *Debugger) findBreakpointByName(name string) *api.Breakpoint { // CreateWatchpoint creates a watchpoint on the specified expression. func (d *Debugger) CreateWatchpoint(goid, frame, deferredCall int, expr string, wtype api.WatchType) (*api.Breakpoint, error) { - s, err := proc.ConvertEvalScope(d.target, goid, frame, deferredCall) + if len(d.target.Targets()) != 1 { + panic("multiple targets not implemented") + } + p := d.target.Selected + + s, err := proc.ConvertEvalScope(p, goid, frame, deferredCall) if err != nil { return nil, err } d.breakpointIDCounter++ - bp, err := d.target.SetWatchpoint(d.breakpointIDCounter, s, expr, proc.WatchType(wtype), nil) + bp, err := p.SetWatchpoint(d.breakpointIDCounter, s, expr, proc.WatchType(wtype), nil) if err != nil { return nil, err } @@ -1116,7 +1162,7 @@ func (d *Debugger) FindGoroutine(id int) (*proc.G, error) { d.targetMutex.Lock() defer d.targetMutex.Unlock() - return proc.FindGoroutine(d.target, id) + return proc.FindGoroutine(d.target.Selected, id) } func (d *Debugger) setRunning(running bool) { @@ -1184,9 +1230,9 @@ func (d *Debugger) Command(command *api.DebuggerCommand, resumeNotify chan struc if command.ReturnInfoLoadConfig == nil { return nil, errors.New("can not call function with nil ReturnInfoLoadConfig") } - g := d.target.SelectedGoroutine() + g := d.target.Selected.SelectedGoroutine() if command.GoroutineID > 0 { - g, err = proc.FindGoroutine(d.target, command.GoroutineID) + g, err = proc.FindGoroutine(d.target.Selected, command.GoroutineID) if err != nil { return nil, err } @@ -1248,14 +1294,14 @@ func (d *Debugger) Command(command *api.DebuggerCommand, resumeNotify chan struc err = d.target.StepOut() case api.SwitchThread: d.log.Debugf("switching to thread %d", command.ThreadID) - err = d.target.SwitchThread(command.ThreadID) + err = d.target.Selected.SwitchThread(command.ThreadID) withBreakpointInfo = false case api.SwitchGoroutine: d.log.Debugf("switching to goroutine %d", command.GoroutineID) var g *proc.G - g, err = proc.FindGoroutine(d.target, command.GoroutineID) + g, err = proc.FindGoroutine(d.target.Selected, command.GoroutineID) if err == nil { - err = d.target.SwitchGoroutine(g) + err = d.target.Selected.SwitchGoroutine(g) } withBreakpointInfo = false case api.Halt: @@ -1266,7 +1312,7 @@ func (d *Debugger) Command(command *api.DebuggerCommand, resumeNotify chan struc if err != nil { if pe, ok := err.(proc.ErrProcessExited); ok && command.Name != api.SwitchGoroutine && command.Name != api.SwitchThread { state := &api.DebuggerState{} - state.Pid = d.target.Pid() + state.Pid = d.target.Selected.Pid() state.Exited = true state.ExitStatus = pe.Status state.Err = pe @@ -1298,6 +1344,8 @@ func (d *Debugger) Command(command *api.DebuggerCommand, resumeNotify chan struc } func (d *Debugger) collectBreakpointInformation(state *api.DebuggerState) error { + //TODO(aarzilli): this doesn't work when there are multiple targets because the state.Threads slice will contain threads from all targets, not just d.target .Selected + if state == nil { return nil } @@ -1312,15 +1360,15 @@ func (d *Debugger) collectBreakpointInformation(state *api.DebuggerState) error state.Threads[i].BreakpointInfo = bpi if bp.Goroutine { - g, err := proc.GetG(d.target.CurrentThread()) + g, err := proc.GetG(d.target.Selected.CurrentThread()) if err != nil { return err } - bpi.Goroutine = api.ConvertGoroutine(d.target, g) + bpi.Goroutine = api.ConvertGoroutine(d.target.Selected, g) } if bp.Stacktrace > 0 { - rawlocs, err := proc.ThreadStacktrace(d.target.CurrentThread(), bp.Stacktrace) + rawlocs, err := proc.ThreadStacktrace(d.target.Selected.CurrentThread(), bp.Stacktrace) if err != nil { return err } @@ -1330,7 +1378,7 @@ func (d *Debugger) collectBreakpointInformation(state *api.DebuggerState) error } } - thread, found := d.target.FindThread(state.Threads[i].ID) + thread, found := d.target.Selected.FindThread(state.Threads[i].ID) if !found { return fmt.Errorf("could not find thread %d", state.Threads[i].ID) } @@ -1340,7 +1388,7 @@ func (d *Debugger) collectBreakpointInformation(state *api.DebuggerState) error continue } - s, err := proc.GoroutineScope(d.target, thread) + s, err := proc.GoroutineScope(d.target.Selected, thread) if err != nil { return err } @@ -1382,14 +1430,33 @@ func (d *Debugger) Sources(filter string) ([]string, error) { } files := []string{} - for _, f := range d.target.BinInfo().Sources { - if regex.Match([]byte(f)) { - files = append(files, f) + for _, t := range d.target.Targets() { + for _, f := range t.BinInfo().Sources { + if regex.Match([]byte(f)) { + files = append(files, f) + } } } + sort.Strings(files) + files = uniq(files) return files, nil } +func uniq(s []string) []string { + if len(s) <= 0 { + return s + } + src, dst := 1, 1 + for src < len(s) { + if s[src] != s[dst-1] { + s[dst] = s[src] + dst++ + } + src++ + } + return s[:dst] +} + // Functions returns a list of functions in the target process. func (d *Debugger) Functions(filter string) ([]string, error) { d.targetMutex.Lock() @@ -1401,11 +1468,15 @@ func (d *Debugger) Functions(filter string) ([]string, error) { } funcs := []string{} - for _, f := range d.target.BinInfo().Functions { - if regex.MatchString(f.Name) { - funcs = append(funcs, f.Name) + for _, t := range d.target.Targets() { + for _, f := range t.BinInfo().Functions { + if regex.MatchString(f.Name) { + funcs = append(funcs, f.Name) + } } } + sort.Strings(funcs) + funcs = uniq(funcs) return funcs, nil } @@ -1419,17 +1490,22 @@ func (d *Debugger) Types(filter string) ([]string, error) { return nil, fmt.Errorf("invalid filter argument: %s", err.Error()) } - types, err := d.target.BinInfo().Types() - if err != nil { - return nil, err - } + r := []string{} - r := make([]string, 0, len(types)) - for _, typ := range types { - if regex.Match([]byte(typ)) { - r = append(r, typ) + for _, t := range d.target.Targets() { + types, err := t.BinInfo().Types() + if err != nil { + return nil, err + } + + for _, typ := range types { + if regex.Match([]byte(typ)) { + r = append(r, typ) + } } } + sort.Strings(r) + r = uniq(r) return r, nil } @@ -1439,13 +1515,14 @@ func (d *Debugger) Types(filter string) ([]string, error) { func (d *Debugger) PackageVariables(filter string, cfg proc.LoadConfig) ([]*proc.Variable, error) { d.targetMutex.Lock() defer d.targetMutex.Unlock() + p := d.target.Selected regex, err := regexp.Compile(filter) if err != nil { return nil, fmt.Errorf("invalid filter argument: %s", err.Error()) } - scope, err := proc.ThreadScope(d.target, d.target.CurrentThread()) + scope, err := proc.ThreadScope(p, p.CurrentThread()) if err != nil { return nil, err } @@ -1467,7 +1544,7 @@ func (d *Debugger) ThreadRegisters(threadID int, floatingPoint bool) (*op.DwarfR d.targetMutex.Lock() defer d.targetMutex.Unlock() - thread, found := d.target.FindThread(threadID) + thread, found := d.target.Selected.FindThread(threadID) if !found { return nil, fmt.Errorf("couldn't find thread %d", threadID) } @@ -1475,7 +1552,7 @@ func (d *Debugger) ThreadRegisters(threadID int, floatingPoint bool) (*op.DwarfR if err != nil { return nil, err } - return d.target.BinInfo().Arch.RegistersToDwarfRegisters(0, regs), nil + return d.target.Selected.BinInfo().Arch.RegistersToDwarfRegisters(0, regs), nil } // ScopeRegisters returns registers for the specified scope. @@ -1483,7 +1560,7 @@ func (d *Debugger) ScopeRegisters(goid, frame, deferredCall int, floatingPoint b d.targetMutex.Lock() defer d.targetMutex.Unlock() - s, err := proc.ConvertEvalScope(d.target, goid, frame, deferredCall) + s, err := proc.ConvertEvalScope(d.target.Selected, goid, frame, deferredCall) if err != nil { return nil, err } @@ -1492,7 +1569,7 @@ func (d *Debugger) ScopeRegisters(goid, frame, deferredCall int, floatingPoint b // DwarfRegisterToString returns the name and value representation of the given register. func (d *Debugger) DwarfRegisterToString(i int, reg *op.DwarfRegister) (string, bool, string) { - return d.target.BinInfo().Arch.DwarfRegisterToString(i, reg) + return d.target.Selected.BinInfo().Arch.DwarfRegisterToString(i, reg) } // LocalVariables returns a list of the local variables. @@ -1500,7 +1577,7 @@ func (d *Debugger) LocalVariables(goid, frame, deferredCall int, cfg proc.LoadCo d.targetMutex.Lock() defer d.targetMutex.Unlock() - s, err := proc.ConvertEvalScope(d.target, goid, frame, deferredCall) + s, err := proc.ConvertEvalScope(d.target.Selected, goid, frame, deferredCall) if err != nil { return nil, err } @@ -1512,7 +1589,7 @@ func (d *Debugger) FunctionArguments(goid, frame, deferredCall int, cfg proc.Loa d.targetMutex.Lock() defer d.targetMutex.Unlock() - s, err := proc.ConvertEvalScope(d.target, goid, frame, deferredCall) + s, err := proc.ConvertEvalScope(d.target.Selected, goid, frame, deferredCall) if err != nil { return nil, err } @@ -1524,7 +1601,7 @@ func (d *Debugger) Function(goid, frame, deferredCall int, cfg proc.LoadConfig) d.targetMutex.Lock() defer d.targetMutex.Unlock() - s, err := proc.ConvertEvalScope(d.target, goid, frame, deferredCall) + s, err := proc.ConvertEvalScope(d.target.Selected, goid, frame, deferredCall) if err != nil { return nil, err } @@ -1537,7 +1614,7 @@ func (d *Debugger) EvalVariableInScope(goid, frame, deferredCall int, expr strin d.targetMutex.Lock() defer d.targetMutex.Unlock() - s, err := proc.ConvertEvalScope(d.target, goid, frame, deferredCall) + s, err := proc.ConvertEvalScope(d.target.Selected, goid, frame, deferredCall) if err != nil { return nil, err } @@ -1558,7 +1635,7 @@ func (d *Debugger) SetVariableInScope(goid, frame, deferredCall int, symbol, val d.targetMutex.Lock() defer d.targetMutex.Unlock() - s, err := proc.ConvertEvalScope(d.target, goid, frame, deferredCall) + s, err := proc.ConvertEvalScope(d.target.Selected, goid, frame, deferredCall) if err != nil { return err } @@ -1569,7 +1646,7 @@ func (d *Debugger) SetVariableInScope(goid, frame, deferredCall int, symbol, val func (d *Debugger) Goroutines(start, count int) ([]*proc.G, int, error) { d.targetMutex.Lock() defer d.targetMutex.Unlock() - return proc.GoroutinesInfo(d.target, start, count) + return proc.GoroutinesInfo(d.target.Selected, start, count) } // FilterGoroutines returns the goroutines in gs that satisfy the specified filters. @@ -1583,7 +1660,7 @@ func (d *Debugger) FilterGoroutines(gs []*proc.G, filters []api.ListGoroutinesFi for _, g := range gs { ok := true for i := range filters { - if !matchGoroutineFilter(d.target, g, &filters[i]) { + if !matchGoroutineFilter(d.target.Selected, g, &filters[i]) { ok = false break } @@ -1663,13 +1740,13 @@ func (d *Debugger) GroupGoroutines(gs []*proc.G, group *api.GoroutineGroupingOpt case api.GoroutineGoLoc: key = formatLoc(g.Go()) case api.GoroutineStartLoc: - key = formatLoc(g.StartLoc(d.target)) + key = formatLoc(g.StartLoc(d.target.Selected)) case api.GoroutineLabel: key = fmt.Sprintf("%s=%s", group.GroupByKey, g.Labels()[group.GroupByKey]) case api.GoroutineRunning: key = fmt.Sprintf("running=%v", g.Thread != nil) case api.GoroutineUser: - key = fmt.Sprintf("user=%v", !g.System(d.target)) + key = fmt.Sprintf("user=%v", !g.System(d.target.Selected)) } if len(groupMembers[key]) < group.MaxGroupMembers { groupMembers[key] = append(groupMembers[key], g) @@ -1708,13 +1785,13 @@ func (d *Debugger) Stacktrace(goroutineID, depth int, opts api.StacktraceOptions return nil, err } - g, err := proc.FindGoroutine(d.target, goroutineID) + g, err := proc.FindGoroutine(d.target.Selected, goroutineID) if err != nil { return nil, err } if g == nil { - return proc.ThreadStacktrace(d.target.CurrentThread(), depth) + return proc.ThreadStacktrace(d.target.Selected.CurrentThread(), depth) } else { return g.Stacktrace(depth, proc.StacktraceOptions(opts)) } @@ -1729,7 +1806,7 @@ func (d *Debugger) Ancestors(goroutineID, numAncestors, depth int) ([]api.Ancest return nil, err } - g, err := proc.FindGoroutine(d.target, goroutineID) + g, err := proc.FindGoroutine(d.target.Selected, goroutineID) if err != nil { return nil, err } @@ -1737,7 +1814,7 @@ func (d *Debugger) Ancestors(goroutineID, numAncestors, depth int) ([]api.Ancest return nil, errors.New("no selected goroutine") } - ancestors, err := proc.Ancestors(d.target, g, numAncestors) + ancestors, err := proc.Ancestors(d.target.Selected, g, numAncestors) if err != nil { return nil, err } @@ -1789,7 +1866,7 @@ func (d *Debugger) convertStacktrace(rawlocs []proc.Stackframe, cfg *proc.LoadCo } if cfg != nil && rawlocs[i].Current.Fn != nil { var err error - scope := proc.FrameToScope(d.target, d.target.Memory(), nil, rawlocs[i:]...) + scope := proc.FrameToScope(d.target.Selected, d.target.Selected.Memory(), nil, rawlocs[i:]...) locals, err := scope.LocalVariables(*cfg) if err != nil { return nil, err @@ -1811,8 +1888,8 @@ func (d *Debugger) convertStacktrace(rawlocs []proc.Stackframe, cfg *proc.LoadCo func (d *Debugger) convertDefers(defers []*proc.Defer) []api.Defer { r := make([]api.Defer, len(defers)) for i := range defers { - ddf, ddl, ddfn := defers[i].DeferredFunc(d.target) - drf, drl, drfn := d.target.BinInfo().PCToLine(defers[i].DeferPC) + ddf, ddl, ddfn := defers[i].DeferredFunc(d.target.Selected) + drf, drl, drfn := d.target.Selected.BinInfo().PCToLine(defers[i].DeferPC) r[i] = api.Defer{ DeferredLoc: api.ConvertLocation(proc.Location{ @@ -1848,7 +1925,7 @@ func (d *Debugger) CurrentPackage() (string, error) { if _, err := d.target.Valid(); err != nil { return "", err } - loc, err := d.target.CurrentThread().Location() + loc, err := d.target.Selected.CurrentThread().Location() if err != nil { return "", err } @@ -1863,6 +1940,13 @@ func (d *Debugger) FindLocation(goid, frame, deferredCall int, locStr string, in d.targetMutex.Lock() defer d.targetMutex.Unlock() + if len(d.target.Targets()) != 1 { + //TODO(aarzilli): if there is more than one target process all must be + //searched and the addresses returned need to specify which target process + //they belong to. + panic("multiple targets not implemented") + } + if _, err := d.target.Valid(); err != nil { return nil, err } @@ -1872,7 +1956,7 @@ func (d *Debugger) FindLocation(goid, frame, deferredCall int, locStr string, in return nil, err } - return d.findLocation(goid, frame, deferredCall, locStr, loc, includeNonExecutableLines, substitutePathRules) + return d.findLocation(d.target.Selected, goid, frame, deferredCall, locStr, loc, includeNonExecutableLines, substitutePathRules) } // FindLocationSpec will find the location specified by 'locStr' and 'locSpec'. @@ -1883,22 +1967,29 @@ func (d *Debugger) FindLocationSpec(goid, frame, deferredCall int, locStr string d.targetMutex.Lock() defer d.targetMutex.Unlock() + if len(d.target.Targets()) != 1 { + //TODO(aarzilli): if there is more than one target process all must be + //searched and the addresses returned need to specify which target process + //they belong to. + panic("multiple targets not implemented") + } + if _, err := d.target.Valid(); err != nil { return nil, err } - return d.findLocation(goid, frame, deferredCall, locStr, locSpec, includeNonExecutableLines, substitutePathRules) + return d.findLocation(d.target.Selected, goid, frame, deferredCall, locStr, locSpec, includeNonExecutableLines, substitutePathRules) } -func (d *Debugger) findLocation(goid, frame, deferredCall int, locStr string, locSpec locspec.LocationSpec, includeNonExecutableLines bool, substitutePathRules [][2]string) ([]api.Location, error) { - s, _ := proc.ConvertEvalScope(d.target, goid, frame, deferredCall) +func (d *Debugger) findLocation(p *proc.Target, goid, frame, deferredCall int, locStr string, locSpec locspec.LocationSpec, includeNonExecutableLines bool, substitutePathRules [][2]string) ([]api.Location, error) { + s, _ := proc.ConvertEvalScope(p, goid, frame, deferredCall) - locs, err := locSpec.Find(d.target, d.processArgs, s, locStr, includeNonExecutableLines, substitutePathRules) + locs, err := locSpec.Find(p, d.processArgs, s, locStr, includeNonExecutableLines, substitutePathRules) for i := range locs { if locs[i].PC == 0 { continue } - file, line, fn := d.target.BinInfo().PCToLine(locs[i].PC) + file, line, fn := p.BinInfo().PCToLine(locs[i].PC) locs[i].File = file locs[i].Line = line locs[i].Function = api.ConvertFunction(fn) @@ -1917,7 +2008,7 @@ func (d *Debugger) Disassemble(goroutineID int, addr1, addr2 uint64) ([]proc.Asm } if addr2 == 0 { - fn := d.target.BinInfo().PCToFunc(addr1) + fn := d.target.Selected.BinInfo().PCToFunc(addr1) if fn == nil { return nil, fmt.Errorf("address %#x does not belong to any function", addr1) } @@ -1925,24 +2016,24 @@ func (d *Debugger) Disassemble(goroutineID int, addr1, addr2 uint64) ([]proc.Asm addr2 = fn.End } - g, err := proc.FindGoroutine(d.target, goroutineID) + g, err := proc.FindGoroutine(d.target.Selected, goroutineID) if err != nil { return nil, err } - curthread := d.target.CurrentThread() + curthread := d.target.Selected.CurrentThread() if g != nil && g.Thread != nil { curthread = g.Thread } regs, _ := curthread.Registers() - return proc.Disassemble(d.target.Memory(), regs, d.target.Breakpoints(), d.target.BinInfo(), addr1, addr2) + return proc.Disassemble(d.target.Selected.Memory(), regs, d.target.Selected.Breakpoints(), d.target.Selected.BinInfo(), addr1, addr2) } func (d *Debugger) AsmInstructionText(inst *proc.AsmInstruction, flavour proc.AssemblyFlavour) string { d.targetMutex.Lock() defer d.targetMutex.Unlock() - return inst.Text(flavour, d.target.BinInfo()) + return inst.Text(flavour, d.target.Selected.BinInfo()) } // Recorded returns true if the target is a recording. @@ -1962,7 +2053,7 @@ func (d *Debugger) FindThreadReturnValues(id int, cfg proc.LoadConfig) ([]*proc. return nil, err } - thread, found := d.target.FindThread(id) + thread, found := d.target.Selected.FindThread(id) if !found { return nil, fmt.Errorf("could not find thread %d", id) } @@ -1995,7 +2086,7 @@ func (d *Debugger) ClearCheckpoint(id int) error { func (d *Debugger) ListDynamicLibraries() []*proc.Image { d.targetMutex.Lock() defer d.targetMutex.Unlock() - return d.target.BinInfo().Images[1:] // skips the first image because it's the executable file + return d.target.Selected.BinInfo().Images[1:] // skips the first image because it's the executable file } @@ -2006,7 +2097,7 @@ func (d *Debugger) ExamineMemory(address uint64, length int) ([]byte, error) { d.targetMutex.Lock() defer d.targetMutex.Unlock() - mem := d.target.Memory() + mem := d.target.Selected.Memory() data := make([]byte, length) n, err := mem.ReadMemory(data, address) if err != nil { @@ -2038,7 +2129,7 @@ func (d *Debugger) GetVersion(out *api.GetVersionOut) error { } if !d.isRecording() && !d.IsRunning() { - out.TargetGoVersion = d.target.BinInfo().Producer() + out.TargetGoVersion = d.target.Selected.BinInfo().Producer() } out.MinSupportedVersionOfGo = fmt.Sprintf("%d.%d.0", goversion.MinSupportedVersionOfGoMajor, goversion.MinSupportedVersionOfGoMinor) @@ -2053,7 +2144,7 @@ func (d *Debugger) GetVersion(out *api.GetVersionOut) error { func (d *Debugger) ListPackagesBuildInfo(includeFiles bool) []*proc.PackageBuildInfo { d.targetMutex.Lock() defer d.targetMutex.Unlock() - return d.target.BinInfo().ListPackagesBuildInfo(includeFiles) + return d.target.Selected.BinInfo().ListPackagesBuildInfo(includeFiles) } // StopRecording stops a recording (if one is in progress) @@ -2072,7 +2163,7 @@ func (d *Debugger) StopRecording() error { func (d *Debugger) StopReason() proc.StopReason { d.targetMutex.Lock() defer d.targetMutex.Unlock() - return d.target.StopReason + return d.target.Selected.StopReason } // LockTarget acquires the target mutex. @@ -2090,7 +2181,9 @@ func (d *Debugger) DumpStart(dest string) error { d.targetMutex.Lock() // targetMutex will only be unlocked when the dump is done - if !d.target.CanDump { + //TODO(aarzilli): what do we do if the user switches to a different target after starting a dump but before it's finished? + + if !d.target.Selected.CanDump { d.targetMutex.Unlock() return ErrCoreDumpNotSupported } @@ -2120,7 +2213,7 @@ func (d *Debugger) DumpStart(dest string) error { d.dumpState.Err = nil go func() { defer d.targetMutex.Unlock() - d.target.Dump(fh, 0, &d.dumpState) + d.target.Selected.Dump(fh, 0, &d.dumpState) }() return nil @@ -2157,11 +2250,15 @@ func (d *Debugger) DumpCancel() error { } func (d *Debugger) Target() *proc.Target { + return d.target.Selected +} + +func (d *Debugger) TargetGroup() *proc.TargetGroup { return d.target } func (d *Debugger) BuildID() string { - return d.target.BinInfo().BuildID + return d.target.Selected.BinInfo().BuildID } func (d *Debugger) AttachPid() int { @@ -2169,13 +2266,13 @@ func (d *Debugger) AttachPid() int { } func (d *Debugger) GetBufferedTracepoints() []api.TracepointResult { - traces := d.target.GetBufferedTracepoints() + traces := d.target.Selected.GetBufferedTracepoints() if traces == nil { return nil } results := make([]api.TracepointResult, len(traces)) for i, trace := range traces { - f, l, fn := d.target.BinInfo().PCToLine(uint64(trace.FnAddr)) + f, l, fn := d.target.Selected.BinInfo().PCToLine(uint64(trace.FnAddr)) results[i].FunctionName = fn.Name results[i].Line = l diff --git a/service/debugger/debugger_darwin.go b/service/debugger/debugger_darwin.go index c317718f..9edeca65 100644 --- a/service/debugger/debugger_darwin.go +++ b/service/debugger/debugger_darwin.go @@ -2,14 +2,9 @@ package debugger import ( "fmt" - sys "golang.org/x/sys/unix" ) func attachErrorMessage(pid int, err error) error { //TODO: mention certificates? return fmt.Errorf("could not attach to pid %d: %s", pid, err) } - -func stopProcess(pid int) error { - return sys.Kill(pid, sys.SIGSTOP) -} diff --git a/service/debugger/debugger_freebsd.go b/service/debugger/debugger_freebsd.go index b6f36676..06c7f14d 100644 --- a/service/debugger/debugger_freebsd.go +++ b/service/debugger/debugger_freebsd.go @@ -2,13 +2,8 @@ package debugger import ( "fmt" - sys "golang.org/x/sys/unix" ) func attachErrorMessage(pid int, err error) error { return fmt.Errorf("could not attach to pid %d: %s", pid, err) } - -func stopProcess(pid int) error { - return sys.Kill(pid, sys.SIGSTOP) -} diff --git a/service/debugger/debugger_linux.go b/service/debugger/debugger_linux.go index 0f65c83e..ab19c848 100644 --- a/service/debugger/debugger_linux.go +++ b/service/debugger/debugger_linux.go @@ -5,8 +5,6 @@ import ( "io/ioutil" "os" "syscall" - - sys "golang.org/x/sys/unix" ) //lint:file-ignore ST1005 errors here can be capitalized @@ -32,7 +30,3 @@ func attachErrorMessage(pid int, err error) error { } return fallbackerr } - -func stopProcess(pid int) error { - return sys.Kill(pid, sys.SIGSTOP) -} diff --git a/service/debugger/debugger_windows.go b/service/debugger/debugger_windows.go index cd0bad96..0fa24930 100644 --- a/service/debugger/debugger_windows.go +++ b/service/debugger/debugger_windows.go @@ -14,13 +14,6 @@ func attachErrorMessage(pid int, err error) error { return fmt.Errorf("could not attach to pid %d: %s", pid, err) } -func stopProcess(pid int) error { - // We cannot gracefully stop a process on Windows, - // so just ignore this request and let `Detach` kill - // the process. - return nil -} - func verifyBinaryFormat(exePath string) error { f, err := os.Open(exePath) if err != nil {