*: 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:
parent
4db9939845
commit
3129aa7330
@ -397,11 +397,34 @@ func traceCmd(cmd *cobra.Command, args []string) {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
for i := range funcs {
|
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 {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, err)
|
fmt.Fprintln(os.Stderr, err)
|
||||||
return 1
|
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)
|
cmds := terminal.DebugCommands(client)
|
||||||
t := terminal.New(client, nil)
|
t := terminal.New(client, nil)
|
||||||
|
@ -30,7 +30,8 @@ type Breakpoint struct {
|
|||||||
Kind BreakpointKind
|
Kind BreakpointKind
|
||||||
|
|
||||||
// Breakpoint information
|
// Breakpoint information
|
||||||
Tracepoint bool // Tracepoint flag
|
Tracepoint bool // Tracepoint flag
|
||||||
|
TraceReturn bool
|
||||||
Goroutine bool // Retrieve goroutine information
|
Goroutine bool // Retrieve goroutine information
|
||||||
Stacktrace int // Number of stack frames to retrieve
|
Stacktrace int // Number of stack frames to retrieve
|
||||||
Variables []string // Variables to evaluate
|
Variables []string // Variables to evaluate
|
||||||
@ -45,7 +46,7 @@ type Breakpoint struct {
|
|||||||
// Next uses NextDeferBreakpoints for the breakpoint it sets on the
|
// Next uses NextDeferBreakpoints for the breakpoint it sets on the
|
||||||
// deferred function, DeferReturns is populated with the
|
// deferred function, DeferReturns is populated with the
|
||||||
// addresses of calls to runtime.deferreturn in the current
|
// 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
|
// function only triggers on panic or on the defer call to
|
||||||
// the function, not when the function is called directly
|
// the function, not when the function is called directly
|
||||||
DeferReturns []uint64
|
DeferReturns []uint64
|
||||||
|
@ -65,6 +65,14 @@ func (inst *AsmInstruction) IsCall() bool {
|
|||||||
return inst.Inst.Op == x86asm.CALL || inst.Inst.Op == x86asm.LCALL
|
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 {
|
func resolveCallArg(inst *archInst, currentGoroutine bool, regs Registers, mem MemoryReadWriter, bininfo *BinaryInfo) *Location {
|
||||||
if inst.Op != x86asm.CALL && inst.Op != x86asm.LCALL {
|
if inst.Op != x86asm.CALL && inst.Op != x86asm.LCALL {
|
||||||
return nil
|
return nil
|
||||||
|
@ -88,6 +88,33 @@ func FindFunctionLocation(p Process, funcName string, firstLine bool, lineOffset
|
|||||||
return origfn.Entry, nil
|
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.
|
// Next continues execution until the next source line.
|
||||||
func Next(dbp Process) (err error) {
|
func Next(dbp Process) (err error) {
|
||||||
if _, err := dbp.Valid(); err != nil {
|
if _, err := dbp.Valid(); err != nil {
|
||||||
@ -184,7 +211,8 @@ func Continue(dbp Process) error {
|
|||||||
return conditionErrors(threads)
|
return conditionErrors(threads)
|
||||||
}
|
}
|
||||||
case curbp.Active && curbp.Internal:
|
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
|
// See description of proc.(*Process).next for the meaning of StepBreakpoints
|
||||||
if err := conditionErrors(threads); err != nil {
|
if err := conditionErrors(threads); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -204,7 +232,7 @@ func Continue(dbp Process) error {
|
|||||||
if err = setStepIntoBreakpoint(dbp, text, SameGoroutineCondition(dbp.SelectedGoroutine())); err != nil {
|
if err = setStepIntoBreakpoint(dbp, text, SameGoroutineCondition(dbp.SelectedGoroutine())); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
default:
|
||||||
curthread.Common().returnValues = curbp.Breakpoint.returnInfo.Collect(curthread)
|
curthread.Common().returnValues = curbp.Breakpoint.returnInfo.Collect(curthread)
|
||||||
if err := dbp.ClearInternalBreakpoints(); err != nil {
|
if err := dbp.ClearInternalBreakpoints(); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -225,15 +225,7 @@ func next(dbp Process, stepInto, inlinedStepOut bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !csource {
|
if !csource {
|
||||||
deferreturns := []uint64{}
|
deferreturns := findDeferReturnCalls(text)
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set breakpoint on the most recently deferred function (if any)
|
// Set breakpoint on the most recently deferred function (if any)
|
||||||
var deferpc uint64
|
var deferpc uint64
|
||||||
@ -333,6 +325,20 @@ func next(dbp Process, stepInto, inlinedStepOut bool) error {
|
|||||||
return nil
|
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.
|
// Removes instructions belonging to inlined calls of topframe from pcs.
|
||||||
// If includeCurrentFn is true it will also remove all instructions
|
// If includeCurrentFn is true it will also remove all instructions
|
||||||
// belonging to the current function.
|
// belonging to the current function.
|
||||||
|
@ -405,13 +405,13 @@ func (scope *EvalScope) PtrSize() int {
|
|||||||
return scope.BinInfo.Arch.PtrSize()
|
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.
|
// for a specific thread.
|
||||||
type NoGError struct {
|
type ErrNoGoroutine struct {
|
||||||
tid int
|
tid int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ng NoGError) Error() string {
|
func (ng ErrNoGoroutine) Error() string {
|
||||||
return fmt.Sprintf("no G executing on thread %d", ng.tid)
|
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 {
|
if thread, ok := mem.(Thread); ok {
|
||||||
id = thread.ThreadID()
|
id = thread.ThreadID()
|
||||||
}
|
}
|
||||||
return nil, NoGError{tid: id}
|
return nil, ErrNoGoroutine{tid: id}
|
||||||
}
|
}
|
||||||
for {
|
for {
|
||||||
if _, isptr := v.RealType.(*godwarf.PtrType); !isptr {
|
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 {
|
if th.BreakpointInfo != nil && th.Breakpoint.LoadArgs != nil && *th.Breakpoint.LoadArgs == ShortLoadConfig {
|
||||||
var arg []string
|
var arg []string
|
||||||
for _, ar := range th.BreakpointInfo.Arguments {
|
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, ", ")
|
args = strings.Join(arg, ", ")
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ func ConvertBreakpoint(bp *proc.Breakpoint) *Breakpoint {
|
|||||||
Line: bp.Line,
|
Line: bp.Line,
|
||||||
Addr: bp.Addr,
|
Addr: bp.Addr,
|
||||||
Tracepoint: bp.Tracepoint,
|
Tracepoint: bp.Tracepoint,
|
||||||
|
TraceReturn: bp.TraceReturn,
|
||||||
Stacktrace: bp.Stacktrace,
|
Stacktrace: bp.Stacktrace,
|
||||||
Goroutine: bp.Goroutine,
|
Goroutine: bp.Goroutine,
|
||||||
Variables: bp.Variables,
|
Variables: bp.Variables,
|
||||||
|
@ -59,8 +59,11 @@ type Breakpoint struct {
|
|||||||
// Breakpoint condition
|
// Breakpoint condition
|
||||||
Cond string
|
Cond string
|
||||||
|
|
||||||
// tracepoint flag
|
// Tracepoint flag, signifying this is a tracepoint.
|
||||||
Tracepoint bool `json:"continue"`
|
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
|
// retrieve goroutine information
|
||||||
Goroutine bool `json:"goroutine"`
|
Goroutine bool `json:"goroutine"`
|
||||||
// number of stack frames to retrieve
|
// number of stack frames to retrieve
|
||||||
@ -197,11 +200,11 @@ const (
|
|||||||
// that may outlive the stack frame are allocated on the heap instead and
|
// 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
|
// only the address is recorded on the stack. These variables will be
|
||||||
// marked with this flag.
|
// marked with this flag.
|
||||||
VariableEscaped = VariableFlags(proc.VariableEscaped)
|
VariableEscaped = (1 << iota)
|
||||||
|
|
||||||
// VariableShadowed is set for local variables that are shadowed by a
|
// VariableShadowed is set for local variables that are shadowed by a
|
||||||
// variable with the same name in another scope
|
// variable with the same name in another scope
|
||||||
VariableShadowed = VariableFlags(proc.VariableShadowed)
|
VariableShadowed
|
||||||
|
|
||||||
// VariableConstant means this variable is a constant value
|
// VariableConstant means this variable is a constant value
|
||||||
VariableConstant
|
VariableConstant
|
||||||
|
@ -195,6 +195,14 @@ func (d *Debugger) LastModified() time.Time {
|
|||||||
return d.target.BinInfo().LastModified()
|
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.
|
// Detach detaches from the target process.
|
||||||
// If `kill` is true we will kill the process after
|
// If `kill` is true we will kill the process after
|
||||||
// detaching.
|
// detaching.
|
||||||
@ -348,6 +356,8 @@ func (d *Debugger) CreateBreakpoint(requestedBp *api.Breakpoint) (*api.Breakpoin
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
|
case requestedBp.TraceReturn:
|
||||||
|
addr = requestedBp.Addr
|
||||||
case len(requestedBp.File) > 0:
|
case len(requestedBp.File) > 0:
|
||||||
fileName := requestedBp.File
|
fileName := requestedBp.File
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
@ -414,6 +424,7 @@ func (d *Debugger) CancelNext() error {
|
|||||||
func copyBreakpointInfo(bp *proc.Breakpoint, requested *api.Breakpoint) (err error) {
|
func copyBreakpointInfo(bp *proc.Breakpoint, requested *api.Breakpoint) (err error) {
|
||||||
bp.Name = requested.Name
|
bp.Name = requested.Name
|
||||||
bp.Tracepoint = requested.Tracepoint
|
bp.Tracepoint = requested.Tracepoint
|
||||||
|
bp.TraceReturn = requested.TraceReturn
|
||||||
bp.Goroutine = requested.Goroutine
|
bp.Goroutine = requested.Goroutine
|
||||||
bp.Stacktrace = requested.Stacktrace
|
bp.Stacktrace = requested.Stacktrace
|
||||||
bp.Variables = requested.Variables
|
bp.Variables = requested.Variables
|
||||||
@ -617,6 +628,15 @@ func (d *Debugger) Command(command *api.DebuggerCommand) (*api.DebuggerState, er
|
|||||||
if withBreakpointInfo {
|
if withBreakpointInfo {
|
||||||
err = d.collectBreakpointInformation(state)
|
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
|
return state, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,7 +117,7 @@ func (c *RPCClient) continueDir(cmd string) <-chan *api.DebuggerState {
|
|||||||
for i := range state.Threads {
|
for i := range state.Threads {
|
||||||
if state.Threads[i].Breakpoint != nil {
|
if state.Threads[i].Breakpoint != nil {
|
||||||
isbreakpoint = true
|
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
|
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 {
|
func (c *RPCClient) IsMulticlient() bool {
|
||||||
var out IsMulticlientOut
|
var out IsMulticlientOut
|
||||||
c.call("IsMulticlient", IsMulticlientIn{}, &out)
|
c.call("IsMulticlient", IsMulticlientIn{}, &out)
|
||||||
|
@ -655,3 +655,33 @@ func (s *RPCServer) IsMulticlient(arg IsMulticlientIn, out *IsMulticlientOut) er
|
|||||||
}
|
}
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user