Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
9cae217
code to extract defer function address
archanaravindar Dec 18, 2024
f42314d
added code to include defer function into queue in call back
archanaravindar Dec 19, 2024
05018a9
added code to create tracepoint on function called in defer and also …
archanaravindar Jan 9, 2025
22d91de
added root function name,stacktrace for depth in printing trace
archanaravindar Jan 13, 2025
da132a5
added code to trace descendants of defer calls too
archanaravindar Jan 13, 2025
b74bea6
more cleanup
archanaravindar Jan 28, 2025
fe50fa5
refactor createbreakpoint function to avoid locks
archanaravindar Feb 7, 2025
7edacd3
Added depth check inside callback
archanaravindar Feb 24, 2025
a8277d7
added dynamic depth computation inside callback
archanaravindar Mar 5, 2025
6de0b25
factor FunctionReturnLocations as well to avoid lock and unlock code
archanaravindar Mar 17, 2025
bc9c591
More refactoring to remove lock and unlock, added tests to cover
archanaravindar Mar 18, 2025
24ff8ca
Add new tests, correct expected output to match current output
archanaravindar Mar 18, 2025
b0dbb50
port defer call function recognition mechanism across arches
archanaravindar Mar 21, 2025
9911fc4
code cleanup
archanaravindar Mar 21, 2025
52ccd7e
naming convention
archanaravindar Mar 21, 2025
b62b9fb
add comments to explain changes
archanaravindar Apr 2, 2025
8daef30
code cleanup
archanaravindar Apr 8, 2025
3c35d32
Added test programs to test tracing of defer functions
archanaravindar Apr 8, 2025
e00ea91
fix staticcheck errors
archanaravindar Apr 10, 2025
0876835
port trace follow calls for defer functions on 386
archanaravindar Apr 10, 2025
f2d80a7
Addressing review comments
archanaravindar Apr 22, 2025
42dda5f
Address review comments-2
archanaravindar Apr 22, 2025
321fe9f
generalize to other kinds of dynamic dispatch
archanaravindar May 16, 2025
671bf28
Added a line on supporting dynamic dispatch in documentation
archanaravindar May 20, 2025
ae291be
Formatting
archanaravindar May 20, 2025
8e8ef76
Addressed review comments by Derek
archanaravindar Jul 29, 2025
0886aa0
Removed tests from cmd/dlv/dlv_test.go and merged into service/test/i…
archanaravindar Jul 30, 2025
b2a43eb
correction of static check error
archanaravindar Jul 30, 2025
8300167
continued addressing comments by Derek
archanaravindar Jul 30, 2025
a97d40b
Remove RootFuncName field in dynbp as its not needed
archanaravindar Aug 1, 2025
f7a8eea
Use disassemble instead of calling DynamicCallReg field to get the de…
archanaravindar Aug 30, 2025
7db5cb7
remove all references to DynamicCallReg
archanaravindar Aug 30, 2025
044e658
Addressing review comments on Callback field, using BreakpointExistsE…
archanaravindar Sep 15, 2025
7241250
Addressing review comments
archanaravindar Oct 16, 2025
2d308d9
Addressing more review comments around createfunctiontracepoint
archanaravindar Oct 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Documentation/usage/dlv_trace.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ dlv trace [package] regexp [flags]
```
--ebpf Trace using eBPF (experimental).
-e, --exec string Binary file to exec and trace.
--follow-calls int Trace all children of the function to the required depth
--follow-calls int Trace all children of the function to the required depth. Trace also supports defer functions and cases where functions are dynamically returned and passed as parameters.
-h, --help help for trace
--output string Output path for the binary.
-p, --pid int Pid to attach to.
Expand Down
75 changes: 75 additions & 0 deletions _fixtures/testtracefns.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,88 @@ func F4() {
panic("blah")
}

var intc, intd int

func swap() {
defer func() {
intc += 100
}()
temp := intc
intc = intd
intd = temp
}

func unnamedDefer() {
intc = -100
intd = 100
swap()
fmt.Println(intc, intd)
}
func formula(op string) func(int, int) int {
var calc func(int, int) int
if op == "add" {
calc = func(m int, n int) int {
res := m + n
return res
}
} else if op == "mul" {
calc = func(m int, n int) int {
res := m * n
return res
}
}
return calc
}

func op() int {
calc := formula("add")
res := calc(10, 20)
return res
}

func assign(bar func()) {
bar()
}
func testfunc() {
intc = 10
intd = 20
}

func dyn() {
intc = 0
intd = 0
assign(testfunc)
}

func outer() {
intc = 40
defer swap()
}
func nestDefer() {
defer outer()
}

func namedDeferLoop(n int) {
for i := 0; i < n; i++ {
defer testfunc()
}
temp := intc
intc = intd
intd = temp
}
func main() {
j := 0
j += A(2)

j += first(6)
j += callme(2)
fmt.Println(j)
unnamedDefer()
nestDefer()
namedDeferLoop(2)
F0()
ans := op()
fmt.Println(ans)
dyn()

}
2 changes: 1 addition & 1 deletion cmd/dlv/cmds/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ only see the output of the trace operations you can redirect stdout.`,
must(traceCommand.RegisterFlagCompletionFunc("stack", cobra.NoFileCompletions))
traceCommand.Flags().String("output", "", "Output path for the binary.")
must(traceCommand.MarkFlagFilename("output"))
traceCommand.Flags().IntVarP(&traceFollowCalls, "follow-calls", "", 0, "Trace all children of the function to the required depth")
traceCommand.Flags().IntVarP(&traceFollowCalls, "follow-calls", "", 0, "Trace all children of the function to the required depth. Trace also supports defer functions and cases where functions are dynamically returned and passed as parameters.")
rootCommand.AddCommand(traceCommand)

coreCommand := &cobra.Command{
Expand Down
5 changes: 5 additions & 0 deletions pkg/proc/breakpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@ type Breaklet struct {
watchpoint *Breakpoint
}

// SetCallback sets the call back field, this was primarily added to prevent exporting callback field
func (b *Breaklet) SetCallback(callback func(th Thread, p *Target) (bool, error)) {
b.callback = callback
}

// BreakpointKind determines the behavior of delve when the
// breakpoint is reached.
type BreakpointKind uint16
Expand Down
170 changes: 160 additions & 10 deletions service/debugger/debugger.go
Original file line number Diff line number Diff line change
Expand Up @@ -409,13 +409,10 @@ func (d *Debugger) LastModified() time.Time {
return d.target.Selected.BinInfo().LastModified()
}

// FunctionReturnLocations returns all return locations
// for the given function, a list of addresses corresponding
// to 'ret' or 'call runtime.deferreturn'.
func (d *Debugger) FunctionReturnLocations(fnName string) ([]uint64, error) {
d.targetMutex.Lock()
defer d.targetMutex.Unlock()

// functionReturnLocationsInternal is same as FunctionReturnLocations
// except that it does not have a lock and unlock as its called from
// within the callback which has already acquired a lock.
func (d *Debugger) functionReturnLocationsInternal(fnName string) ([]uint64, error) {
if len(d.target.Targets()) > 1 {
return nil, ErrNotImplementedWithMultitarget
}
Expand Down Expand Up @@ -454,6 +451,15 @@ func (d *Debugger) FunctionReturnLocations(fnName string) ([]uint64, error) {
return addrs, nil
}

// FunctionReturnLocations returns all return locations
// for the given function, a list of addresses corresponding
// to 'ret' or 'call runtime.deferreturn'.
func (d *Debugger) FunctionReturnLocations(fnName string) ([]uint64, error) {
d.targetMutex.Lock()
defer d.targetMutex.Unlock()
return d.functionReturnLocationsInternal(fnName)
}

// Detach detaches from the target process.
// If `kill` is true we will kill the process after
// detaching.
Expand Down Expand Up @@ -1372,7 +1378,7 @@ func (d *Debugger) Functions(filter string, followCalls int) ([]string, error) {
for _, f := range t.BinInfo().Functions {
if regex.MatchString(f.Name) {
if followCalls > 0 {
newfuncs, err := traverse(t, &f, 1, followCalls)
newfuncs, err := d.traverse(t, &f, 1, followCalls, filter)
if err != nil {
return nil, fmt.Errorf("traverse failed with error %w", err)
}
Expand All @@ -1388,12 +1394,13 @@ func (d *Debugger) Functions(filter string, followCalls int) ([]string, error) {
return funcs, nil
}

func traverse(t proc.ValidTargets, f *proc.Function, depth int, followCalls int) ([]string, error) {
func (d *Debugger) traverse(t proc.ValidTargets, f *proc.Function, depth int, followCalls int, rootstr string) ([]string, error) {
type TraceFunc struct {
Func *proc.Function
Depth int
visited bool
}

type TraceFuncptr *TraceFunc

TraceMap := make(map[string]TraceFuncptr)
Expand All @@ -1409,7 +1416,7 @@ func traverse(t proc.ValidTargets, f *proc.Function, depth int, followCalls int)
parent := queue[0]
queue = queue[1:]
if parent == nil {
panic("attempting to open file Delve cannot parse")
panic("queue has a nil node, cannot traverse!")
}
if parent.Depth > followCalls {
continue
Expand All @@ -1427,10 +1434,89 @@ func traverse(t proc.ValidTargets, f *proc.Function, depth int, followCalls int)
}
f := parent.Func
text, err := proc.Disassemble(t.Memory(), nil, t.Breakpoints(), t.BinInfo(), f.Entry, f.End)

if err != nil {
return nil, fmt.Errorf("disassemble failed with error %w", err)
}
for _, instr := range text {
// Dynamic functions need to be handled specially as their destination location
// is not known statically, hence its required to put a breakpoint in order to
// acquire the address of the function at runtime and we do this via a
// call back mechanism
if instr.IsCall() && instr.DestLoc == nil {
dynbp, err := t.SetBreakpoint(0, instr.Loc.PC, proc.NextBreakpoint, nil)
if err != nil {
return nil, fmt.Errorf("error setting breakpoint inside deferreturn")
}
dynCallback := func(th proc.Thread, tgt *proc.Target) (bool, error) {
// TODO(optimization): Consider using an iterator to avoid materializing
// the full stack when we only need frames up to the root function
rawlocs, err := proc.ThreadStacktrace(tgt, tgt.CurrentThread(), followCalls+2)
if err != nil {
return false, fmt.Errorf("thread stack trace returned error")
}
// Since the dynamic function is known only at runtime, the depth is likewise
// calculated by referring to the stack and the mechanism is similar to that
// used in pkg/terminal/command.go:printTraceOutput
rootindex := -1
for i := len(rawlocs) - 1; i >= 0; i-- {
if rawlocs[i].Call.Fn.Name == rootstr {
if rootindex == -1 {
rootindex = i
break
}
}
}
sdepth := rootindex + 1

if sdepth+1 > followCalls {
return false, nil
}
regs, err := th.Registers()
if err != nil {
return false, fmt.Errorf("registers inside callback returned err")

}
// Disassemble the instruction at the current PC to get the call destination
pc := instr.Loc.PC
maxInstLen := uint64(tgt.BinInfo().Arch.MaxInstructionLength())
disasm, err := proc.Disassemble(t.Memory(), regs, t.Breakpoints(), tgt.BinInfo(), pc, pc+maxInstLen)
if err != nil {
return false, fmt.Errorf("failed to disassemble instruction: %w", err)
}

// Extract address from the decoded instruction's destination location
var addr uint64
if len(disasm) > 0 && disasm[0].DestLoc != nil {
addr = disasm[0].DestLoc.PC
} else {
return false, fmt.Errorf("failed to extract call destination from instruction at PC %#x", pc)
}
fn := tgt.BinInfo().PCToFunc(addr)
if fn == nil {
return false, fmt.Errorf("PCToFunc returned nil")
}
err = createFunctionTracepoints(d, fn.Name, rootstr, followCalls)
if err != nil {
return false, fmt.Errorf("error creating tracepoint in function %s", fn.Name)
}
dynchildren, err := d.traverse(t, fn, sdepth+1, followCalls, rootstr)
if err != nil {
return false, fmt.Errorf("error calling traverse on dynamic children")
}
for _, child := range dynchildren {
err := createFunctionTracepoints(d, child, rootstr, followCalls)
if err != nil {
return false, fmt.Errorf("error creating tracepoint in function %s", child)
}
}
return false, nil
}
for _, dynBrklet := range dynbp.Breaklets {
dynBrklet.SetCallback(dynCallback)
}
}

if instr.IsCall() && instr.DestLoc != nil && instr.DestLoc.Fn != nil {
cf := instr.DestLoc.Fn
if (strings.HasPrefix(cf.Name, "runtime.") || strings.HasPrefix(cf.Name, "runtime/internal")) && cf.Name != "runtime.deferreturn" && cf.Name != "runtime.gorecover" && cf.Name != "runtime.gopanic" {
Expand All @@ -1449,6 +1535,70 @@ func traverse(t proc.ValidTargets, f *proc.Function, depth int, followCalls int)
return funcs, nil
}

// createFunctionTracepoints is a way to create a trace point late in the cycle but just in time that
// it can be included in the trace output as in the case of dynamic functions, we get to know the
// functions that are being called from deferreturn quite late in execution. This might create multiple
// tracepoints
func createFunctionTracepoints(d *Debugger, fname string, rootstr string, followCalls int) error {

// Helper function to create breakpoints for tracepoints
// Ignore BreakpointExistsError since duplicate tracepoints
// are expected during dynamic function discovery
createBrkForTracepoint := func(lbp *proc.LogicalBreakpoint) error {
d.breakpointIDCounter++
lbp.LogicalID = d.breakpointIDCounter
lbp.HitCount = make(map[int64]uint64)
d.target.LogicalBreakpoints[lbp.LogicalID] = lbp
err := d.target.SetBreakpointEnabled(lbp, true)
if err != nil {
delete(d.target.LogicalBreakpoints, lbp.LogicalID)
// Silently ignore BreakpointExistsError - this is expected when
// creating tracepoints for functions that may already have them
if _, exists := err.(proc.BreakpointExistsError); exists {
return nil
}
return err
}
return nil
}

// Create tracepoint for function entry
lbp := &proc.LogicalBreakpoint{
Set: proc.SetBreakpoint{
FunctionName: fname,
},
Tracepoint: true,
RootFuncName: rootstr,
Stacktrace: 20,
TraceFollowCalls: followCalls,
}

err := createBrkForTracepoint(lbp)
if err != nil {
return fmt.Errorf("error creating breakpoint at function %s: %w", fname, err)
}

// Create tracepoints for function return locations
raddrs, _ := d.functionReturnLocationsInternal(fname)
for i := range raddrs {
retLbp := &proc.LogicalBreakpoint{
Set: proc.SetBreakpoint{
PidAddrs: []proc.PidAddr{{Pid: d.target.Selected.Pid(), Addr: raddrs[i]}},
},
TraceReturn: true,
RootFuncName: rootstr,
Stacktrace: 20,
TraceFollowCalls: followCalls,
}

err := createBrkForTracepoint(retLbp)
if err != nil {
return fmt.Errorf("error creating breakpoint at function return %s: %w", fname, err)
}
}
return nil
}

// Types returns all type information in the binary.
func (d *Debugger) Types(filter string) ([]string, error) {
d.targetMutex.Lock()
Expand Down
Loading