diff --git a/proc/proc.go b/proc/proc.go index 39ef3218..65f4138c 100644 --- a/proc/proc.go +++ b/proc/proc.go @@ -9,6 +9,7 @@ import ( "go/constant" "go/token" "os" + "path/filepath" "runtime" "strconv" "strings" @@ -505,6 +506,59 @@ func (dbp *Process) StepInstruction() (err error) { return dbp.SelectedGoroutine.thread.SetCurrentBreakpoint() } +// StepOut will continue until the current goroutine exits the +// function currently being executed or a deferred function is executed +func (dbp *Process) StepOut() error { + cond := sameGoroutineCondition(dbp.SelectedGoroutine) + + topframe, err := topframe(dbp.SelectedGoroutine, dbp.CurrentThread) + if err != nil { + return err + } + + pcs := []uint64{} + + var deferpc uint64 = 0 + if filepath.Ext(topframe.Current.File) == ".go" { + if dbp.SelectedGoroutine != nil && dbp.SelectedGoroutine.DeferPC != 0 { + _, _, deferfn := dbp.goSymTable.PCToLine(dbp.SelectedGoroutine.DeferPC) + deferpc, err = dbp.FirstPCAfterPrologue(deferfn, false) + if err != nil { + return err + } + pcs = append(pcs, deferpc) + } + } + + if topframe.Ret == 0 && deferpc == 0 { + return errors.New("nothing to stepout to") + } + + if deferpc != 0 && deferpc != topframe.Current.PC { + bp, err := dbp.SetBreakpoint(deferpc, NextDeferBreakpoint, cond) + if err != nil { + if _, ok := err.(BreakpointExistsError); !ok { + dbp.ClearInternalBreakpoints() + return err + } + } + if bp != nil { + // For StepOut we do not want to step into the deferred function + // when it's called by runtime.deferreturn so we do not populate + // DeferReturns. + bp.DeferReturns = []uint64{} + } + } + + if topframe.Ret != 0 { + if err := dbp.setInternalBreakpoints(topframe.Current.PC, []uint64{topframe.Ret}, NextBreakpoint, cond); err != nil { + return err + } + } + + return dbp.Continue() +} + // SwitchThread changes from current thread to the thread specified by `tid`. func (dbp *Process) SwitchThread(tid int) error { if dbp.exited { diff --git a/proc/proc_test.go b/proc/proc_test.go index 56355f0d..80f8ad73 100644 --- a/proc/proc_test.go +++ b/proc/proc_test.go @@ -141,6 +141,18 @@ func setFunctionBreakpoint(p *Process, fname string) (*Breakpoint, error) { return p.SetBreakpoint(addr, UserBreakpoint, nil) } +func setFileBreakpoint(p *Process, t *testing.T, fixture protest.Fixture, lineno int) *Breakpoint { + addr, err := p.FindFileLocation(fixture.Source, lineno) + if err != nil { + t.Fatalf("FindFileLocation: %v", err) + } + bp, err := p.SetBreakpoint(addr, UserBreakpoint, nil) + if err != nil { + t.Fatalf("SetBreakpoint: %v", err) + } + return bp +} + func TestHalt(t *testing.T) { stopChan := make(chan interface{}) withTestProcess("loopprog", t, func(p *Process, fixture protest.Fixture) { @@ -2044,6 +2056,27 @@ func TestIssue561(t *testing.T) { }) } +func TestStepOut(t *testing.T) { + withTestProcess("testnextprog", t, func(p *Process, fixture protest.Fixture) { + bp, err := setFunctionBreakpoint(p, "main.helloworld") + assertNoError(err, t, "SetBreakpoint()") + assertNoError(p.Continue(), t, "Continue()") + p.ClearBreakpoint(bp.Addr) + + f, lno := currentLineNumber(p, t) + if lno != 13 { + t.Fatalf("wrong line number %s:%d, expected %d", f, lno, 13) + } + + assertNoError(p.StepOut(), t, "StepOut()") + + f, lno = currentLineNumber(p, t) + if lno != 35 { + t.Fatalf("wrong line number %s:%d, expected %d", f, lno, 34) + } + }) +} + func TestStepConcurrentDirect(t *testing.T) { withTestProcess("teststepconcurrent", t, func(p *Process, fixture protest.Fixture) { pc, err := p.FindFileLocation(fixture.Source, 37) @@ -2161,6 +2194,47 @@ func TestStepConcurrentPtr(t *testing.T) { }) } +func TestStepOutDefer(t *testing.T) { + withTestProcess("testnextdefer", t, func(p *Process, fixture protest.Fixture) { + pc, err := p.FindFileLocation(fixture.Source, 9) + assertNoError(err, t, "FindFileLocation()") + bp, err := p.SetBreakpoint(pc, UserBreakpoint, nil) + assertNoError(err, t, "SetBreakpoint()") + assertNoError(p.Continue(), t, "Continue()") + p.ClearBreakpoint(bp.Addr) + + f, lno := currentLineNumber(p, t) + if lno != 9 { + t.Fatalf("worng line number %s:%d, expected %d", f, lno, 5) + } + + assertNoError(p.StepOut(), t, "StepOut()") + + f, l, _ := p.goSymTable.PCToLine(currentPC(p, t)) + if f == fixture.Source || l == 6 { + t.Fatalf("wrong location %s:%d, expected to end somewhere in runtime", f, l) + } + }) +} + +func TestStepOutDeferReturnAndDirectCall(t *testing.T) { + // StepOut should not step into a deferred function if it is called + // directly, only if it is called through a panic. + // Here we test the case where the function is called by a deferreturn + withTestProcess("defercall", t, func(p *Process, fixture protest.Fixture) { + bp := setFileBreakpoint(p, t, fixture, 11) + assertNoError(p.Continue(), t, "Continue()") + p.ClearBreakpoint(bp.Addr) + + assertNoError(p.StepOut(), t, "StepOut()") + + f, ln := currentLineNumber(p, t) + if ln != 28 { + t.Fatalf("wrong line number, expected %d got %s:%d", 28, f, ln) + } + }) +} + func TestStepOnCallPtrInstr(t *testing.T) { withTestProcess("teststepprog", t, func(p *Process, fixture protest.Fixture) { pc, err := p.FindFileLocation(fixture.Source, 10) @@ -2214,3 +2288,21 @@ func TestIssue594(t *testing.T) { } }) } + +func TestStepOutPanicAndDirectCall(t *testing.T) { + // StepOut should not step into a deferred function if it is called + // directly, only if it is called through a panic. + // Here we test the case where the function is called by a panic + withTestProcess("defercall", t, func(p *Process, fixture protest.Fixture) { + bp := setFileBreakpoint(p, t, fixture, 17) + assertNoError(p.Continue(), t, "Continue()") + p.ClearBreakpoint(bp.Addr) + + assertNoError(p.StepOut(), t, "StepOut()") + + f, ln := currentLineNumber(p, t) + if ln != 5 { + t.Fatalf("wrong line number, expected %d got %s:%d", 5, f, ln) + } + }) +} diff --git a/service/api/types.go b/service/api/types.go index cece872b..e2abd0b5 100644 --- a/service/api/types.go +++ b/service/api/types.go @@ -237,6 +237,8 @@ const ( Continue = "continue" // Step continues to next source line, entering function calls. Step = "step" + // StepOut continues to the return address of the current function + StepOut = "stepOut" // SingleStep continues for exactly 1 cpu instruction. StepInstruction = "stepInstruction" // Next continues to the next source line, not entering function calls. diff --git a/service/client.go b/service/client.go index 95f9bbea..d992e423 100644 --- a/service/client.go +++ b/service/client.go @@ -25,6 +25,9 @@ type Client interface { Next() (*api.DebuggerState, error) // Step continues to the next source line, entering function calls. Step() (*api.DebuggerState, error) + // StepOut continues to the return address of the current function + StepOut() (*api.DebuggerState, error) + // SingleStep will step a single cpu instruction. StepInstruction() (*api.DebuggerState, error) // SwitchThread switches the current thread context. diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go index 56484b64..ec9770a7 100644 --- a/service/debugger/debugger.go +++ b/service/debugger/debugger.go @@ -418,6 +418,9 @@ func (d *Debugger) Command(command *api.DebuggerCommand) (*api.DebuggerState, er case api.StepInstruction: log.Print("single stepping") err = d.process.StepInstruction() + case api.StepOut: + log.Print("step out") + err = d.process.StepOut() case api.SwitchThread: log.Printf("switching to thread %d", command.ThreadID) err = d.process.SwitchThread(command.ThreadID) diff --git a/service/rpc2/client.go b/service/rpc2/client.go index 66aeb308..c6a6f2fe 100644 --- a/service/rpc2/client.go +++ b/service/rpc2/client.go @@ -103,6 +103,12 @@ func (c *RPCClient) Step() (*api.DebuggerState, error) { return &out.State, err } +func (c *RPCClient) StepOut() (*api.DebuggerState, error) { + var out CommandOut + err := c.call("Command", &api.DebuggerCommand{ Name: api.StepOut}, &out) + return &out.State, err +} + func (c *RPCClient) StepInstruction() (*api.DebuggerState, error) { var out CommandOut err := c.call("Command", api.DebuggerCommand{Name: api.StepInstruction}, &out) diff --git a/service/test/integration2_test.go b/service/test/integration2_test.go index 2f2c68c6..bd38dde5 100644 --- a/service/test/integration2_test.go +++ b/service/test/integration2_test.go @@ -199,6 +199,23 @@ func TestClientServer_step(t *testing.T) { }) } +func TestClientServer_stepout(t *testing.T) { + withTestClient2("testnextprog", t, func(c service.Client) { + _, err := c.CreateBreakpoint(&api.Breakpoint{FunctionName: "main.helloworld", Line: -1}) + assertNoError(err, t, "CreateBreakpoint()") + stateBefore := <-c.Continue() + assertNoError(stateBefore.Err, t, "Continue()") + if stateBefore.CurrentThread.Line != 13 { + t.Fatalf("wrong line number %s:%d, expected %d", stateBefore.CurrentThread.File, stateBefore.CurrentThread.Line, 13) + } + stateAfter, err := c.StepOut() + assertNoError(err, t, "StepOut()") + if stateAfter.CurrentThread.Line != 35 { + t.Fatalf("wrong line number %s:%d, expected %d", stateAfter.CurrentThread.File, stateAfter.CurrentThread.Line, 13) + } + }) +} + func testnext2(testcases []nextTest, initialLocation string, t *testing.T) { withTestClient2("testnextprog", t, func(c service.Client) { bp, err := c.CreateBreakpoint(&api.Breakpoint{FunctionName: initialLocation, Line: -1}) @@ -278,7 +295,7 @@ func TestNextGeneral(t *testing.T) { } } - testnext(testcases, "main.testnext", t) + testnext2(testcases, "main.testnext", t) } func TestNextFunctionReturn(t *testing.T) { @@ -287,7 +304,7 @@ func TestNextFunctionReturn(t *testing.T) { {14, 15}, {15, 35}, } - testnext(testcases, "main.helloworld", t) + testnext2(testcases, "main.helloworld", t) } func TestClientServer_breakpointInMainThread(t *testing.T) { diff --git a/terminal/command.go b/terminal/command.go index 9aa78796..9e6cf8ed 100644 --- a/terminal/command.go +++ b/terminal/command.go @@ -102,6 +102,7 @@ See also: "help on", "help cond" and "help clear"`}, {aliases: []string{"step", "s"}, allowedPrefixes: scopePrefix, cmdFn: step, helpMsg: "Single step through program."}, {aliases: []string{"step-instruction", "si"}, allowedPrefixes: scopePrefix, cmdFn: stepInstruction, helpMsg: "Single step a single cpu instruction."}, {aliases: []string{"next", "n"}, allowedPrefixes: scopePrefix, cmdFn: next, helpMsg: "Step over to next source line."}, + {aliases: []string{"stepout"}, allowedPrefixes: scopePrefix, cmdFn: stepout, helpMsg: "Step out of the current function."}, {aliases: []string{"threads"}, cmdFn: threads, helpMsg: "Print out info for every traced thread."}, {aliases: []string{"thread", "tr"}, cmdFn: thread, helpMsg: `Switch to the specified thread. @@ -658,6 +659,18 @@ func next(t *Term, ctx callContext, args string) error { return continueUntilCompleteNext(t, state, "next") } +func stepout(t *Term, ctx callContext, args string) error { + if err := scopePrefixSwitch(t, ctx); err != nil { + return err + } + state, err := t.client.StepOut() + if err != nil { + return err + } + printcontext(t, state) + return continueUntilCompleteNext(t, state, "stepout") +} + func clear(t *Term, ctx callContext, args string) error { if len(args) == 0 { return fmt.Errorf("not enough arguments")