*: Show return values on CLI trace

This patch allows the `trace` CLI subcommand to display return values of
a function. Additionally, it will also display information on where the
function exited, which could also be helpful in determining the path
taken during function execution.

Fixes #388
This commit is contained in:
Derek Parker 2018-10-16 08:49:20 -07:00 committed by Alessandro Arzilli
parent 4db9939845
commit 3129aa7330
12 changed files with 156 additions and 23 deletions

@ -397,11 +397,34 @@ func traceCmd(cmd *cobra.Command, args []string) {
return 1
}
for i := range funcs {
_, err = client.CreateBreakpoint(&api.Breakpoint{FunctionName: funcs[i], Tracepoint: true, Line: -1, Stacktrace: traceStackDepth, LoadArgs: &terminal.ShortLoadConfig})
_, err = client.CreateBreakpoint(&api.Breakpoint{
FunctionName: funcs[i],
Tracepoint: true,
Line: -1,
Stacktrace: traceStackDepth,
LoadArgs: &terminal.ShortLoadConfig,
})
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
addrs, err := client.FunctionReturnLocations(funcs[i])
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
for i := range addrs {
_, err = client.CreateBreakpoint(&api.Breakpoint{
Addr: addrs[i],
TraceReturn: true,
Line: -1,
LoadArgs: &terminal.ShortLoadConfig,
})
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
}
}
cmds := terminal.DebugCommands(client)
t := terminal.New(client, nil)

@ -30,7 +30,8 @@ type Breakpoint struct {
Kind BreakpointKind
// Breakpoint information
Tracepoint bool // Tracepoint flag
Tracepoint bool // Tracepoint flag
TraceReturn bool
Goroutine bool // Retrieve goroutine information
Stacktrace int // Number of stack frames to retrieve
Variables []string // Variables to evaluate
@ -45,7 +46,7 @@ type Breakpoint struct {
// Next uses NextDeferBreakpoints for the breakpoint it sets on the
// deferred function, DeferReturns is populated with the
// addresses of calls to runtime.deferreturn in the current
// function. This insures that the breakpoint on the deferred
// function. This ensures that the breakpoint on the deferred
// function only triggers on panic or on the defer call to
// the function, not when the function is called directly
DeferReturns []uint64

@ -65,6 +65,14 @@ func (inst *AsmInstruction) IsCall() bool {
return inst.Inst.Op == x86asm.CALL || inst.Inst.Op == x86asm.LCALL
}
// IsRet returns true if the instruction is a RET or LRET instruction.
func (inst *AsmInstruction) IsRet() bool {
if inst.Inst == nil {
return false
}
return inst.Inst.Op == x86asm.RET || inst.Inst.Op == x86asm.LRET
}
func resolveCallArg(inst *archInst, currentGoroutine bool, regs Registers, mem MemoryReadWriter, bininfo *BinaryInfo) *Location {
if inst.Op != x86asm.CALL && inst.Op != x86asm.LCALL {
return nil

@ -88,6 +88,33 @@ func FindFunctionLocation(p Process, funcName string, firstLine bool, lineOffset
return origfn.Entry, nil
}
// FunctionReturnLocations will return a list of addresses corresponding
// to 'ret' or 'call runtime.deferreturn'.
func FunctionReturnLocations(p Process, funcName string) ([]uint64, error) {
const deferReturn = "runtime.deferreturn"
g := p.SelectedGoroutine()
fn, ok := p.BinInfo().LookupFunc[funcName]
if !ok {
return nil, fmt.Errorf("unable to find function %s", funcName)
}
instructions, err := Disassemble(p, g, fn.Entry, fn.End)
if err != nil {
return nil, err
}
var addrs []uint64
for _, instruction := range instructions {
if instruction.IsRet() {
addrs = append(addrs, instruction.Loc.PC)
}
}
addrs = append(addrs, findDeferReturnCalls(instructions)...)
return addrs, nil
}
// Next continues execution until the next source line.
func Next(dbp Process) (err error) {
if _, err := dbp.Valid(); err != nil {
@ -184,7 +211,8 @@ func Continue(dbp Process) error {
return conditionErrors(threads)
}
case curbp.Active && curbp.Internal:
if curbp.Kind == StepBreakpoint {
switch curbp.Kind {
case StepBreakpoint:
// See description of proc.(*Process).next for the meaning of StepBreakpoints
if err := conditionErrors(threads); err != nil {
return err
@ -204,7 +232,7 @@ func Continue(dbp Process) error {
if err = setStepIntoBreakpoint(dbp, text, SameGoroutineCondition(dbp.SelectedGoroutine())); err != nil {
return err
}
} else {
default:
curthread.Common().returnValues = curbp.Breakpoint.returnInfo.Collect(curthread)
if err := dbp.ClearInternalBreakpoints(); err != nil {
return err

@ -225,15 +225,7 @@ func next(dbp Process, stepInto, inlinedStepOut bool) error {
}
if !csource {
deferreturns := []uint64{}
// Find all runtime.deferreturn locations in the function
// See documentation of Breakpoint.DeferCond for why this is necessary
for _, instr := range text {
if instr.IsCall() && instr.DestLoc != nil && instr.DestLoc.Fn != nil && instr.DestLoc.Fn.Name == "runtime.deferreturn" {
deferreturns = append(deferreturns, instr.Loc.PC)
}
}
deferreturns := findDeferReturnCalls(text)
// Set breakpoint on the most recently deferred function (if any)
var deferpc uint64
@ -333,6 +325,20 @@ func next(dbp Process, stepInto, inlinedStepOut bool) error {
return nil
}
func findDeferReturnCalls(text []AsmInstruction) []uint64 {
const deferreturn = "runtime.deferreturn"
deferreturns := []uint64{}
// Find all runtime.deferreturn locations in the function
// See documentation of Breakpoint.DeferCond for why this is necessary
for _, instr := range text {
if instr.IsCall() && instr.DestLoc != nil && instr.DestLoc.Fn != nil && instr.DestLoc.Fn.Name == deferreturn {
deferreturns = append(deferreturns, instr.Loc.PC)
}
}
return deferreturns
}
// Removes instructions belonging to inlined calls of topframe from pcs.
// If includeCurrentFn is true it will also remove all instructions
// belonging to the current function.

@ -405,13 +405,13 @@ func (scope *EvalScope) PtrSize() int {
return scope.BinInfo.Arch.PtrSize()
}
// NoGError returned when a G could not be found
// ErrNoGoroutine returned when a G could not be found
// for a specific thread.
type NoGError struct {
type ErrNoGoroutine struct {
tid int
}
func (ng NoGError) Error() string {
func (ng ErrNoGoroutine) Error() string {
return fmt.Sprintf("no G executing on thread %d", ng.tid)
}
@ -433,7 +433,7 @@ func (v *Variable) parseG() (*G, error) {
if thread, ok := mem.(Thread); ok {
id = thread.ThreadID()
}
return nil, NoGError{tid: id}
return nil, ErrNoGoroutine{tid: id}
}
for {
if _, isptr := v.RealType.(*godwarf.PtrType); !isptr {

@ -1702,7 +1702,14 @@ func printcontextThread(t *Term, th *api.Thread) {
if th.BreakpointInfo != nil && th.Breakpoint.LoadArgs != nil && *th.Breakpoint.LoadArgs == ShortLoadConfig {
var arg []string
for _, ar := range th.BreakpointInfo.Arguments {
arg = append(arg, ar.SinglelineString())
// For AI compatibility return values are included in the
// argument list. This is a relic of the dark ages when the
// Go debug information did not distinguish between the two.
// Filter them out here instead, so during trace operations
// they are not printed as an argument.
if (ar.Flags & api.VariableArgument) != 0 {
arg = append(arg, ar.SinglelineString())
}
}
args = strings.Join(arg, ", ")
}

@ -23,6 +23,7 @@ func ConvertBreakpoint(bp *proc.Breakpoint) *Breakpoint {
Line: bp.Line,
Addr: bp.Addr,
Tracepoint: bp.Tracepoint,
TraceReturn: bp.TraceReturn,
Stacktrace: bp.Stacktrace,
Goroutine: bp.Goroutine,
Variables: bp.Variables,

@ -59,8 +59,11 @@ type Breakpoint struct {
// Breakpoint condition
Cond string
// tracepoint flag
// Tracepoint flag, signifying this is a tracepoint.
Tracepoint bool `json:"continue"`
// TraceReturn flag signifying this is a breakpoint set at a return
// statement in a traced function.
TraceReturn bool `json:"traceReturn"`
// retrieve goroutine information
Goroutine bool `json:"goroutine"`
// number of stack frames to retrieve
@ -197,11 +200,11 @@ const (
// that may outlive the stack frame are allocated on the heap instead and
// only the address is recorded on the stack. These variables will be
// marked with this flag.
VariableEscaped = VariableFlags(proc.VariableEscaped)
VariableEscaped = (1 << iota)
// VariableShadowed is set for local variables that are shadowed by a
// variable with the same name in another scope
VariableShadowed = VariableFlags(proc.VariableShadowed)
VariableShadowed
// VariableConstant means this variable is a constant value
VariableConstant

@ -195,6 +195,14 @@ func (d *Debugger) LastModified() time.Time {
return d.target.BinInfo().LastModified()
}
// FunctionReturnLocations returns all return locations
// for the given function. See the documentation for the
// function of the same name within the `proc` package for
// more information.
func (d *Debugger) FunctionReturnLocations(fnName string) ([]uint64, error) {
return proc.FunctionReturnLocations(d.target, fnName)
}
// Detach detaches from the target process.
// If `kill` is true we will kill the process after
// detaching.
@ -348,6 +356,8 @@ func (d *Debugger) CreateBreakpoint(requestedBp *api.Breakpoint) (*api.Breakpoin
}
switch {
case requestedBp.TraceReturn:
addr = requestedBp.Addr
case len(requestedBp.File) > 0:
fileName := requestedBp.File
if runtime.GOOS == "windows" {
@ -414,6 +424,7 @@ func (d *Debugger) CancelNext() error {
func copyBreakpointInfo(bp *proc.Breakpoint, requested *api.Breakpoint) (err error) {
bp.Name = requested.Name
bp.Tracepoint = requested.Tracepoint
bp.TraceReturn = requested.TraceReturn
bp.Goroutine = requested.Goroutine
bp.Stacktrace = requested.Stacktrace
bp.Variables = requested.Variables
@ -617,6 +628,15 @@ func (d *Debugger) Command(command *api.DebuggerCommand) (*api.DebuggerState, er
if withBreakpointInfo {
err = d.collectBreakpointInformation(state)
}
for _, th := range state.Threads {
if th.Breakpoint != nil && th.Breakpoint.TraceReturn {
for _, v := range th.BreakpointInfo.Arguments {
if (v.Flags & api.VariableReturnArgument) != 0 {
th.ReturnValues = append(th.ReturnValues, v)
}
}
}
}
return state, err
}

@ -117,7 +117,7 @@ func (c *RPCClient) continueDir(cmd string) <-chan *api.DebuggerState {
for i := range state.Threads {
if state.Threads[i].Breakpoint != nil {
isbreakpoint = true
istracepoint = istracepoint && state.Threads[i].Breakpoint.Tracepoint
istracepoint = istracepoint && (state.Threads[i].Breakpoint.Tracepoint || state.Threads[i].Breakpoint.TraceReturn)
}
}
@ -375,6 +375,12 @@ func (c *RPCClient) SetReturnValuesLoadConfig(cfg *api.LoadConfig) {
c.retValLoadCfg = cfg
}
func (c *RPCClient) FunctionReturnLocations(fnName string) ([]uint64, error) {
var out FunctionReturnLocationsOut
err := c.call("FunctionReturnLocations", FunctionReturnLocationsIn{fnName}, &out)
return out.Addrs, err
}
func (c *RPCClient) IsMulticlient() bool {
var out IsMulticlientOut
c.call("IsMulticlient", IsMulticlientIn{}, &out)

@ -655,3 +655,33 @@ func (s *RPCServer) IsMulticlient(arg IsMulticlientIn, out *IsMulticlientOut) er
}
return nil
}
// FunctionReturnLocationsIn holds arguments for the
// FunctionReturnLocationsRPC call. It holds the name of
// the function for which all return locations should be
// given.
type FunctionReturnLocationsIn struct {
// FnName is the name of the function for which all
// return locations should be given.
FnName string
}
// FunctionReturnLocationsOut holds the result of the FunctionReturnLocations
// RPC call. It provides the list of addresses that the given function returns,
// for example with a `RET` instruction or `CALL runtime.deferreturn`.
type FunctionReturnLocationsOut struct {
// Addrs is the list of all locations where the given function returns.
Addrs []uint64
}
// FunctionReturnLocations is the implements the client call of the same name. Look at client documentation for more information.
func (s *RPCServer) FunctionReturnLocations(in FunctionReturnLocationsIn, out *FunctionReturnLocationsOut) error {
addrs, err := s.debugger.FunctionReturnLocations(in.FnName)
if err != nil {
return err
}
*out = FunctionReturnLocationsOut{
Addrs: addrs,
}
return nil
}