proc,terminal: Implement reverse step, next and stepout (#1785)

* proc: move defer breakpoint code into a function

Moves the code that sets a breakpoint on the first deferred function,
used by both next and StepOut, to its function.

* proc: implement reverse step/next/stepout

When the direction of execution is reversed (on a recording) Step, Next and
StepOut will behave similarly to their forward version. However there are
some subtle interactions between their behavior, prologue skipping, deferred
calls and normal calls. Specifically:

- when stepping backwards we need to set a breakpoint on the first
  instruction after each CALL instruction, once this breakpoint is reached we
  need to execute a single StepInstruction operation to reverse step into the
  CALL.
- to insure that the prologue is skipped reverse next needs to check if it
  is on the first instruction after the prologue, and if it is behave like
  reverse stepout.
- there is no reason to set breakpoints on deferred calls when reverse
  nexting or reverse stepping out, they will never be hit.
- reverse step out should generally place its breakpoint on the CALL
  instruction that created the current stack frame (which will be the CALL
  instruction immediately preceding the instruction at the return address).
- reverse step out needs to treat panic calls and deferreturn calls
  specially.

* service,terminal: implement reverse step, next, stepout
This commit is contained in:
Alessandro Arzilli
2020-03-11 23:40:41 +01:00
committed by GitHub
parent e90a5b48ca
commit 1a9e38aa0c
17 changed files with 589 additions and 140 deletions

View File

@ -3,3 +3,4 @@
- Delve does not currently support 32bit systems. This will usually manifest as a compiler error in `proc/disasm.go`. See [Issue #20](https://github.com/go-delve/delve/issues/20). - Delve does not currently support 32bit systems. This will usually manifest as a compiler error in `proc/disasm.go`. See [Issue #20](https://github.com/go-delve/delve/issues/20).
- When Delve is compiled with versions of go prior to 1.7.0 it is not possible to set a breakpoint on a function in a remote package using the `Receiver.MethodName` syntax. See [Issue #528](https://github.com/go-delve/delve/issues/528). - When Delve is compiled with versions of go prior to 1.7.0 it is not possible to set a breakpoint on a function in a remote package using the `Receiver.MethodName` syntax. See [Issue #528](https://github.com/go-delve/delve/issues/528).
- When running Delve on binaries compiled with a version of go prior to 1.9.0 `locals` will print all local variables, including ones that are out of scope, the shadowed flag will be applied arbitrarily. If there are multiple variables defined with the same name in the current function `print` will not be able to select the correct one for the current line. - When running Delve on binaries compiled with a version of go prior to 1.9.0 `locals` will print all local variables, including ones that are out of scope, the shadowed flag will be applied arbitrarily. If there are multiple variables defined with the same name in the current function `print` will not be able to select the correct one for the current line.
- `reverse step` will not reverse step into functions called by deferred calls.

View File

@ -14,6 +14,7 @@ Command | Description
[continue](#continue) | Run until breakpoint or program termination. [continue](#continue) | Run until breakpoint or program termination.
[next](#next) | Step over to next source line. [next](#next) | Step over to next source line.
[restart](#restart) | Restart process from a checkpoint or event. [restart](#restart) | Restart process from a checkpoint or event.
[rev](#rev) | Reverses the execution of the target program for the command specified.
[rewind](#rewind) | Run backwards until breakpoint or program termination. [rewind](#rewind) | Run backwards until breakpoint or program termination.
[step](#step) | Single step through program. [step](#step) | Single step through program.
[step-instruction](#step-instruction) | Single step a single cpu instruction. [step-instruction](#step-instruction) | Single step a single cpu instruction.
@ -83,7 +84,6 @@ Command | Description
[help](#help) | Prints the help message. [help](#help) | Prints the help message.
[libraries](#libraries) | List loaded dynamic libraries [libraries](#libraries) | List loaded dynamic libraries
[list](#list) | Show source code. [list](#list) | Show source code.
[rev](#rev) | Reverses the execution of the target program for the command specified.
[source](#source) | Executes a file containing a list of delve commands [source](#source) | Executes a file containing a list of delve commands
[sources](#sources) | Print list of source files. [sources](#sources) | Print list of source files.
[types](#types) | Print list of types [types](#types) | Print list of types

View File

@ -445,7 +445,7 @@ func TestTypecheckRPC(t *testing.T) {
continue continue
} }
if fndecl.Name.Name == "Continue" || fndecl.Name.Name == "Rewind" { if fndecl.Name.Name == "Continue" || fndecl.Name.Name == "Rewind" || fndecl.Name.Name == "DirectionCongruentContinue" {
// using continueDir // using continueDir
continue continue
} }

View File

@ -124,19 +124,13 @@ func (bp *Breakpoint) CheckCondition(thread Thread) BreakpointState {
} }
nextDeferOk := true nextDeferOk := true
if bp.Kind&NextDeferBreakpoint != 0 { if bp.Kind&NextDeferBreakpoint != 0 {
var err error
frames, err := ThreadStacktrace(thread, 2) frames, err := ThreadStacktrace(thread, 2)
if err == nil { if err == nil {
ispanic := len(frames) >= 3 && frames[2].Current.Fn != nil && frames[2].Current.Fn.Name == "runtime.gopanic" nextDeferOk = isPanicCall(frames)
isdeferreturn := false if !nextDeferOk {
if len(frames) >= 1 { nextDeferOk, _ = isDeferReturnCall(frames, bp.DeferReturns)
for _, pc := range bp.DeferReturns {
if frames[0].Ret == pc {
isdeferreturn = true
break
}
}
} }
nextDeferOk = ispanic || isdeferreturn
} }
} }
if bp.IsInternal() { if bp.IsInternal() {
@ -155,6 +149,21 @@ func (bp *Breakpoint) CheckCondition(thread Thread) BreakpointState {
return bpstate return bpstate
} }
func isPanicCall(frames []Stackframe) bool {
return len(frames) >= 3 && frames[2].Current.Fn != nil && frames[2].Current.Fn.Name == "runtime.gopanic"
}
func isDeferReturnCall(frames []Stackframe, deferReturns []uint64) (bool, uint64) {
if len(frames) >= 1 {
for _, pc := range deferReturns {
if frames[0].Ret == pc {
return true, pc
}
}
}
return false, 0
}
// IsInternal returns true if bp is an internal breakpoint. // IsInternal returns true if bp is an internal breakpoint.
// User-set breakpoints can overlap with internal breakpoints, in that case // User-set breakpoints can overlap with internal breakpoints, in that case
// both IsUser and IsInternal will be true. // both IsUser and IsInternal will be true.

View File

@ -243,8 +243,11 @@ func (p *Process) Recorded() (bool, string) { return true, "" }
// Restart will only return an error for core files, as they are not executing. // Restart will only return an error for core files, as they are not executing.
func (p *Process) Restart(string) error { return ErrContinueCore } func (p *Process) Restart(string) error { return ErrContinueCore }
// Direction will only return an error as you cannot continue a core process. // ChangeDirection will only return an error as you cannot continue a core process.
func (p *Process) Direction(proc.Direction) error { return ErrContinueCore } func (p *Process) ChangeDirection(proc.Direction) error { return ErrContinueCore }
// GetDirection will always return forward.
func (p *Process) GetDirection() proc.Direction { return proc.Forward }
// When does not apply to core files, it is to support the Mozilla 'rr' backend. // When does not apply to core files, it is to support the Mozilla 'rr' backend.
func (p *Process) When() (string, error) { return "", nil } func (p *Process) When() (string, error) { return "", nil }

View File

@ -658,6 +658,7 @@ func (p *Process) ContinueOnce() (proc.Thread, proc.StopReason, error) {
var threadID string var threadID string
var trapthread *Thread var trapthread *Thread
var tu = threadUpdater{p: p} var tu = threadUpdater{p: p}
var atstart bool
continueLoop: continueLoop:
for { for {
var err error var err error
@ -683,7 +684,7 @@ continueLoop:
} }
var shouldStop bool var shouldStop bool
trapthread, shouldStop = p.handleThreadSignals(trapthread) trapthread, atstart, shouldStop = p.handleThreadSignals(trapthread)
if shouldStop { if shouldStop {
break continueLoop break continueLoop
} }
@ -691,18 +692,23 @@ continueLoop:
p.clearThreadRegisters() p.clearThreadRegisters()
stopReason := proc.StopUnknown
if atstart {
stopReason = proc.StopLaunched
}
if p.BinInfo().GOOS == "linux" { if p.BinInfo().GOOS == "linux" {
if err := linutil.ElfUpdateSharedObjects(p); err != nil { if err := linutil.ElfUpdateSharedObjects(p); err != nil {
return nil, proc.StopUnknown, err return nil, stopReason, err
} }
} }
if err := p.setCurrentBreakpoints(); err != nil { if err := p.setCurrentBreakpoints(); err != nil {
return nil, proc.StopUnknown, err return nil, stopReason, err
} }
if trapthread == nil { if trapthread == nil {
return nil, proc.StopUnknown, fmt.Errorf("could not find thread %s", threadID) return nil, stopReason, fmt.Errorf("could not find thread %s", threadID)
} }
var err error var err error
@ -724,7 +730,7 @@ continueLoop:
// the signals that are reported here can not be propagated back to the target process. // the signals that are reported here can not be propagated back to the target process.
trapthread.sig = 0 trapthread.sig = 0
} }
return trapthread, proc.StopUnknown, err return trapthread, stopReason, err
} }
func (p *Process) findThreadByStrID(threadID string) *Thread { func (p *Process) findThreadByStrID(threadID string) *Thread {
@ -741,7 +747,7 @@ func (p *Process) findThreadByStrID(threadID string) *Thread {
// and returns true if we should stop execution in response to one of the // and returns true if we should stop execution in response to one of the
// signals and return control to the user. // signals and return control to the user.
// Adjusts trapthread to a thread that we actually want to stop at. // Adjusts trapthread to a thread that we actually want to stop at.
func (p *Process) handleThreadSignals(trapthread *Thread) (trapthreadOut *Thread, shouldStop bool) { func (p *Process) handleThreadSignals(trapthread *Thread) (trapthreadOut *Thread, atstart bool, shouldStop bool) {
var trapthreadCandidate *Thread var trapthreadCandidate *Thread
for _, th := range p.threads { for _, th := range p.threads {
@ -777,8 +783,9 @@ func (p *Process) handleThreadSignals(trapthread *Thread) (trapthreadOut *Thread
// Signal 0 is returned by rr when it reaches the start of the process // Signal 0 is returned by rr when it reaches the start of the process
// in backward continue mode. // in backward continue mode.
case 0: case 0:
if p.conn.direction == proc.Backward { if p.conn.direction == proc.Backward && th == trapthread {
isStopSignal = true isStopSignal = true
atstart = true
} }
default: default:
@ -809,7 +816,7 @@ func (p *Process) handleThreadSignals(trapthread *Thread) (trapthreadOut *Thread
shouldStop = true shouldStop = true
} }
return trapthread, shouldStop return trapthread, atstart, shouldStop
} }
// RequestManualStop will attempt to stop the process // RequestManualStop will attempt to stop the process
@ -1014,10 +1021,13 @@ func (p *Process) ClearCheckpoint(id int) error {
return nil return nil
} }
// Direction sets whether to run the program forwards or in reverse execution. // ChangeDirection sets whether to run the program forwards or in reverse execution.
func (p *Process) Direction(dir proc.Direction) error { func (p *Process) ChangeDirection(dir proc.Direction) error {
if p.tracedir == "" { if p.tracedir == "" {
return proc.ErrNotRecorded if dir != proc.Forward {
return proc.ErrNotRecorded
}
return nil
} }
if p.conn.conn == nil { if p.conn.conn == nil {
return proc.ErrProcessExited{Pid: p.conn.pid} return proc.ErrProcessExited{Pid: p.conn.pid}
@ -1032,6 +1042,11 @@ func (p *Process) Direction(dir proc.Direction) error {
return nil return nil
} }
// GetDirection returns the current direction of execution.
func (p *Process) GetDirection() proc.Direction {
return p.conn.direction
}
// Breakpoints returns the list of breakpoints currently set. // Breakpoints returns the list of breakpoints currently set.
func (p *Process) Breakpoints() *proc.BreakpointMap { func (p *Process) Breakpoints() *proc.BreakpointMap {
return &p.breakpoints return &p.breakpoints

View File

@ -146,7 +146,7 @@ func TestReverseBreakpointCounts(t *testing.T) {
} }
p.ClearBreakpoint(endbp.Addr) p.ClearBreakpoint(endbp.Addr)
assertNoError(p.Direction(proc.Backward), t, "Switching to backward direction") assertNoError(p.ChangeDirection(proc.Backward), t, "Switching to backward direction")
bp := setFileBreakpoint(p, t, fixture, 12) bp := setFileBreakpoint(p, t, fixture, 12)
startbp := setFileBreakpoint(p, t, fixture, 20) startbp := setFileBreakpoint(p, t, fixture, 20)
@ -285,7 +285,7 @@ func TestIssue1376(t *testing.T) {
assertNoError(proc.Continue(p), t, "Continue (forward)") assertNoError(proc.Continue(p), t, "Continue (forward)")
_, err := p.ClearBreakpoint(bp.Addr) _, err := p.ClearBreakpoint(bp.Addr)
assertNoError(err, t, "ClearBreakpoint") assertNoError(err, t, "ClearBreakpoint")
assertNoError(p.Direction(proc.Backward), t, "Switching to backward direction") assertNoError(p.ChangeDirection(proc.Backward), t, "Switching to backward direction")
assertNoError(proc.Continue(p), t, "Continue (backward)") assertNoError(proc.Continue(p), t, "Continue (backward)")
}) })
} }

View File

@ -40,7 +40,9 @@ type RecordingManipulation interface {
// to the trace directory. // to the trace directory.
Recorded() (recorded bool, tracedir string) Recorded() (recorded bool, tracedir string)
// Direction changes execution direction. // Direction changes execution direction.
Direction(Direction) error ChangeDirection(Direction) error
// GetDirection returns the current direction of execution.
GetDirection() Direction
// When returns current recording position. // When returns current recording position.
When() (string, error) When() (string, error)
// Checkpoint sets a checkpoint at the current position. // Checkpoint sets a checkpoint at the current position.

View File

@ -70,9 +70,17 @@ func (dbp *Process) Recorded() (bool, string) { return false, "" }
// recorded traces. // recorded traces.
func (dbp *Process) Restart(string) error { return proc.ErrNotRecorded } func (dbp *Process) Restart(string) error { return proc.ErrNotRecorded }
// Direction will always return an error in the native proc backend, only for // ChangeDirection will always return an error in the native proc backend, only for
// recorded traces. // recorded traces.
func (dbp *Process) Direction(proc.Direction) error { return proc.ErrNotRecorded } func (dbp *Process) ChangeDirection(dir proc.Direction) error {
if dir != proc.Forward {
return proc.ErrNotRecorded
}
return nil
}
// GetDirection will always return Forward.
func (p *Process) GetDirection() proc.Direction { return proc.Forward }
// When will always return an empty string and nil, not supported on native proc backend. // When will always return an empty string and nil, not supported on native proc backend.
func (dbp *Process) When() (string, error) { return "", nil } func (dbp *Process) When() (string, error) { return "", nil }

View File

@ -8,7 +8,6 @@ import (
"go/constant" "go/constant"
"go/token" "go/token"
"os" "os"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
@ -164,6 +163,9 @@ func Continue(dbp *Target) error {
if err != nil { if err != nil {
return err return err
} }
if dbp.StopReason == StopLaunched {
dbp.ClearInternalBreakpoints()
}
threads := dbp.ThreadList() threads := dbp.ThreadList()
@ -229,20 +231,19 @@ func Continue(dbp *Target) error {
if err := conditionErrors(threads); err != nil { if err := conditionErrors(threads); err != nil {
return err return err
} }
regs, err := curthread.Registers(false) if dbp.GetDirection() == Forward {
if err != nil { text, err := disassembleCurrentInstruction(dbp, curthread)
return err // here we either set a breakpoint into the destination of the CALL
} // instruction or we determined that the called function is hidden,
pc := regs.PC() // either way we need to resume execution
text, err := disassemble(curthread, regs, dbp.Breakpoints(), dbp.BinInfo(), pc, pc+uint64(dbp.BinInfo().Arch.MaxInstructionLength()), true) if err = setStepIntoBreakpoint(dbp, text, SameGoroutineCondition(dbp.SelectedGoroutine())); err != nil {
if err != nil { return err
return err }
} } else {
// here we either set a breakpoint into the destination of the CALL if err := dbp.ClearInternalBreakpoints(); err != nil {
// instruction or we determined that the called function is hidden, return err
// either way we need to resume execution }
if err = setStepIntoBreakpoint(dbp, text, SameGoroutineCondition(dbp.SelectedGoroutine())); err != nil { return StepInstruction(dbp)
return err
} }
default: default:
curthread.Common().returnValues = curbp.Breakpoint.returnInfo.Collect(curthread) curthread.Common().returnValues = curbp.Breakpoint.returnInfo.Collect(curthread)
@ -315,6 +316,15 @@ func pickCurrentThread(dbp *Target, trapthread Thread, threads []Thread) error {
return dbp.SwitchThread(trapthread.ThreadID()) return dbp.SwitchThread(trapthread.ThreadID())
} }
func disassembleCurrentInstruction(p Process, thread Thread) ([]AsmInstruction, error) {
regs, err := thread.Registers(false)
if err != nil {
return nil, err
}
pc := regs.PC()
return disassemble(thread, regs, p.Breakpoints(), p.BinInfo(), pc, pc+uint64(p.BinInfo().Arch.MaxInstructionLength()), true)
}
// stepInstructionOut repeatedly calls StepInstruction until the current // stepInstructionOut repeatedly calls StepInstruction until the current
// function is neither fnname1 or fnname2. // function is neither fnname1 or fnname2.
// This function is used to step out of runtime.Breakpoint as well as // This function is used to step out of runtime.Breakpoint as well as
@ -355,6 +365,11 @@ func Step(dbp *Target) (err error) {
} }
} }
if bp := dbp.CurrentThread().Breakpoint().Breakpoint; bp != nil && bp.Kind == StepBreakpoint && dbp.GetDirection() == Backward {
dbp.ClearInternalBreakpoints()
return StepInstruction(dbp)
}
return Continue(dbp) return Continue(dbp)
} }
@ -402,6 +417,7 @@ func andFrameoffCondition(cond ast.Expr, frameoff int64) ast.Expr {
// StepOut will continue until the current goroutine exits the // StepOut will continue until the current goroutine exits the
// function currently being executed or a deferred function is executed // function currently being executed or a deferred function is executed
func StepOut(dbp *Target) error { func StepOut(dbp *Target) error {
backward := dbp.GetDirection() == Backward
if _, err := dbp.Valid(); err != nil { if _, err := dbp.Valid(); err != nil {
return err return err
} }
@ -436,29 +452,20 @@ func StepOut(dbp *Target) error {
sameGCond := SameGoroutineCondition(selg) sameGCond := SameGoroutineCondition(selg)
retFrameCond := andFrameoffCondition(sameGCond, retframe.FrameOffset()) retFrameCond := andFrameoffCondition(sameGCond, retframe.FrameOffset())
var deferpc uint64 if backward {
if filepath.Ext(topframe.Current.File) == ".go" { if err := stepOutReverse(dbp, topframe, retframe, sameGCond); err != nil {
if topframe.TopmostDefer != nil && topframe.TopmostDefer.DeferredPC != 0 { return err
deferfn := dbp.BinInfo().PCToFunc(topframe.TopmostDefer.DeferredPC)
deferpc, err = FirstPCAfterPrologue(dbp, deferfn, false)
if err != nil {
return err
}
} }
success = true
return Continue(dbp)
} }
if deferpc != 0 && deferpc != topframe.Current.PC { var deferpc uint64
bp, err := dbp.SetBreakpoint(deferpc, NextDeferBreakpoint, sameGCond) if !backward {
deferpc, err = setDeferBreakpoint(dbp, nil, topframe, sameGCond, false)
if err != nil { if err != nil {
if _, ok := err.(BreakpointExistsError); !ok { return err
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{}
} }
} }
@ -467,11 +474,9 @@ func StepOut(dbp *Target) error {
} }
if topframe.Ret != 0 { if topframe.Ret != 0 {
bp, err := dbp.SetBreakpoint(topframe.Ret, NextBreakpoint, retFrameCond) bp, err := allowDuplicateBreakpoint(dbp.SetBreakpoint(topframe.Ret, NextBreakpoint, retFrameCond))
if err != nil { if err != nil {
if _, isexists := err.(BreakpointExistsError); !isexists { return err
return err
}
} }
if bp != nil { if bp != nil {
configureReturnBreakpoint(dbp.BinInfo(), bp, &topframe, retFrameCond) configureReturnBreakpoint(dbp.BinInfo(), bp, &topframe, retFrameCond)
@ -523,6 +528,119 @@ func StepInstruction(dbp *Target) (err error) {
return nil return nil
} }
func allowDuplicateBreakpoint(bp *Breakpoint, err error) (*Breakpoint, error) {
if err != nil {
if _, isexists := err.(BreakpointExistsError); isexists {
return bp, nil
}
}
return bp, err
}
// setDeferBreakpoint is a helper function used by next and StepOut to set a
// breakpoint on the first deferred function.
func setDeferBreakpoint(p Process, text []AsmInstruction, topframe Stackframe, sameGCond ast.Expr, stepInto bool) (uint64, error) {
// Set breakpoint on the most recently deferred function (if any)
var deferpc uint64
if topframe.TopmostDefer != nil && topframe.TopmostDefer.DeferredPC != 0 {
deferfn := p.BinInfo().PCToFunc(topframe.TopmostDefer.DeferredPC)
var err error
deferpc, err = FirstPCAfterPrologue(p, deferfn, false)
if err != nil {
return 0, err
}
}
if deferpc != 0 && deferpc != topframe.Current.PC {
bp, err := allowDuplicateBreakpoint(p.SetBreakpoint(deferpc, NextDeferBreakpoint, sameGCond))
if err != nil {
return 0, err
}
if bp != nil && stepInto {
// If DeferReturns is set then the breakpoint will also be triggered when
// called from runtime.deferreturn. We only do this for the step command,
// not for next or stepout.
bp.DeferReturns = FindDeferReturnCalls(text)
}
}
return deferpc, nil
}
// findCallInstrForRet returns the PC address of the CALL instruction
// immediately preceding the instruction at ret.
func findCallInstrForRet(p Process, mem MemoryReadWriter, ret uint64, fn *Function) (uint64, error) {
text, err := disassemble(mem, nil, p.Breakpoints(), p.BinInfo(), fn.Entry, fn.End, false)
if err != nil {
return 0, err
}
var prevInstr AsmInstruction
for _, instr := range text {
if instr.Loc.PC == ret {
return prevInstr.Loc.PC, nil
}
prevInstr = instr
}
return 0, fmt.Errorf("could not find CALL instruction for address %#x in %s", ret, fn.Name)
}
// stepOutReverse sets a breakpoint on the CALL instruction that created the current frame, this is either:
// - the CALL instruction immediately preceding the return address of the
// current frame
// - the return address of the current frame if the current frame was
// created by a runtime.deferreturn run
// - the return address of the runtime.gopanic frame if the current frame
// was created by a panic
// This function is used to implement reversed StepOut
func stepOutReverse(p *Target, topframe, retframe Stackframe, sameGCond ast.Expr) error {
curthread := p.CurrentThread()
selg := p.SelectedGoroutine()
if selg != nil && selg.Thread != nil {
curthread = selg.Thread
}
callerText, err := disassemble(curthread, nil, p.Breakpoints(), p.BinInfo(), retframe.Current.Fn.Entry, retframe.Current.Fn.End, false)
if err != nil {
return err
}
deferReturns := FindDeferReturnCalls(callerText)
var frames []Stackframe
if selg == nil {
if !curthread.Blocked() {
frames, err = ThreadStacktrace(curthread, 3)
}
} else {
frames, err = selg.Stacktrace(3, 0)
}
if err != nil {
return err
}
var callpc uint64
if isPanicCall(frames) {
if len(frames) < 4 || frames[3].Current.Fn == nil {
return &ErrNoSourceForPC{frames[2].Current.PC}
}
callpc, err = findCallInstrForRet(p, curthread, frames[2].Ret, frames[3].Current.Fn)
if err != nil {
return err
}
} else if ok, pc := isDeferReturnCall(frames, deferReturns); ok {
callpc = pc
} else {
callpc, err = findCallInstrForRet(p, curthread, topframe.Ret, retframe.Current.Fn)
if err != nil {
return err
}
}
_, err = allowDuplicateBreakpoint(p.SetBreakpoint(callpc, NextBreakpoint, sameGCond))
return err
}
// GoroutinesInfo searches for goroutines starting at index 'start', and // GoroutinesInfo searches for goroutines starting at index 'start', and
// returns an array of up to 'count' (or all found elements, if 'count' is 0) // returns an array of up to 'count' (or all found elements, if 'count' is 0)
// G structures representing the information Delve care about from the internal // G structures representing the information Delve care about from the internal

View File

@ -385,6 +385,10 @@ const (
contNext contNext
contStep contStep
contStepout contStepout
contReverseNext
contReverseStep
contReverseStepout
contContinueToBreakpoint
) )
type seqTest struct { type seqTest struct {
@ -459,6 +463,35 @@ func testseq2Args(wd string, args []string, buildFlags protest.BuildFlags, t *te
_, err := p.ClearBreakpoint(bp.Addr) _, err := p.ClearBreakpoint(bp.Addr)
assertNoError(err, t, "ClearBreakpoint() returned an error") assertNoError(err, t, "ClearBreakpoint() returned an error")
} }
case contReverseNext:
if traceTestseq2 {
t.Log("reverse-next")
}
assertNoError(p.ChangeDirection(proc.Backward), t, "direction switch")
assertNoError(proc.Next(p), t, "reverse Next() returned an error")
assertNoError(p.ChangeDirection(proc.Forward), t, "direction switch")
case contReverseStep:
if traceTestseq2 {
t.Log("reverse-step")
}
assertNoError(p.ChangeDirection(proc.Backward), t, "direction switch")
assertNoError(proc.Step(p), t, "reverse Step() returned an error")
assertNoError(p.ChangeDirection(proc.Forward), t, "direction switch")
case contReverseStepout:
if traceTestseq2 {
t.Log("reverse-stepout")
}
assertNoError(p.ChangeDirection(proc.Backward), t, "direction switch")
assertNoError(proc.StepOut(p), t, "reverse StepOut() returned an error")
assertNoError(p.ChangeDirection(proc.Forward), t, "direction switch")
case contContinueToBreakpoint:
bp := setFileBreakpoint(p, t, fixture.Source, tc.pos.(int))
if traceTestseq2 {
t.Log("continue")
}
assertNoError(proc.Continue(p), t, "Continue() returned an error")
_, err := p.ClearBreakpoint(bp.Addr)
assertNoError(err, t, "ClearBreakpoint() returned an error")
} }
f, ln = currentLineNumber(p, t) f, ln = currentLineNumber(p, t)
@ -467,7 +500,7 @@ func testseq2Args(wd string, args []string, buildFlags protest.BuildFlags, t *te
if traceTestseq2 { if traceTestseq2 {
t.Logf("at %#x %s:%d", pc, f, ln) t.Logf("at %#x %s:%d", pc, f, ln)
fmt.Printf("at %#x %s:%d", pc, f, ln) fmt.Printf("at %#x %s:%d\n", pc, f, ln)
} }
switch pos := tc.pos.(type) { switch pos := tc.pos.(type) {
case int: case int:
@ -4600,3 +4633,118 @@ func BenchmarkConditionalBreakpoints(b *testing.B) {
} }
}) })
} }
func TestBackwardNextGeneral(t *testing.T) {
if testBackend != "rr" {
t.Skip("Reverse stepping test needs rr")
}
testseq2(t, "testnextprog", "main.helloworld", []seqTest{
{contContinue, 13},
{contNext, 14},
{contReverseNext, 13},
{contReverseNext, 34},
{contReverseNext, 28},
{contReverseNext, 27},
{contReverseNext, 26},
{contReverseNext, 24},
{contReverseNext, 23},
{contReverseNext, 31},
{contReverseNext, 26},
{contReverseNext, 24},
{contReverseNext, 23},
{contReverseNext, 31},
{contReverseNext, 26},
{contReverseNext, 24},
{contReverseNext, 23},
{contReverseNext, 20},
{contReverseNext, 19},
{contReverseNext, 17},
{contReverseNext, 39},
{contReverseNext, 38},
{contReverseNext, 37},
})
}
func TestBackwardStepOutGeneral(t *testing.T) {
if testBackend != "rr" {
t.Skip("Reverse stepping test needs rr")
}
testseq2(t, "testnextprog", "main.helloworld", []seqTest{
{contContinue, 13},
{contNext, 14},
{contReverseStepout, 34},
{contReverseStepout, 39},
})
}
func TestBackwardStepGeneral(t *testing.T) {
if testBackend != "rr" {
t.Skip("Reverse stepping test needs rr")
}
testseq2(t, "testnextprog", "main.helloworld", []seqTest{
{contContinue, 13},
{contNext, 14},
{contReverseStep, 13},
{contReverseStep, 34},
{contReverseStep, 28},
{contReverseNext, 27}, // skip fmt.Printf
{contReverseStep, 26},
{contReverseStep, 24},
{contReverseStep, 23},
{contReverseStep, 11},
{contReverseNext, 10}, // skip time.Sleep
{contReverseStep, 9},
{contReverseStep, 31},
{contReverseStep, 26},
{contReverseStep, 24},
{contReverseStep, 23},
{contReverseStep, 11},
{contReverseNext, 10}, // skip time.Sleep
{contReverseStep, 9},
{contReverseStep, 31},
{contReverseStep, 26},
{contReverseStep, 24},
{contReverseStep, 23},
{contReverseStep, 20},
{contReverseStep, 19},
{contReverseStep, 17},
{contReverseStep, 39},
{contReverseStep, 38},
{contReverseStep, 37},
})
}
func TestBackwardNextDeferPanic(t *testing.T) {
if testBackend != "rr" {
t.Skip("Reverse stepping test needs rr")
}
testseq2(t, "defercall", "", []seqTest{
{contContinue, 12},
{contReverseNext, 11},
{contReverseNext, 10},
{contReverseNext, 9},
{contReverseNext, 27},
{contContinueToBreakpoint, 12}, // skip first call to sampleFunction
{contContinueToBreakpoint, 6}, // go to call to sampleFunction through deferreturn
{contReverseNext, 13},
{contReverseNext, 12},
{contReverseNext, 11},
{contReverseNext, 10},
{contReverseNext, 9},
{contReverseNext, 27},
{contContinueToBreakpoint, 18}, // go to panic call
{contNext, 6}, // panic so the deferred call happens
{contReverseNext, 18},
{contReverseNext, 17},
{contReverseNext, 16},
{contReverseNext, 15},
{contReverseNext, 23},
{contReverseNext, 22},
{contReverseNext, 21},
{contReverseNext, 28},
})
}

View File

@ -143,6 +143,7 @@ func (err *ErrNoSourceForPC) Error() string {
// when removing instructions belonging to inlined calls we also remove all // when removing instructions belonging to inlined calls we also remove all
// instructions belonging to the current inlined call. // instructions belonging to the current inlined call.
func next(dbp *Target, stepInto, inlinedStepOut bool) error { func next(dbp *Target, stepInto, inlinedStepOut bool) error {
backward := dbp.GetDirection() == Backward
selg := dbp.SelectedGoroutine() selg := dbp.SelectedGoroutine()
curthread := dbp.CurrentThread() curthread := dbp.CurrentThread()
topframe, retframe, err := topframe(selg, curthread) topframe, retframe, err := topframe(selg, curthread)
@ -154,6 +155,10 @@ func next(dbp *Target, stepInto, inlinedStepOut bool) error {
return &ErrNoSourceForPC{topframe.Current.PC} return &ErrNoSourceForPC{topframe.Current.PC}
} }
if backward && retframe.Current.Fn == nil {
return &ErrNoSourceForPC{retframe.Current.PC}
}
// sanity check // sanity check
if inlinedStepOut && !topframe.Inlined { if inlinedStepOut && !topframe.Inlined {
panic("next called with inlinedStepOut but topframe was not inlined") panic("next called with inlinedStepOut but topframe was not inlined")
@ -178,12 +183,36 @@ func next(dbp *Target, stepInto, inlinedStepOut bool) error {
} }
} }
sameGCond := SameGoroutineCondition(selg)
var firstPCAfterPrologue uint64
if backward {
firstPCAfterPrologue, err = FirstPCAfterPrologue(dbp, topframe.Current.Fn, false)
if err != nil {
return err
}
if firstPCAfterPrologue == topframe.Current.PC {
// We don't want to step into the prologue so we just execute a reverse step out instead
if err := stepOutReverse(dbp, topframe, retframe, sameGCond); err != nil {
return err
}
success = true
return nil
}
topframe.Ret, err = findCallInstrForRet(dbp, thread, topframe.Ret, retframe.Current.Fn)
if err != nil {
return err
}
}
text, err := disassemble(thread, regs, dbp.Breakpoints(), dbp.BinInfo(), topframe.Current.Fn.Entry, topframe.Current.Fn.End, false) text, err := disassemble(thread, regs, dbp.Breakpoints(), dbp.BinInfo(), topframe.Current.Fn.Entry, topframe.Current.Fn.End, false)
if err != nil && stepInto { if err != nil && stepInto {
return err return err
} }
sameGCond := SameGoroutineCondition(selg)
retFrameCond := andFrameoffCondition(sameGCond, retframe.FrameOffset()) retFrameCond := andFrameoffCondition(sameGCond, retframe.FrameOffset())
sameFrameCond := andFrameoffCondition(sameGCond, topframe.FrameOffset()) sameFrameCond := andFrameoffCondition(sameGCond, topframe.FrameOffset())
var sameOrRetFrameCond ast.Expr var sameOrRetFrameCond ast.Expr
@ -203,50 +232,17 @@ func next(dbp *Target, stepInto, inlinedStepOut bool) error {
} }
} }
if stepInto { if stepInto && !backward {
for _, instr := range text { err := setStepIntoBreakpoints(dbp, text, topframe, sameGCond)
if instr.Loc.File != topframe.Current.File || instr.Loc.Line != topframe.Current.Line || !instr.IsCall() { if err != nil {
continue return err
}
if instr.DestLoc != nil {
if err := setStepIntoBreakpoint(dbp, []AsmInstruction{instr}, sameGCond); err != nil {
return err
}
} else {
// Non-absolute call instruction, set a StepBreakpoint here
if _, err := dbp.SetBreakpoint(instr.Loc.PC, StepBreakpoint, sameGCond); err != nil {
if _, ok := err.(BreakpointExistsError); !ok {
return err
}
}
}
} }
} }
if !csource { if !backward {
deferreturns := FindDeferReturnCalls(text) _, err = setDeferBreakpoint(dbp, text, topframe, sameGCond, stepInto)
if err != nil {
// Set breakpoint on the most recently deferred function (if any) return err
var deferpc uint64
if topframe.TopmostDefer != nil && topframe.TopmostDefer.DeferredPC != 0 {
deferfn := dbp.BinInfo().PCToFunc(topframe.TopmostDefer.DeferredPC)
var err error
deferpc, err = FirstPCAfterPrologue(dbp, deferfn, false)
if err != nil {
return err
}
}
if deferpc != 0 && deferpc != topframe.Current.PC {
bp, err := dbp.SetBreakpoint(deferpc, NextDeferBreakpoint, sameGCond)
if err != nil {
if _, ok := err.(BreakpointExistsError); !ok {
return err
}
}
if bp != nil && stepInto {
bp.DeferReturns = deferreturns
}
} }
} }
@ -256,6 +252,20 @@ func next(dbp *Target, stepInto, inlinedStepOut bool) error {
return err return err
} }
if backward {
// Ensure that pcs contains firstPCAfterPrologue when reverse stepping.
found := false
for _, pc := range pcs {
if pc == firstPCAfterPrologue {
found = true
break
}
}
if !found {
pcs = append(pcs, firstPCAfterPrologue)
}
}
if !stepInto { if !stepInto {
// Removing any PC range belonging to an inlined call // Removing any PC range belonging to an inlined call
frame := topframe frame := topframe
@ -286,14 +296,19 @@ func next(dbp *Target, stepInto, inlinedStepOut bool) error {
} }
for _, pc := range pcs { for _, pc := range pcs {
if _, err := dbp.SetBreakpoint(pc, NextBreakpoint, sameFrameCond); err != nil { if _, err := allowDuplicateBreakpoint(dbp.SetBreakpoint(pc, NextBreakpoint, sameFrameCond)); err != nil {
if _, ok := err.(BreakpointExistsError); !ok { dbp.ClearInternalBreakpoints()
dbp.ClearInternalBreakpoints() return err
return err
}
} }
} }
if stepInto && backward {
err := setStepIntoBreakpointsReverse(dbp, text, topframe, sameGCond)
if err != nil {
return err
}
}
if !topframe.Inlined { if !topframe.Inlined {
// Add a breakpoint on the return address for the current frame. // Add a breakpoint on the return address for the current frame.
// For inlined functions there is no need to do this, the set of PCs // For inlined functions there is no need to do this, the set of PCs
@ -325,6 +340,46 @@ func next(dbp *Target, stepInto, inlinedStepOut bool) error {
return nil return nil
} }
func setStepIntoBreakpoints(dbp Process, text []AsmInstruction, topframe Stackframe, sameGCond ast.Expr) error {
for _, instr := range text {
if instr.Loc.File != topframe.Current.File || instr.Loc.Line != topframe.Current.Line || !instr.IsCall() {
continue
}
if instr.DestLoc != nil {
if err := setStepIntoBreakpoint(dbp, []AsmInstruction{instr}, sameGCond); err != nil {
return err
}
} else {
// Non-absolute call instruction, set a StepBreakpoint here
if _, err := allowDuplicateBreakpoint(dbp.SetBreakpoint(instr.Loc.PC, StepBreakpoint, sameGCond)); err != nil {
return err
}
}
}
return nil
}
func setStepIntoBreakpointsReverse(dbp Process, text []AsmInstruction, topframe Stackframe, sameGCond ast.Expr) error {
// Set a breakpoint after every CALL instruction
for i, instr := range text {
if instr.Loc.File != topframe.Current.File || !instr.IsCall() || instr.DestLoc == nil || instr.DestLoc.Fn == nil {
continue
}
if fn := instr.DestLoc.Fn; strings.HasPrefix(fn.Name, "runtime.") && !isExportedRuntime(fn.Name) {
continue
}
if nextIdx := i + 1; nextIdx < len(text) {
if _, err := allowDuplicateBreakpoint(dbp.SetBreakpoint(text[nextIdx].Loc.PC, StepBreakpoint, sameGCond)); err != nil {
return err
}
}
}
return nil
}
func FindDeferReturnCalls(text []AsmInstruction) []uint64 { func FindDeferReturnCalls(text []AsmInstruction) []uint64 {
const deferreturn = "runtime.deferreturn" const deferreturn = "runtime.deferreturn"
deferreturns := []uint64{} deferreturns := []uint64{}
@ -415,10 +470,8 @@ func setStepIntoBreakpoint(dbp Process, text []AsmInstruction, cond ast.Expr) er
} }
// Set a breakpoint after the function's prologue // Set a breakpoint after the function's prologue
if _, err := dbp.SetBreakpoint(pc, NextBreakpoint, cond); err != nil { if _, err := allowDuplicateBreakpoint(dbp.SetBreakpoint(pc, NextBreakpoint, cond)); err != nil {
if _, ok := err.(BreakpointExistsError); !ok { return err
return err
}
} }
return nil return nil

View File

@ -142,16 +142,16 @@ For live targets the command takes the following forms:
If newargv is omitted the process is restarted (or re-recorded) with the same argument vector. If newargv is omitted the process is restarted (or re-recorded) with the same argument vector.
If -noargs is specified instead, the argument vector is cleared. If -noargs is specified instead, the argument vector is cleared.
`}, `},
{aliases: []string{"continue", "c"}, group: runCmds, cmdFn: c.cont, helpMsg: "Run until breakpoint or program termination."}, {aliases: []string{"continue", "c"}, group: runCmds, cmdFn: c.cont, allowedPrefixes: revPrefix, helpMsg: "Run until breakpoint or program termination."},
{aliases: []string{"step", "s"}, group: runCmds, cmdFn: c.step, helpMsg: "Single step through program."}, {aliases: []string{"step", "s"}, group: runCmds, cmdFn: c.step, allowedPrefixes: revPrefix, helpMsg: "Single step through program."},
{aliases: []string{"step-instruction", "si"}, group: runCmds, allowedPrefixes: revPrefix, cmdFn: c.stepInstruction, helpMsg: "Single step a single cpu instruction."}, {aliases: []string{"step-instruction", "si"}, group: runCmds, allowedPrefixes: revPrefix, cmdFn: c.stepInstruction, helpMsg: "Single step a single cpu instruction."},
{aliases: []string{"next", "n"}, group: runCmds, cmdFn: c.next, helpMsg: `Step over to next source line. {aliases: []string{"next", "n"}, group: runCmds, cmdFn: c.next, allowedPrefixes: revPrefix, helpMsg: `Step over to next source line.
next [count] next [count]
Optional [count] argument allows you to skip multiple lines. Optional [count] argument allows you to skip multiple lines.
`}, `},
{aliases: []string{"stepout", "so"}, group: runCmds, cmdFn: c.stepout, helpMsg: "Step out of the current function."}, {aliases: []string{"stepout", "so"}, group: runCmds, allowedPrefixes: revPrefix, cmdFn: c.stepout, helpMsg: "Step out of the current function."},
{aliases: []string{"call"}, group: runCmds, cmdFn: c.call, helpMsg: `Resumes process, injecting a function call (EXPERIMENTAL!!!) {aliases: []string{"call"}, group: runCmds, cmdFn: c.call, helpMsg: `Resumes process, injecting a function call (EXPERIMENTAL!!!)
call [-unsafe] <function call expression> call [-unsafe] <function call expression>
@ -390,7 +390,7 @@ For example:
c.cmds = append(c.cmds, command{ c.cmds = append(c.cmds, command{
aliases: []string{"rewind", "rw"}, aliases: []string{"rewind", "rw"},
group: runCmds, group: runCmds,
cmdFn: rewind, cmdFn: c.rewind,
helpMsg: "Run backwards until breakpoint or program termination.", helpMsg: "Run backwards until breakpoint or program termination.",
}) })
c.cmds = append(c.cmds, command{ c.cmds = append(c.cmds, command{
@ -416,6 +416,7 @@ The "note" is arbitrary text that can be used to identify the checkpoint, if it
}) })
c.cmds = append(c.cmds, command{ c.cmds = append(c.cmds, command{
aliases: []string{"rev"}, aliases: []string{"rev"},
group: runCmds,
cmdFn: c.revCmd, cmdFn: c.revCmd,
helpMsg: `Reverses the execution of the target program for the command specified. helpMsg: `Reverses the execution of the target program for the command specified.
Currently, only the rev step-instruction command is supported.`, Currently, only the rev step-instruction command is supported.`,
@ -1056,6 +1057,9 @@ func printcontextNoState(t *Term) {
} }
func (c *Commands) cont(t *Term, ctx callContext, args string) error { func (c *Commands) cont(t *Term, ctx callContext, args string) error {
if ctx.Prefix == revPrefix {
return c.rewind(t, ctx, args)
}
c.frame = 0 c.frame = 0
stateChan := t.client.Continue() stateChan := t.client.Continue()
var state *api.DebuggerState var state *api.DebuggerState
@ -1079,7 +1083,7 @@ func continueUntilCompleteNext(t *Term, state *api.DebuggerState, op string, sho
} }
for { for {
fmt.Printf("\tbreakpoint hit during %s, continuing...\n", op) fmt.Printf("\tbreakpoint hit during %s, continuing...\n", op)
stateChan := t.client.Continue() stateChan := t.client.DirectionCongruentContinue()
var state *api.DebuggerState var state *api.DebuggerState
for state = range stateChan { for state = range stateChan {
if state.Err != nil { if state.Err != nil {
@ -1117,7 +1121,11 @@ func (c *Commands) step(t *Term, ctx callContext, args string) error {
return err return err
} }
c.frame = 0 c.frame = 0
state, err := exitedToError(t.client.Step()) stepfn := t.client.Step
if ctx.Prefix == revPrefix {
stepfn = t.client.ReverseStep
}
state, err := exitedToError(stepfn())
if err != nil { if err != nil {
printcontextNoState(t) printcontextNoState(t)
return err return err
@ -1172,6 +1180,12 @@ func (c *Commands) next(t *Term, ctx callContext, args string) error {
if c.frame != 0 { if c.frame != 0 {
return notOnFrameZeroErr return notOnFrameZeroErr
} }
nextfn := t.client.Next
if ctx.Prefix == revPrefix {
nextfn = t.client.ReverseNext
}
var count int64 var count int64
var err error var err error
if count, err = parseOptionalCount(args); err != nil { if count, err = parseOptionalCount(args); err != nil {
@ -1180,7 +1194,7 @@ func (c *Commands) next(t *Term, ctx callContext, args string) error {
return errors.New("Invalid next count") return errors.New("Invalid next count")
} }
for ; count > 0; count-- { for ; count > 0; count-- {
state, err := exitedToError(t.client.Next()) state, err := exitedToError(nextfn())
if err != nil { if err != nil {
printcontextNoState(t) printcontextNoState(t)
return err return err
@ -1204,7 +1218,13 @@ func (c *Commands) stepout(t *Term, ctx callContext, args string) error {
if c.frame != 0 { if c.frame != 0 {
return notOnFrameZeroErr return notOnFrameZeroErr
} }
state, err := exitedToError(t.client.StepOut())
stepoutfn := t.client.StepOut
if ctx.Prefix == revPrefix {
stepoutfn = t.client.ReverseStepOut
}
state, err := exitedToError(stepoutfn())
if err != nil { if err != nil {
printcontextNoState(t) printcontextNoState(t)
return err return err
@ -2304,7 +2324,8 @@ func (c *Commands) executeFile(t *Term, name string) error {
return scanner.Err() return scanner.Err()
} }
func rewind(t *Term, ctx callContext, args string) error { func (c *Commands) rewind(t *Term, ctx callContext, args string) error {
c.frame = 0
stateChan := t.client.Rewind() stateChan := t.client.Rewind()
var state *api.DebuggerState var state *api.DebuggerState
for state = range stateChan { for state = range stateChan {

View File

@ -366,16 +366,24 @@ const (
Continue = "continue" Continue = "continue"
// Rewind resumes process execution backwards (target must be a recording). // Rewind resumes process execution backwards (target must be a recording).
Rewind = "rewind" Rewind = "rewind"
// DirecitonCongruentContinue resumes process execution, if a reverse next, step or stepout operation is in progress it will resume execution backward.
DirectionCongruentContinue = "directionCongruentContinue"
// Step continues to next source line, entering function calls. // Step continues to next source line, entering function calls.
Step = "step" Step = "step"
// ReverseStep continues backward to the previous line of source code, entering function calls.
ReverseStep = "reverseStep"
// StepOut continues to the return address of the current function // StepOut continues to the return address of the current function
StepOut = "stepOut" StepOut = "stepOut"
// ReverseStepOut continues backward to the calle rof the current function.
ReverseStepOut = "reverseStepOut"
// StepInstruction continues for exactly 1 cpu instruction. // StepInstruction continues for exactly 1 cpu instruction.
StepInstruction = "stepInstruction" StepInstruction = "stepInstruction"
// ReverseStepInstruction reverses execution for exactly 1 cpu instruction. // ReverseStepInstruction reverses execution for exactly 1 cpu instruction.
ReverseStepInstruction = "reverseStepInstruction" ReverseStepInstruction = "reverseStepInstruction"
// Next continues to the next source line, not entering function calls. // Next continues to the next source line, not entering function calls.
Next = "next" Next = "next"
// ReverseNext continues backward to the previous line of source code, not entering function calls.
ReverseNext = "reverseNext"
// SwitchThread switches the debugger's current thread context. // SwitchThread switches the debugger's current thread context.
SwitchThread = "switchThread" SwitchThread = "switchThread"
// SwitchGoroutine switches the debugger's current thread context to the thread running the specified goroutine // SwitchGoroutine switches the debugger's current thread context to the thread running the specified goroutine

View File

@ -32,12 +32,20 @@ type Client interface {
Continue() <-chan *api.DebuggerState Continue() <-chan *api.DebuggerState
// Rewind resumes process execution backwards. // Rewind resumes process execution backwards.
Rewind() <-chan *api.DebuggerState Rewind() <-chan *api.DebuggerState
// DirecitonCongruentContinue resumes process execution, if a reverse next, step or stepout operation is in progress it will resume execution backward.
DirectionCongruentContinue() <-chan *api.DebuggerState
// Next continues to the next source line, not entering function calls. // Next continues to the next source line, not entering function calls.
Next() (*api.DebuggerState, error) Next() (*api.DebuggerState, error)
// ReverseNext continues backward to the previous line of source code, not entering function calls.
ReverseNext() (*api.DebuggerState, error)
// Step continues to the next source line, entering function calls. // Step continues to the next source line, entering function calls.
Step() (*api.DebuggerState, error) Step() (*api.DebuggerState, error)
// StepOut continues to the return address of the current function // ReverseStep continues backward to the previous line of source code, entering function calls.
ReverseStep() (*api.DebuggerState, error)
// StepOut continues to the return address of the current function.
StepOut() (*api.DebuggerState, error) StepOut() (*api.DebuggerState, error)
// ReverseStepOut continues backward to the calle rof the current function.
ReverseStepOut() (*api.DebuggerState, error)
// Call resumes process execution while making a function call. // Call resumes process execution while making a function call.
Call(goroutineID int, expr string, unsafe bool) (*api.DebuggerState, error) Call(goroutineID int, expr string, unsafe bool) (*api.DebuggerState, error)

View File

@ -695,9 +695,18 @@ func (d *Debugger) Command(command *api.DebuggerCommand) (*api.DebuggerState, er
switch command.Name { switch command.Name {
case api.Continue: case api.Continue:
d.log.Debug("continuing") d.log.Debug("continuing")
if err := d.target.ChangeDirection(proc.Forward); err != nil {
return nil, err
}
err = proc.Continue(d.target)
case api.DirectionCongruentContinue:
d.log.Debug("continuing (direction congruent)")
err = proc.Continue(d.target) err = proc.Continue(d.target)
case api.Call: case api.Call:
d.log.Debugf("function call %s", command.Expr) d.log.Debugf("function call %s", command.Expr)
if err := d.target.ChangeDirection(proc.Forward); err != nil {
return nil, err
}
if command.ReturnInfoLoadConfig == nil { if command.ReturnInfoLoadConfig == nil {
return nil, errors.New("can not call function with nil ReturnInfoLoadConfig") return nil, errors.New("can not call function with nil ReturnInfoLoadConfig")
} }
@ -711,33 +720,57 @@ func (d *Debugger) Command(command *api.DebuggerCommand) (*api.DebuggerState, er
err = proc.EvalExpressionWithCalls(d.target, g, command.Expr, *api.LoadConfigToProc(command.ReturnInfoLoadConfig), !command.UnsafeCall) err = proc.EvalExpressionWithCalls(d.target, g, command.Expr, *api.LoadConfigToProc(command.ReturnInfoLoadConfig), !command.UnsafeCall)
case api.Rewind: case api.Rewind:
d.log.Debug("rewinding") d.log.Debug("rewinding")
if err := d.target.Direction(proc.Backward); err != nil { if err := d.target.ChangeDirection(proc.Backward); err != nil {
return nil, err return nil, err
} }
defer func() {
d.target.Direction(proc.Forward)
}()
err = proc.Continue(d.target) err = proc.Continue(d.target)
case api.Next: case api.Next:
d.log.Debug("nexting") d.log.Debug("nexting")
if err := d.target.ChangeDirection(proc.Forward); err != nil {
return nil, err
}
err = proc.Next(d.target)
case api.ReverseNext:
d.log.Debug("reverse nexting")
if err := d.target.ChangeDirection(proc.Backward); err != nil {
return nil, err
}
err = proc.Next(d.target) err = proc.Next(d.target)
case api.Step: case api.Step:
d.log.Debug("stepping") d.log.Debug("stepping")
if err := d.target.ChangeDirection(proc.Forward); err != nil {
return nil, err
}
err = proc.Step(d.target)
case api.ReverseStep:
d.log.Debug("reverse stepping")
if err := d.target.ChangeDirection(proc.Backward); err != nil {
return nil, err
}
err = proc.Step(d.target) err = proc.Step(d.target)
case api.StepInstruction: case api.StepInstruction:
d.log.Debug("single stepping") d.log.Debug("single stepping")
if err := d.target.ChangeDirection(proc.Forward); err != nil {
return nil, err
}
err = proc.StepInstruction(d.target) err = proc.StepInstruction(d.target)
case api.ReverseStepInstruction: case api.ReverseStepInstruction:
d.log.Debug("reverse single stepping") d.log.Debug("reverse single stepping")
if err := d.target.Direction(proc.Backward); err != nil { if err := d.target.ChangeDirection(proc.Backward); err != nil {
return nil, err return nil, err
} }
defer func() {
d.target.Direction(proc.Forward)
}()
err = proc.StepInstruction(d.target) err = proc.StepInstruction(d.target)
case api.StepOut: case api.StepOut:
d.log.Debug("step out") d.log.Debug("step out")
if err := d.target.ChangeDirection(proc.Forward); err != nil {
return nil, err
}
err = proc.StepOut(d.target)
case api.ReverseStepOut:
d.log.Debug("reverse step out")
if err := d.target.ChangeDirection(proc.Backward); err != nil {
return nil, err
}
err = proc.StepOut(d.target) err = proc.StepOut(d.target)
case api.SwitchThread: case api.SwitchThread:
d.log.Debugf("switching to thread %d", command.ThreadID) d.log.Debugf("switching to thread %d", command.ThreadID)

View File

@ -92,6 +92,10 @@ func (c *RPCClient) Rewind() <-chan *api.DebuggerState {
return c.continueDir(api.Rewind) return c.continueDir(api.Rewind)
} }
func (c *RPCClient) DirectionCongruentContinue() <-chan *api.DebuggerState {
return c.continueDir(api.DirectionCongruentContinue)
}
func (c *RPCClient) continueDir(cmd string) <-chan *api.DebuggerState { func (c *RPCClient) continueDir(cmd string) <-chan *api.DebuggerState {
ch := make(chan *api.DebuggerState) ch := make(chan *api.DebuggerState)
go func() { go func() {
@ -136,18 +140,36 @@ func (c *RPCClient) Next() (*api.DebuggerState, error) {
return &out.State, err return &out.State, err
} }
func (c *RPCClient) ReverseNext() (*api.DebuggerState, error) {
var out CommandOut
err := c.call("Command", api.DebuggerCommand{Name: api.ReverseNext, ReturnInfoLoadConfig: c.retValLoadCfg}, &out)
return &out.State, err
}
func (c *RPCClient) Step() (*api.DebuggerState, error) { func (c *RPCClient) Step() (*api.DebuggerState, error) {
var out CommandOut var out CommandOut
err := c.call("Command", api.DebuggerCommand{Name: api.Step, ReturnInfoLoadConfig: c.retValLoadCfg}, &out) err := c.call("Command", api.DebuggerCommand{Name: api.Step, ReturnInfoLoadConfig: c.retValLoadCfg}, &out)
return &out.State, err return &out.State, err
} }
func (c *RPCClient) ReverseStep() (*api.DebuggerState, error) {
var out CommandOut
err := c.call("Command", api.DebuggerCommand{Name: api.ReverseStep, ReturnInfoLoadConfig: c.retValLoadCfg}, &out)
return &out.State, err
}
func (c *RPCClient) StepOut() (*api.DebuggerState, error) { func (c *RPCClient) StepOut() (*api.DebuggerState, error) {
var out CommandOut var out CommandOut
err := c.call("Command", api.DebuggerCommand{Name: api.StepOut, ReturnInfoLoadConfig: c.retValLoadCfg}, &out) err := c.call("Command", api.DebuggerCommand{Name: api.StepOut, ReturnInfoLoadConfig: c.retValLoadCfg}, &out)
return &out.State, err return &out.State, err
} }
func (c *RPCClient) ReverseStepOut() (*api.DebuggerState, error) {
var out CommandOut
err := c.call("Command", api.DebuggerCommand{Name: api.ReverseStepOut, ReturnInfoLoadConfig: c.retValLoadCfg}, &out)
return &out.State, err
}
func (c *RPCClient) Call(goroutineID int, expr string, unsafe bool) (*api.DebuggerState, error) { func (c *RPCClient) Call(goroutineID int, expr string, unsafe bool) (*api.DebuggerState, error) {
var out CommandOut var out CommandOut
err := c.call("Command", api.DebuggerCommand{Name: api.Call, ReturnInfoLoadConfig: c.retValLoadCfg, Expr: expr, UnsafeCall: unsafe, GoroutineID: goroutineID}, &out) err := c.call("Command", api.DebuggerCommand{Name: api.Call, ReturnInfoLoadConfig: c.retValLoadCfg, Expr: expr, UnsafeCall: unsafe, GoroutineID: goroutineID}, &out)