pkg/terminal,pkg/proc: Implement next-instruction (#3671)

The next-instruction (nexti) command behaves like
step-instruction (stepi) however, similar to the
`next` command it will step over function calls.
This commit is contained in:
Derek Parker 2024-02-28 00:28:33 -08:00 committed by GitHub
parent 5a9b835406
commit 29aa2ea8c9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 105 additions and 24 deletions

@ -13,6 +13,7 @@ Command | Description
[call](#call) | Resumes process, injecting a function call (EXPERIMENTAL!!!)
[continue](#continue) | Run until breakpoint or program termination.
[next](#next) | Step over to next source line.
[next-instruction](#next-instruction) | Single step a single cpu instruction, skipping function calls.
[rebuild](#rebuild) | Rebuild the target executable and restarts it. It does not work if the executable was not built by delve.
[restart](#restart) | Restart process.
[rev](#rev) | Reverses the execution of the target program for the command specified.
@ -523,6 +524,11 @@ Optional [count] argument allows you to skip multiple lines.
Aliases: n
## next-instruction
Single step a single cpu instruction, skipping function calls.
Aliases: ni nexti
## on
Executes a command when a breakpoint is hit.
@ -658,7 +664,7 @@ Aliases: s
## step-instruction
Single step a single cpu instruction.
Aliases: si
Aliases: si stepi
## stepout
Step out of the current function.

@ -22,7 +22,7 @@ func TestStepInstructionOnBreakpoint(t *testing.T) {
assertNoError(grp.Continue(), t, "Continue()")
pc := getRegisters(p, t).PC()
assertNoError(grp.StepInstruction(), t, "StepInstruction()")
assertNoError(grp.StepInstruction(false), t, "StepInstruction()")
if pc == getRegisters(p, t).PC() {
t.Fatal("Could not step a single instruction")
}

@ -36,7 +36,7 @@ func TestSetYMMRegister(t *testing.T) {
0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44}))
assertNoError(grp.StepInstruction(), t, "SetpInstruction")
assertNoError(grp.StepInstruction(false), t, "StepInstruction")
xmm0 := getReg("after")

@ -315,7 +315,7 @@ func TestHalt(t *testing.T) {
})
}
func TestStep(t *testing.T) {
func TestStepInstruction(t *testing.T) {
protest.AllowRecording(t)
withTestProcess("testprog", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
setFunctionBreakpoint(p, t, "main.helloworld")
@ -324,7 +324,7 @@ func TestStep(t *testing.T) {
regs := getRegisters(p, t)
rip := regs.PC()
err := grp.StepInstruction()
err := grp.StepInstruction(false)
assertNoError(err, t, "Step()")
regs = getRegisters(p, t)
@ -334,6 +334,19 @@ func TestStep(t *testing.T) {
})
}
func TestNextInstruction(t *testing.T) {
protest.AllowRecording(t)
withTestProcess("testprog", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
setFileBreakpoint(p, t, fixture.Source, 19)
assertNoError(grp.Continue(), t, "Continue()")
err := grp.StepInstruction(true)
assertNoError(err, t, "Step()")
assertLineNumber(p, t, 20, "next-instruction did not step over call")
})
}
func TestBreakpoint(t *testing.T) {
protest.AllowRecording(t)
withTestProcess("testprog", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
@ -2719,7 +2732,7 @@ func TestStepOnCallPtrInstr(t *testing.T) {
found = true
break
}
assertNoError(grp.StepInstruction(), t, "StepInstruction()")
assertNoError(grp.StepInstruction(false), t, "StepInstruction()")
}
if !found {
@ -3095,7 +3108,7 @@ func TestStepInstructionNoGoroutine(t *testing.T) {
withTestProcess("increment", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
// Call StepInstruction immediately after launching the program, it should
// work even though no goroutine is selected.
assertNoError(grp.StepInstruction(), t, "StepInstruction")
assertNoError(grp.StepInstruction(false), t, "StepInstruction")
})
}

@ -196,7 +196,7 @@ func (grp *TargetGroup) Continue() error {
if err := dbp.ClearSteppingBreakpoints(); err != nil {
return err
}
return grp.StepInstruction()
return grp.StepInstruction(false)
}
} else {
curthread.Common().returnValues = curbp.Breakpoint.returnInfo.Collect(dbp, curthread)
@ -455,7 +455,7 @@ func (grp *TargetGroup) Step() (err error) {
if bpstate := grp.Selected.CurrentThread().Breakpoint(); bpstate.Breakpoint != nil && bpstate.Active && bpstate.SteppingInto && grp.GetDirection() == Backward {
grp.Selected.ClearSteppingBreakpoints()
return grp.StepInstruction()
return grp.StepInstruction(false)
}
return grp.Continue()
@ -567,7 +567,7 @@ func (grp *TargetGroup) StepOut() error {
// one instruction. This method affects only the thread
// associated with the selected goroutine. All other
// threads will remain stopped.
func (grp *TargetGroup) StepInstruction() (err error) {
func (grp *TargetGroup) StepInstruction(skipCalls bool) (err error) {
dbp := grp.Selected
thread := dbp.CurrentThread()
g := dbp.SelectedGoroutine()
@ -586,6 +586,12 @@ func (grp *TargetGroup) StepInstruction() (err error) {
if ok, err := dbp.Valid(); !ok {
return err
}
var isCall bool
instr, err := disassembleCurrentInstruction(dbp, thread, 0)
if err != nil {
return err
}
isCall = len(instr) > 0 && instr[0].IsCall()
err = grp.procgrp.StepInstruction(thread.ThreadID())
if err != nil {
return err
@ -599,6 +605,11 @@ func (grp *TargetGroup) StepInstruction() (err error) {
dbp.selectedGoroutine = tg
}
dbp.StopReason = StopNextFinished
if skipCalls && isCall {
return grp.StepOut()
}
return nil
}

@ -186,7 +186,8 @@ For example:
continue encoding/json.Marshal
`},
{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", "stepi"}, group: runCmds, allowedPrefixes: revPrefix, cmdFn: c.stepInstruction, helpMsg: "Single step a single cpu instruction."},
{aliases: []string{"next-instruction", "ni", "nexti"}, group: runCmds, allowedPrefixes: revPrefix, cmdFn: c.nextInstruction, helpMsg: "Single step a single cpu instruction, skipping function calls."},
{aliases: []string{"next", "n"}, group: runCmds, cmdFn: c.next, allowedPrefixes: revPrefix, helpMsg: `Step over to next source line.
next [count]
@ -1511,24 +1512,34 @@ func (c *Commands) step(t *Term, ctx callContext, args string) error {
var errNotOnFrameZero = errors.New("not on topmost frame")
// stepInstruction implements the step-instruction (stepi) command.
func (c *Commands) stepInstruction(t *Term, ctx callContext, args string) error {
return stepInstruction(t, ctx, c.frame, false)
}
// nextInstruction implements the next-instruction (nexti) command.
func (c *Commands) nextInstruction(t *Term, ctx callContext, args string) error {
return stepInstruction(t, ctx, c.frame, true)
}
func stepInstruction(t *Term, ctx callContext, frame int, skipCalls bool) error {
if err := scopePrefixSwitch(t, ctx); err != nil {
return err
}
if c.frame != 0 {
if frame != 0 {
return errNotOnFrameZero
}
defer t.onStop()
var fn func() (*api.DebuggerState, error)
var fn func(bool) (*api.DebuggerState, error)
if ctx.Prefix == revPrefix {
fn = t.client.ReverseStepInstruction
} else {
fn = t.client.StepInstruction
}
state, err := exitedToError(fn())
state, err := exitedToError(fn(skipCalls))
if err != nil {
printcontextNoState(t)
return err

@ -456,8 +456,12 @@ const (
ReverseStepOut = "reverseStepOut"
// StepInstruction continues for exactly 1 cpu instruction.
StepInstruction = "stepInstruction"
// NextInstruction continues for 1 cpu instruction, skipping over CALL instructions.
NextInstruction = "nextInstruction"
// ReverseStepInstruction reverses execution for exactly 1 cpu instruction.
ReverseStepInstruction = "reverseStepInstruction"
// ReverseNextInstruction reverses execution for 1 cpu instruction, skipping over CALL instructions.
ReverseNextInstruction = "reverseNextInstruction"
// 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.

@ -53,9 +53,9 @@ type Client interface {
Call(goroutineID int64, expr string, unsafe bool) (*api.DebuggerState, error)
// StepInstruction will step a single cpu instruction.
StepInstruction() (*api.DebuggerState, error)
StepInstruction(skipCalls bool) (*api.DebuggerState, error)
// ReverseStepInstruction will reverse step a single cpu instruction.
ReverseStepInstruction() (*api.DebuggerState, error)
ReverseStepInstruction(skipCalls bool) (*api.DebuggerState, error)
// SwitchThread switches the current thread context.
SwitchThread(threadID int) (*api.DebuggerState, error)
// SwitchGoroutine switches the current goroutine (and the current thread as well)

@ -1255,13 +1255,25 @@ func (d *Debugger) Command(command *api.DebuggerCommand, resumeNotify chan struc
if err := d.target.ChangeDirection(proc.Forward); err != nil {
return nil, err
}
err = d.target.StepInstruction()
err = d.target.StepInstruction(false)
case api.ReverseStepInstruction:
d.log.Debug("reverse single stepping")
if err := d.target.ChangeDirection(proc.Backward); err != nil {
return nil, err
}
err = d.target.StepInstruction()
err = d.target.StepInstruction(false)
case api.NextInstruction:
d.log.Debug("single stepping")
if err := d.target.ChangeDirection(proc.Forward); err != nil {
return nil, err
}
err = d.target.StepInstruction(true)
case api.ReverseNextInstruction:
d.log.Debug("reverse single stepping")
if err := d.target.ChangeDirection(proc.Backward); err != nil {
return nil, err
}
err = d.target.StepInstruction(true)
case api.StepOut:
d.log.Debug("step out")
if err := d.target.ChangeDirection(proc.Forward); err != nil {

@ -183,15 +183,23 @@ func (c *RPCClient) Call(goroutineID int64, expr string, unsafe bool) (*api.Debu
return &out.State, err
}
func (c *RPCClient) StepInstruction() (*api.DebuggerState, error) {
func (c *RPCClient) StepInstruction(skipCalls bool) (*api.DebuggerState, error) {
var out CommandOut
err := c.call("Command", api.DebuggerCommand{Name: api.StepInstruction}, &out)
name := api.StepInstruction
if skipCalls {
name = api.NextInstruction
}
err := c.call("Command", api.DebuggerCommand{Name: name}, &out)
return &out.State, err
}
func (c *RPCClient) ReverseStepInstruction() (*api.DebuggerState, error) {
func (c *RPCClient) ReverseStepInstruction(skipCalls bool) (*api.DebuggerState, error) {
var out CommandOut
err := c.call("Command", api.DebuggerCommand{Name: api.ReverseStepInstruction}, &out)
name := api.ReverseStepInstruction
if skipCalls {
name = api.ReverseNextInstruction
}
err := c.call("Command", api.DebuggerCommand{Name: name}, &out)
return &out.State, err
}

@ -1334,7 +1334,7 @@ func TestIssue355(t *testing.T) {
assertErrorOrExited(s, err, t, "Next()")
s, err = c.Step()
assertErrorOrExited(s, err, t, "Step()")
s, err = c.StepInstruction()
s, err = c.StepInstruction(false)
assertErrorOrExited(s, err, t, "StepInstruction()")
s, err = c.SwitchThread(tid)
assertErrorOrExited(s, err, t, "SwitchThread()")
@ -1452,7 +1452,7 @@ func TestDisasm(t *testing.T) {
if count > 20 {
t.Fatal("too many step instructions executed without finding a call instruction")
}
state, err := c.StepInstruction()
state, err := c.StepInstruction(false)
assertNoError(err, t, fmt.Sprintf("StepInstruction() %d", count))
d3, err = c.DisassemblePC(api.EvalScope{GoroutineID: -1}, state.CurrentThread.PC, api.IntelFlavour)
@ -3105,3 +3105,19 @@ func TestClientServer_chanGoroutines(t *testing.T) {
}
})
}
func TestNextInstruction(t *testing.T) {
protest.AllowRecording(t)
withTestClient2("testprog", t, func(c service.Client) {
fp := testProgPath(t, "testprog")
_, err := c.CreateBreakpoint(&api.Breakpoint{File: fp, Line: 19})
state := <-c.Continue()
assertNoError(state.Err, t, "Continue()")
state, err = c.StepInstruction(true)
assertNoError(err, t, "Step()")
if state.CurrentThread.Line != 20 {
t.Fatalf("expected line %d got %d", 20, state.CurrentThread.Line)
}
})
}