diff --git a/_fixtures/teststep.go b/_fixtures/teststep.go new file mode 100644 index 00000000..b698ccf1 --- /dev/null +++ b/_fixtures/teststep.go @@ -0,0 +1,15 @@ +package main + +import ( + "fmt" + "runtime" +) + +func callme() { + fmt.Println("hi") +} + +func main() { + runtime.Breakpoint() + callme() +} diff --git a/proc/proc.go b/proc/proc.go index c0c4457a..71cbb558 100644 --- a/proc/proc.go +++ b/proc/proc.go @@ -220,6 +220,11 @@ func (dbp *Process) FunctionEntryToFirstLine(entry uint64) (uint64, error) { return entry, nil } +// CurrentLocation returns the location of the current thread. +func (dbp *Process) CurrentLocation() (*Location, error) { + return dbp.CurrentThread.Location() +} + // RequestManualStop sets the `halt` flag and // sends SIGSTOP to all threads. func (dbp *Process) RequestManualStop() error { @@ -381,7 +386,7 @@ func (dbp *Process) Continue() error { // runtime.Breakpoint or manual stop if dbp.CurrentThread.onRuntimeBreakpoint() { for i := 0; i < 2; i++ { - if err = dbp.CurrentThread.Step(); err != nil { + if err = dbp.CurrentThread.StepInstruction(); err != nil { return err } } @@ -446,18 +451,48 @@ func (dbp *Process) pickCurrentThread(trapthread *Thread) error { return dbp.SwitchThread(trapthread.ID) } -// Step will continue the current thread for exactly +// Step will continue until another source line is reached. +// Will step into functions. +func (dbp *Process) Step() (err error) { + fn := func() error { + var nloc *Location + th := dbp.CurrentThread + loc, err := th.Location() + if err != nil { + return err + } + for { + err = dbp.CurrentThread.singleStep() + if err != nil { + return err + } + nloc, err = th.Location() + if err != nil { + return err + } + if nloc.File != loc.File { + return nil + } + if nloc.File == loc.File && nloc.Line != loc.Line { + return nil + } + } + } + return dbp.run(fn) +} + +// StepInstruction will continue the current thread for exactly // one instruction. This method affects only the thread // asssociated with the selected goroutine. All other // threads will remain stopped. -func (dbp *Process) Step() (err error) { +func (dbp *Process) StepInstruction() (err error) { if dbp.SelectedGoroutine == nil { return errors.New("cannot single step: no selected goroutine") } if dbp.SelectedGoroutine.thread == nil { return fmt.Errorf("cannot single step: no thread associated with goroutine %d", dbp.SelectedGoroutine.ID) } - return dbp.run(dbp.SelectedGoroutine.thread.Step) + return dbp.run(dbp.SelectedGoroutine.thread.StepInstruction) } // SwitchThread changes from current thread to the thread specified by `tid`. diff --git a/proc/proc_darwin.go b/proc/proc_darwin.go index 6f71729d..86a13721 100644 --- a/proc/proc_darwin.go +++ b/proc/proc_darwin.go @@ -396,7 +396,7 @@ func (dbp *Process) resume() error { // all threads stopped over a breakpoint are made to step over it for _, thread := range dbp.Threads { if thread.CurrentBreakpoint != nil { - if err := thread.Step(); err != nil { + if err := thread.StepInstruction(); err != nil { return err } thread.CurrentBreakpoint = nil diff --git a/proc/proc_linux.go b/proc/proc_linux.go index 1af427e3..8aad8fa9 100644 --- a/proc/proc_linux.go +++ b/proc/proc_linux.go @@ -417,7 +417,7 @@ func (dbp *Process) resume() error { // all threads stopped over a breakpoint are made to step over it for _, thread := range dbp.Threads { if thread.CurrentBreakpoint != nil { - if err := thread.Step(); err != nil { + if err := thread.StepInstruction(); err != nil { return err } thread.CurrentBreakpoint = nil diff --git a/proc/proc_test.go b/proc/proc_test.go index cb0088e3..d2235629 100644 --- a/proc/proc_test.go +++ b/proc/proc_test.go @@ -177,7 +177,7 @@ func TestStep(t *testing.T) { regs := getRegisters(p, t) rip := regs.PC() - err = p.Step() + err = p.CurrentThread.StepInstruction() assertNoError(err, t, "Step()") regs = getRegisters(p, t) @@ -1438,3 +1438,27 @@ func TestIssue356(t *testing.T) { } }) } + +func TestStepIntoFunction(t *testing.T) { + withTestProcess("teststep", t, func(p *Process, fixture protest.Fixture) { + // Continue until breakpoint + assertNoError(p.Continue(), t, "Continue() returned an error") + // Step into function + assertNoError(p.Step(), t, "Step() returned an error") + // We should now be inside the function. + loc, err := p.CurrentLocation() + if err != nil { + t.Fatal(err) + } + if loc.Fn.Name != "main.callme" { + t.Fatalf("expected to be within the 'callme' function, was in %s instead", loc.Fn.Name) + } + if !strings.Contains(loc.File, "teststep") { + t.Fatalf("debugger stopped at incorrect location: %s:%d", loc.File, loc.Line) + } + // TODO(derekparker) consider skipping function prologue when stepping into func. + if loc.Line != 8 { + t.Fatalf("debugger stopped at incorrect line: %d", loc.Line) + } + }) +} diff --git a/proc/proc_windows.go b/proc/proc_windows.go index 10f99d4b..cafcd470 100644 --- a/proc/proc_windows.go +++ b/proc/proc_windows.go @@ -461,7 +461,7 @@ func (dbp *Process) resume() error { thread := dbp.Threads[dbp.os.breakThread] // This relies on the same assumptions as dbp.setCurrentBreakpoints if thread.CurrentBreakpoint != nil { - if err := thread.Step(); err != nil { + if err := thread.StepInstruction(); err != nil { return err } thread.CurrentBreakpoint = nil diff --git a/proc/threads.go b/proc/threads.go index 164f9a00..797633f4 100644 --- a/proc/threads.go +++ b/proc/threads.go @@ -4,11 +4,12 @@ import ( "debug/gosym" "encoding/binary" "fmt" - "golang.org/x/debug/dwarf" "path/filepath" "reflect" "runtime" + "golang.org/x/debug/dwarf" + "github.com/derekparker/delve/dwarf/frame" ) @@ -53,20 +54,20 @@ func (thread *Thread) Continue() error { // Check whether we are stopped at a breakpoint, and // if so, single step over it before continuing. if _, ok := thread.dbp.FindBreakpoint(pc); ok { - if err := thread.Step(); err != nil { + if err := thread.StepInstruction(); err != nil { return err } } return thread.resume() } -// Step a single instruction. +// StepInstruction steps a single instruction. // // Executes exactly one instruction and then returns. // If the thread is at a breakpoint, we first clear it, // execute the instruction, and then replace the breakpoint. // Otherwise we simply execute the next instruction. -func (thread *Thread) Step() (err error) { +func (thread *Thread) StepInstruction() (err error) { thread.running = true thread.singleStepping = true defer func() { diff --git a/service/api/types.go b/service/api/types.go index 62cab6e1..c778d4f3 100644 --- a/service/api/types.go +++ b/service/api/types.go @@ -184,8 +184,10 @@ type EvalScope struct { const ( // Continue resumes process execution. Continue = "continue" - // Step continues for a single instruction, entering function calls. + // Step continues to next source line, entering function calls. Step = "step" + // SingleStep continues for exactly 1 cpu instruction. + StepInstruction = "stepInstruction" // Next continues to the next source line, not entering function calls. Next = "next" // SwitchThread switches the debugger's current thread context. diff --git a/service/client.go b/service/client.go index 3d97e4f3..feee3f3a 100644 --- a/service/client.go +++ b/service/client.go @@ -25,6 +25,8 @@ type Client interface { Next() (*api.DebuggerState, error) // Step continues to the next source line, entering function calls. Step() (*api.DebuggerState, error) + // SingleStep will step a single cpu instruction. + StepInstruction() (*api.DebuggerState, error) // SwitchThread switches the current thread context. SwitchThread(threadID int) (*api.DebuggerState, error) // SwitchGoroutine switches the current goroutine (and the current thread as well) diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go index 8b896fea..3b8d4b19 100644 --- a/service/debugger/debugger.go +++ b/service/debugger/debugger.go @@ -261,6 +261,9 @@ func (d *Debugger) Command(command *api.DebuggerCommand) (*api.DebuggerState, er case api.Step: log.Print("stepping") err = d.process.Step() + case api.StepInstruction: + log.Print("single stepping") + err = d.process.StepInstruction() case api.SwitchThread: log.Printf("switching to thread %d", command.ThreadID) err = d.process.SwitchThread(command.ThreadID) diff --git a/service/rpc/client.go b/service/rpc/client.go index 96b759f0..9e3759f9 100644 --- a/service/rpc/client.go +++ b/service/rpc/client.go @@ -101,6 +101,12 @@ func (c *RPCClient) Step() (*api.DebuggerState, error) { return state, err } +func (c *RPCClient) StepInstruction() (*api.DebuggerState, error) { + state := new(api.DebuggerState) + err := c.call("Command", &api.DebuggerCommand{Name: api.StepInstruction}, state) + return state, err +} + func (c *RPCClient) SwitchThread(threadID int) (*api.DebuggerState, error) { state := new(api.DebuggerState) cmd := &api.DebuggerCommand{ diff --git a/terminal/command.go b/terminal/command.go index 093a392f..4545cc1f 100644 --- a/terminal/command.go +++ b/terminal/command.go @@ -60,7 +60,8 @@ func DebugCommands(client service.Client) *Commands { {aliases: []string{"trace", "t"}, cmdFn: tracepoint, helpMsg: "Set tracepoint, takes the same arguments as break."}, {aliases: []string{"restart", "r"}, cmdFn: restart, helpMsg: "Restart process."}, {aliases: []string{"continue", "c"}, cmdFn: cont, helpMsg: "Run until breakpoint or program termination."}, - {aliases: []string{"step", "si"}, cmdFn: step, helpMsg: "Single step through program."}, + {aliases: []string{"step", "s"}, cmdFn: step, helpMsg: "Single step through program."}, + {aliases: []string{"step-instruction", "si"}, cmdFn: stepInstruction, helpMsg: "Single step a single cpu instruction."}, {aliases: []string{"next", "n"}, cmdFn: next, helpMsg: "Step over to next source line."}, {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."}, @@ -471,6 +472,15 @@ func step(t *Term, args string) error { return nil } +func stepInstruction(t *Term, args string) error { + state, err := t.client.StepInstruction() + if err != nil { + return err + } + printcontext(t, state) + return nil +} + func next(t *Term, args string) error { state, err := t.client.Next() if err != nil {