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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 589 additions and 140 deletions

@ -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).
- 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.
- `reverse step` will not reverse step into functions called by deferred calls.

@ -14,6 +14,7 @@ Command | Description
[continue](#continue) | Run until breakpoint or program termination.
[next](#next) | Step over to next source line.
[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.
[step](#step) | Single step through program.
[step-instruction](#step-instruction) | Single step a single cpu instruction.
@ -83,7 +84,6 @@ Command | Description
[help](#help) | Prints the help message.
[libraries](#libraries) | List loaded dynamic libraries
[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
[sources](#sources) | Print list of source files.
[types](#types) | Print list of types

@ -445,7 +445,7 @@ func TestTypecheckRPC(t *testing.T) {
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
continue
}

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

@ -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.
func (p *Process) Restart(string) error { return ErrContinueCore }
// Direction will only return an error as you cannot continue a core process.
func (p *Process) Direction(proc.Direction) error { return ErrContinueCore }
// ChangeDirection will only return an error as you cannot continue a core process.
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.
func (p *Process) When() (string, error) { return "", nil }

@ -658,6 +658,7 @@ func (p *Process) ContinueOnce() (proc.Thread, proc.StopReason, error) {
var threadID string
var trapthread *Thread
var tu = threadUpdater{p: p}
var atstart bool
continueLoop:
for {
var err error
@ -683,7 +684,7 @@ continueLoop:
}
var shouldStop bool
trapthread, shouldStop = p.handleThreadSignals(trapthread)
trapthread, atstart, shouldStop = p.handleThreadSignals(trapthread)
if shouldStop {
break continueLoop
}
@ -691,18 +692,23 @@ continueLoop:
p.clearThreadRegisters()
stopReason := proc.StopUnknown
if atstart {
stopReason = proc.StopLaunched
}
if p.BinInfo().GOOS == "linux" {
if err := linutil.ElfUpdateSharedObjects(p); err != nil {
return nil, proc.StopUnknown, err
return nil, stopReason, err
}
}
if err := p.setCurrentBreakpoints(); err != nil {
return nil, proc.StopUnknown, err
return nil, stopReason, err
}
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
@ -724,7 +730,7 @@ continueLoop:
// the signals that are reported here can not be propagated back to the target process.
trapthread.sig = 0
}
return trapthread, proc.StopUnknown, err
return trapthread, stopReason, err
}
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
// signals and return control to the user.
// 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
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
// in backward continue mode.
case 0:
if p.conn.direction == proc.Backward {
if p.conn.direction == proc.Backward && th == trapthread {
isStopSignal = true
atstart = true
}
default:
@ -809,7 +816,7 @@ func (p *Process) handleThreadSignals(trapthread *Thread) (trapthreadOut *Thread
shouldStop = true
}
return trapthread, shouldStop
return trapthread, atstart, shouldStop
}
// RequestManualStop will attempt to stop the process
@ -1014,10 +1021,13 @@ func (p *Process) ClearCheckpoint(id int) error {
return nil
}
// Direction sets whether to run the program forwards or in reverse execution.
func (p *Process) Direction(dir proc.Direction) error {
// ChangeDirection sets whether to run the program forwards or in reverse execution.
func (p *Process) ChangeDirection(dir proc.Direction) error {
if p.tracedir == "" {
return proc.ErrNotRecorded
if dir != proc.Forward {
return proc.ErrNotRecorded
}
return nil
}
if p.conn.conn == nil {
return proc.ErrProcessExited{Pid: p.conn.pid}
@ -1032,6 +1042,11 @@ func (p *Process) Direction(dir proc.Direction) error {
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.
func (p *Process) Breakpoints() *proc.BreakpointMap {
return &p.breakpoints

@ -146,7 +146,7 @@ func TestReverseBreakpointCounts(t *testing.T) {
}
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)
startbp := setFileBreakpoint(p, t, fixture, 20)
@ -285,7 +285,7 @@ func TestIssue1376(t *testing.T) {
assertNoError(proc.Continue(p), t, "Continue (forward)")
_, err := p.ClearBreakpoint(bp.Addr)
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)")
})
}

@ -40,7 +40,9 @@ type RecordingManipulation interface {
// to the trace directory.
Recorded() (recorded bool, tracedir string)
// 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() (string, error)
// Checkpoint sets a checkpoint at the current position.

@ -70,9 +70,17 @@ func (dbp *Process) Recorded() (bool, string) { return false, "" }
// recorded traces.
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.
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.
func (dbp *Process) When() (string, error) { return "", nil }

@ -8,7 +8,6 @@ import (
"go/constant"
"go/token"
"os"
"path/filepath"
"strconv"
"strings"
@ -164,6 +163,9 @@ func Continue(dbp *Target) error {
if err != nil {
return err
}
if dbp.StopReason == StopLaunched {
dbp.ClearInternalBreakpoints()
}
threads := dbp.ThreadList()
@ -229,20 +231,19 @@ func Continue(dbp *Target) error {
if err := conditionErrors(threads); err != nil {
return err
}
regs, err := curthread.Registers(false)
if err != nil {
return err
}
pc := regs.PC()
text, err := disassemble(curthread, regs, dbp.Breakpoints(), dbp.BinInfo(), pc, pc+uint64(dbp.BinInfo().Arch.MaxInstructionLength()), true)
if err != nil {
return err
}
// here we either set a breakpoint into the destination of the CALL
// instruction or we determined that the called function is hidden,
// either way we need to resume execution
if err = setStepIntoBreakpoint(dbp, text, SameGoroutineCondition(dbp.SelectedGoroutine())); err != nil {
return err
if dbp.GetDirection() == Forward {
text, err := disassembleCurrentInstruction(dbp, curthread)
// here we either set a breakpoint into the destination of the CALL
// instruction or we determined that the called function is hidden,
// either way we need to resume execution
if err = setStepIntoBreakpoint(dbp, text, SameGoroutineCondition(dbp.SelectedGoroutine())); err != nil {
return err
}
} else {
if err := dbp.ClearInternalBreakpoints(); err != nil {
return err
}
return StepInstruction(dbp)
}
default:
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())
}
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
// function is neither fnname1 or fnname2.
// 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)
}
@ -402,6 +417,7 @@ func andFrameoffCondition(cond ast.Expr, frameoff int64) ast.Expr {
// StepOut will continue until the current goroutine exits the
// function currently being executed or a deferred function is executed
func StepOut(dbp *Target) error {
backward := dbp.GetDirection() == Backward
if _, err := dbp.Valid(); err != nil {
return err
}
@ -436,29 +452,20 @@ func StepOut(dbp *Target) error {
sameGCond := SameGoroutineCondition(selg)
retFrameCond := andFrameoffCondition(sameGCond, retframe.FrameOffset())
var deferpc uint64
if filepath.Ext(topframe.Current.File) == ".go" {
if topframe.TopmostDefer != nil && topframe.TopmostDefer.DeferredPC != 0 {
deferfn := dbp.BinInfo().PCToFunc(topframe.TopmostDefer.DeferredPC)
deferpc, err = FirstPCAfterPrologue(dbp, deferfn, false)
if err != nil {
return err
}
if backward {
if err := stepOutReverse(dbp, topframe, retframe, sameGCond); err != nil {
return err
}
success = true
return Continue(dbp)
}
if deferpc != 0 && deferpc != topframe.Current.PC {
bp, err := dbp.SetBreakpoint(deferpc, NextDeferBreakpoint, sameGCond)
var deferpc uint64
if !backward {
deferpc, err = setDeferBreakpoint(dbp, nil, topframe, sameGCond, false)
if err != nil {
if _, ok := err.(BreakpointExistsError); !ok {
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{}
return err
}
}
@ -467,11 +474,9 @@ func StepOut(dbp *Target) error {
}
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 _, isexists := err.(BreakpointExistsError); !isexists {
return err
}
return err
}
if bp != nil {
configureReturnBreakpoint(dbp.BinInfo(), bp, &topframe, retFrameCond)
@ -523,6 +528,119 @@ func StepInstruction(dbp *Target) (err error) {
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
// 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

@ -385,6 +385,10 @@ const (
contNext
contStep
contStepout
contReverseNext
contReverseStep
contReverseStepout
contContinueToBreakpoint
)
type seqTest struct {
@ -459,6 +463,35 @@ func testseq2Args(wd string, args []string, buildFlags protest.BuildFlags, t *te
_, err := p.ClearBreakpoint(bp.Addr)
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)
@ -467,7 +500,7 @@ func testseq2Args(wd string, args []string, buildFlags protest.BuildFlags, t *te
if traceTestseq2 {
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) {
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},
})
}

@ -143,6 +143,7 @@ func (err *ErrNoSourceForPC) Error() string {
// 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
selg := dbp.SelectedGoroutine()
curthread := dbp.CurrentThread()
topframe, retframe, err := topframe(selg, curthread)
@ -154,6 +155,10 @@ func next(dbp *Target, stepInto, inlinedStepOut bool) error {
return &ErrNoSourceForPC{topframe.Current.PC}
}
if backward && retframe.Current.Fn == nil {
return &ErrNoSourceForPC{retframe.Current.PC}
}
// sanity check
if inlinedStepOut && !topframe.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)
if err != nil && stepInto {
return err
}
sameGCond := SameGoroutineCondition(selg)
retFrameCond := andFrameoffCondition(sameGCond, retframe.FrameOffset())
sameFrameCond := andFrameoffCondition(sameGCond, topframe.FrameOffset())
var sameOrRetFrameCond ast.Expr
@ -203,50 +232,17 @@ func next(dbp *Target, stepInto, inlinedStepOut bool) error {
}
}
if stepInto {
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 := dbp.SetBreakpoint(instr.Loc.PC, StepBreakpoint, sameGCond); err != nil {
if _, ok := err.(BreakpointExistsError); !ok {
return err
}
}
}
if stepInto && !backward {
err := setStepIntoBreakpoints(dbp, text, topframe, sameGCond)
if err != nil {
return err
}
}
if !csource {
deferreturns := FindDeferReturnCalls(text)
// Set breakpoint on the most recently deferred function (if any)
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
}
if !backward {
_, err = setDeferBreakpoint(dbp, text, topframe, sameGCond, stepInto)
if err != nil {
return err
}
}
@ -256,6 +252,20 @@ func next(dbp *Target, stepInto, inlinedStepOut bool) error {
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 {
// Removing any PC range belonging to an inlined call
frame := topframe
@ -286,14 +296,19 @@ func next(dbp *Target, stepInto, inlinedStepOut bool) error {
}
for _, pc := range pcs {
if _, err := dbp.SetBreakpoint(pc, NextBreakpoint, sameFrameCond); err != nil {
if _, ok := err.(BreakpointExistsError); !ok {
dbp.ClearInternalBreakpoints()
return err
}
if _, err := allowDuplicateBreakpoint(dbp.SetBreakpoint(pc, NextBreakpoint, sameFrameCond)); err != nil {
dbp.ClearInternalBreakpoints()
return err
}
}
if stepInto && backward {
err := setStepIntoBreakpointsReverse(dbp, text, topframe, sameGCond)
if err != nil {
return err
}
}
if !topframe.Inlined {
// 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
@ -325,6 +340,46 @@ func next(dbp *Target, stepInto, inlinedStepOut bool) error {
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 {
const deferreturn = "runtime.deferreturn"
deferreturns := []uint64{}
@ -415,10 +470,8 @@ func setStepIntoBreakpoint(dbp Process, text []AsmInstruction, cond ast.Expr) er
}
// Set a breakpoint after the function's prologue
if _, err := dbp.SetBreakpoint(pc, NextBreakpoint, cond); err != nil {
if _, ok := err.(BreakpointExistsError); !ok {
return err
}
if _, err := allowDuplicateBreakpoint(dbp.SetBreakpoint(pc, NextBreakpoint, cond)); err != nil {
return err
}
return nil

@ -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 -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{"step", "s"}, group: runCmds, cmdFn: c.step, helpMsg: "Single step through program."},
{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, 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{"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]
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!!!)
call [-unsafe] <function call expression>
@ -390,7 +390,7 @@ For example:
c.cmds = append(c.cmds, command{
aliases: []string{"rewind", "rw"},
group: runCmds,
cmdFn: rewind,
cmdFn: c.rewind,
helpMsg: "Run backwards until breakpoint or program termination.",
})
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{
aliases: []string{"rev"},
group: runCmds,
cmdFn: c.revCmd,
helpMsg: `Reverses the execution of the target program for the command specified.
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 {
if ctx.Prefix == revPrefix {
return c.rewind(t, ctx, args)
}
c.frame = 0
stateChan := t.client.Continue()
var state *api.DebuggerState
@ -1079,7 +1083,7 @@ func continueUntilCompleteNext(t *Term, state *api.DebuggerState, op string, sho
}
for {
fmt.Printf("\tbreakpoint hit during %s, continuing...\n", op)
stateChan := t.client.Continue()
stateChan := t.client.DirectionCongruentContinue()
var state *api.DebuggerState
for state = range stateChan {
if state.Err != nil {
@ -1117,7 +1121,11 @@ func (c *Commands) step(t *Term, ctx callContext, args string) error {
return err
}
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 {
printcontextNoState(t)
return err
@ -1172,6 +1180,12 @@ func (c *Commands) next(t *Term, ctx callContext, args string) error {
if c.frame != 0 {
return notOnFrameZeroErr
}
nextfn := t.client.Next
if ctx.Prefix == revPrefix {
nextfn = t.client.ReverseNext
}
var count int64
var err error
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")
}
for ; count > 0; count-- {
state, err := exitedToError(t.client.Next())
state, err := exitedToError(nextfn())
if err != nil {
printcontextNoState(t)
return err
@ -1204,7 +1218,13 @@ func (c *Commands) stepout(t *Term, ctx callContext, args string) error {
if c.frame != 0 {
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 {
printcontextNoState(t)
return err
@ -2304,7 +2324,8 @@ func (c *Commands) executeFile(t *Term, name string) error {
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()
var state *api.DebuggerState
for state = range stateChan {

@ -366,16 +366,24 @@ const (
Continue = "continue"
// Rewind resumes process execution backwards (target must be a recording).
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 = "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 = "stepOut"
// ReverseStepOut continues backward to the calle rof the current function.
ReverseStepOut = "reverseStepOut"
// StepInstruction continues for exactly 1 cpu instruction.
StepInstruction = "stepInstruction"
// ReverseStepInstruction reverses execution for exactly 1 cpu instruction.
ReverseStepInstruction = "reverseStepInstruction"
// Next continues to the next source line, not entering function calls.
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 = "switchThread"
// SwitchGoroutine switches the debugger's current thread context to the thread running the specified goroutine

@ -32,12 +32,20 @@ type Client interface {
Continue() <-chan *api.DebuggerState
// Rewind resumes process execution backwards.
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() (*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() (*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)
// ReverseStepOut continues backward to the calle rof the current function.
ReverseStepOut() (*api.DebuggerState, error)
// Call resumes process execution while making a function call.
Call(goroutineID int, expr string, unsafe bool) (*api.DebuggerState, error)

@ -695,9 +695,18 @@ func (d *Debugger) Command(command *api.DebuggerCommand) (*api.DebuggerState, er
switch command.Name {
case api.Continue:
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)
case api.Call:
d.log.Debugf("function call %s", command.Expr)
if err := d.target.ChangeDirection(proc.Forward); err != nil {
return nil, err
}
if command.ReturnInfoLoadConfig == nil {
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)
case api.Rewind:
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
}
defer func() {
d.target.Direction(proc.Forward)
}()
err = proc.Continue(d.target)
case api.Next:
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)
case api.Step:
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)
case api.StepInstruction:
d.log.Debug("single stepping")
if err := d.target.ChangeDirection(proc.Forward); err != nil {
return nil, err
}
err = proc.StepInstruction(d.target)
case api.ReverseStepInstruction:
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
}
defer func() {
d.target.Direction(proc.Forward)
}()
err = proc.StepInstruction(d.target)
case api.StepOut:
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)
case api.SwitchThread:
d.log.Debugf("switching to thread %d", command.ThreadID)

@ -92,6 +92,10 @@ func (c *RPCClient) Rewind() <-chan *api.DebuggerState {
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 {
ch := make(chan *api.DebuggerState)
go func() {
@ -136,18 +140,36 @@ func (c *RPCClient) Next() (*api.DebuggerState, error) {
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) {
var out CommandOut
err := c.call("Command", api.DebuggerCommand{Name: api.Step, ReturnInfoLoadConfig: c.retValLoadCfg}, &out)
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) {
var out CommandOut
err := c.call("Command", api.DebuggerCommand{Name: api.StepOut, ReturnInfoLoadConfig: c.retValLoadCfg}, &out)
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) {
var out CommandOut
err := c.call("Command", api.DebuggerCommand{Name: api.Call, ReturnInfoLoadConfig: c.retValLoadCfg, Expr: expr, UnsafeCall: unsafe, GoroutineID: goroutineID}, &out)