Skip to content

Commit 35ccc94

Browse files
Add support to trace defer function calls under trace follow option (#3978)
1 parent 7080b62 commit 35ccc94

File tree

6 files changed

+301
-34
lines changed

6 files changed

+301
-34
lines changed

Documentation/usage/dlv_trace.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ dlv trace [package] regexp [flags]
2323
```
2424
--ebpf Trace using eBPF (experimental).
2525
-e, --exec string Binary file to exec and trace.
26-
--follow-calls int Trace all children of the function to the required depth
26+
--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.
2727
-h, --help help for trace
2828
--output string Output path for the binary.
2929
-p, --pid int Pid to attach to.

_fixtures/testtracefns.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,88 @@ func F4() {
6969
panic("blah")
7070
}
7171

72+
var intc, intd int
73+
74+
func swap() {
75+
defer func() {
76+
intc += 100
77+
}()
78+
temp := intc
79+
intc = intd
80+
intd = temp
81+
}
82+
83+
func unnamedDefer() {
84+
intc = -100
85+
intd = 100
86+
swap()
87+
fmt.Println(intc, intd)
88+
}
89+
func formula(op string) func(int, int) int {
90+
var calc func(int, int) int
91+
if op == "add" {
92+
calc = func(m int, n int) int {
93+
res := m + n
94+
return res
95+
}
96+
} else if op == "mul" {
97+
calc = func(m int, n int) int {
98+
res := m * n
99+
return res
100+
}
101+
}
102+
return calc
103+
}
104+
105+
func op() int {
106+
calc := formula("add")
107+
res := calc(10, 20)
108+
return res
109+
}
110+
111+
func assign(bar func()) {
112+
bar()
113+
}
114+
func testfunc() {
115+
intc = 10
116+
intd = 20
117+
}
118+
119+
func dyn() {
120+
intc = 0
121+
intd = 0
122+
assign(testfunc)
123+
}
124+
125+
func outer() {
126+
intc = 40
127+
defer swap()
128+
}
129+
func nestDefer() {
130+
defer outer()
131+
}
132+
133+
func namedDeferLoop(n int) {
134+
for i := 0; i < n; i++ {
135+
defer testfunc()
136+
}
137+
temp := intc
138+
intc = intd
139+
intd = temp
140+
}
72141
func main() {
73142
j := 0
74143
j += A(2)
75144

76145
j += first(6)
77146
j += callme(2)
78147
fmt.Println(j)
148+
unnamedDefer()
149+
nestDefer()
150+
namedDeferLoop(2)
79151
F0()
152+
ans := op()
153+
fmt.Println(ans)
154+
dyn()
80155

81156
}

cmd/dlv/cmds/commands.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,7 @@ only see the output of the trace operations you can redirect stdout.`,
369369
must(traceCommand.RegisterFlagCompletionFunc("stack", cobra.NoFileCompletions))
370370
traceCommand.Flags().String("output", "", "Output path for the binary.")
371371
must(traceCommand.MarkFlagFilename("output"))
372-
traceCommand.Flags().IntVarP(&traceFollowCalls, "follow-calls", "", 0, "Trace all children of the function to the required depth")
372+
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.")
373373
rootCommand.AddCommand(traceCommand)
374374

375375
coreCommand := &cobra.Command{

pkg/proc/breakpoints.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,11 @@ type Breaklet struct {
113113
watchpoint *Breakpoint
114114
}
115115

116+
// SetCallback sets the call back field, this was primarily added to prevent exporting callback field
117+
func (b *Breaklet) SetCallback(callback func(th Thread, p *Target) (bool, error)) {
118+
b.callback = callback
119+
}
120+
116121
// BreakpointKind determines the behavior of delve when the
117122
// breakpoint is reached.
118123
type BreakpointKind uint16

service/debugger/debugger.go

Lines changed: 160 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -409,13 +409,10 @@ func (d *Debugger) LastModified() time.Time {
409409
return d.target.Selected.BinInfo().LastModified()
410410
}
411411

412-
// FunctionReturnLocations returns all return locations
413-
// for the given function, a list of addresses corresponding
414-
// to 'ret' or 'call runtime.deferreturn'.
415-
func (d *Debugger) FunctionReturnLocations(fnName string) ([]uint64, error) {
416-
d.targetMutex.Lock()
417-
defer d.targetMutex.Unlock()
418-
412+
// functionReturnLocationsInternal is same as FunctionReturnLocations
413+
// except that it does not have a lock and unlock as its called from
414+
// within the callback which has already acquired a lock.
415+
func (d *Debugger) functionReturnLocationsInternal(fnName string) ([]uint64, error) {
419416
if len(d.target.Targets()) > 1 {
420417
return nil, ErrNotImplementedWithMultitarget
421418
}
@@ -454,6 +451,15 @@ func (d *Debugger) FunctionReturnLocations(fnName string) ([]uint64, error) {
454451
return addrs, nil
455452
}
456453

454+
// FunctionReturnLocations returns all return locations
455+
// for the given function, a list of addresses corresponding
456+
// to 'ret' or 'call runtime.deferreturn'.
457+
func (d *Debugger) FunctionReturnLocations(fnName string) ([]uint64, error) {
458+
d.targetMutex.Lock()
459+
defer d.targetMutex.Unlock()
460+
return d.functionReturnLocationsInternal(fnName)
461+
}
462+
457463
// Detach detaches from the target process.
458464
// If `kill` is true we will kill the process after
459465
// detaching.
@@ -1372,7 +1378,7 @@ func (d *Debugger) Functions(filter string, followCalls int) ([]string, error) {
13721378
for _, f := range t.BinInfo().Functions {
13731379
if regex.MatchString(f.Name) {
13741380
if followCalls > 0 {
1375-
newfuncs, err := traverse(t, &f, 1, followCalls)
1381+
newfuncs, err := d.traverse(t, &f, 1, followCalls, filter)
13761382
if err != nil {
13771383
return nil, fmt.Errorf("traverse failed with error %w", err)
13781384
}
@@ -1388,12 +1394,13 @@ func (d *Debugger) Functions(filter string, followCalls int) ([]string, error) {
13881394
return funcs, nil
13891395
}
13901396

1391-
func traverse(t proc.ValidTargets, f *proc.Function, depth int, followCalls int) ([]string, error) {
1397+
func (d *Debugger) traverse(t proc.ValidTargets, f *proc.Function, depth int, followCalls int, rootstr string) ([]string, error) {
13921398
type TraceFunc struct {
13931399
Func *proc.Function
13941400
Depth int
13951401
visited bool
13961402
}
1403+
13971404
type TraceFuncptr *TraceFunc
13981405

13991406
TraceMap := make(map[string]TraceFuncptr)
@@ -1409,7 +1416,7 @@ func traverse(t proc.ValidTargets, f *proc.Function, depth int, followCalls int)
14091416
parent := queue[0]
14101417
queue = queue[1:]
14111418
if parent == nil {
1412-
panic("attempting to open file Delve cannot parse")
1419+
panic("queue has a nil node, cannot traverse!")
14131420
}
14141421
if parent.Depth > followCalls {
14151422
continue
@@ -1427,10 +1434,89 @@ func traverse(t proc.ValidTargets, f *proc.Function, depth int, followCalls int)
14271434
}
14281435
f := parent.Func
14291436
text, err := proc.Disassemble(t.Memory(), nil, t.Breakpoints(), t.BinInfo(), f.Entry, f.End)
1437+
14301438
if err != nil {
14311439
return nil, fmt.Errorf("disassemble failed with error %w", err)
14321440
}
14331441
for _, instr := range text {
1442+
// Dynamic functions need to be handled specially as their destination location
1443+
// is not known statically, hence its required to put a breakpoint in order to
1444+
// acquire the address of the function at runtime and we do this via a
1445+
// call back mechanism
1446+
if instr.IsCall() && instr.DestLoc == nil {
1447+
dynbp, err := t.SetBreakpoint(0, instr.Loc.PC, proc.NextBreakpoint, nil)
1448+
if err != nil {
1449+
return nil, fmt.Errorf("error setting breakpoint inside deferreturn")
1450+
}
1451+
dynCallback := func(th proc.Thread, tgt *proc.Target) (bool, error) {
1452+
// TODO(optimization): Consider using an iterator to avoid materializing
1453+
// the full stack when we only need frames up to the root function
1454+
rawlocs, err := proc.ThreadStacktrace(tgt, tgt.CurrentThread(), followCalls+2)
1455+
if err != nil {
1456+
return false, fmt.Errorf("thread stack trace returned error")
1457+
}
1458+
// Since the dynamic function is known only at runtime, the depth is likewise
1459+
// calculated by referring to the stack and the mechanism is similar to that
1460+
// used in pkg/terminal/command.go:printTraceOutput
1461+
rootindex := -1
1462+
for i := len(rawlocs) - 1; i >= 0; i-- {
1463+
if rawlocs[i].Call.Fn.Name == rootstr {
1464+
if rootindex == -1 {
1465+
rootindex = i
1466+
break
1467+
}
1468+
}
1469+
}
1470+
sdepth := rootindex + 1
1471+
1472+
if sdepth+1 > followCalls {
1473+
return false, nil
1474+
}
1475+
regs, err := th.Registers()
1476+
if err != nil {
1477+
return false, fmt.Errorf("registers inside callback returned err")
1478+
1479+
}
1480+
// Disassemble the instruction at the current PC to get the call destination
1481+
pc := instr.Loc.PC
1482+
maxInstLen := uint64(tgt.BinInfo().Arch.MaxInstructionLength())
1483+
disasm, err := proc.Disassemble(t.Memory(), regs, t.Breakpoints(), tgt.BinInfo(), pc, pc+maxInstLen)
1484+
if err != nil {
1485+
return false, fmt.Errorf("failed to disassemble instruction: %w", err)
1486+
}
1487+
1488+
// Extract address from the decoded instruction's destination location
1489+
var addr uint64
1490+
if len(disasm) > 0 && disasm[0].DestLoc != nil {
1491+
addr = disasm[0].DestLoc.PC
1492+
} else {
1493+
return false, fmt.Errorf("failed to extract call destination from instruction at PC %#x", pc)
1494+
}
1495+
fn := tgt.BinInfo().PCToFunc(addr)
1496+
if fn == nil {
1497+
return false, fmt.Errorf("PCToFunc returned nil")
1498+
}
1499+
err = createFunctionTracepoints(d, fn.Name, rootstr, followCalls)
1500+
if err != nil {
1501+
return false, fmt.Errorf("error creating tracepoint in function %s", fn.Name)
1502+
}
1503+
dynchildren, err := d.traverse(t, fn, sdepth+1, followCalls, rootstr)
1504+
if err != nil {
1505+
return false, fmt.Errorf("error calling traverse on dynamic children")
1506+
}
1507+
for _, child := range dynchildren {
1508+
err := createFunctionTracepoints(d, child, rootstr, followCalls)
1509+
if err != nil {
1510+
return false, fmt.Errorf("error creating tracepoint in function %s", child)
1511+
}
1512+
}
1513+
return false, nil
1514+
}
1515+
for _, dynBrklet := range dynbp.Breaklets {
1516+
dynBrklet.SetCallback(dynCallback)
1517+
}
1518+
}
1519+
14341520
if instr.IsCall() && instr.DestLoc != nil && instr.DestLoc.Fn != nil {
14351521
cf := instr.DestLoc.Fn
14361522
if (strings.HasPrefix(cf.Name, "runtime.") || strings.HasPrefix(cf.Name, "runtime/internal")) && cf.Name != "runtime.deferreturn" && cf.Name != "runtime.gorecover" && cf.Name != "runtime.gopanic" {
@@ -1449,6 +1535,70 @@ func traverse(t proc.ValidTargets, f *proc.Function, depth int, followCalls int)
14491535
return funcs, nil
14501536
}
14511537

1538+
// createFunctionTracepoints is a way to create a trace point late in the cycle but just in time that
1539+
// it can be included in the trace output as in the case of dynamic functions, we get to know the
1540+
// functions that are being called from deferreturn quite late in execution. This might create multiple
1541+
// tracepoints
1542+
func createFunctionTracepoints(d *Debugger, fname string, rootstr string, followCalls int) error {
1543+
1544+
// Helper function to create breakpoints for tracepoints
1545+
// Ignore BreakpointExistsError since duplicate tracepoints
1546+
// are expected during dynamic function discovery
1547+
createBrkForTracepoint := func(lbp *proc.LogicalBreakpoint) error {
1548+
d.breakpointIDCounter++
1549+
lbp.LogicalID = d.breakpointIDCounter
1550+
lbp.HitCount = make(map[int64]uint64)
1551+
d.target.LogicalBreakpoints[lbp.LogicalID] = lbp
1552+
err := d.target.SetBreakpointEnabled(lbp, true)
1553+
if err != nil {
1554+
delete(d.target.LogicalBreakpoints, lbp.LogicalID)
1555+
// Silently ignore BreakpointExistsError - this is expected when
1556+
// creating tracepoints for functions that may already have them
1557+
if _, exists := err.(proc.BreakpointExistsError); exists {
1558+
return nil
1559+
}
1560+
return err
1561+
}
1562+
return nil
1563+
}
1564+
1565+
// Create tracepoint for function entry
1566+
lbp := &proc.LogicalBreakpoint{
1567+
Set: proc.SetBreakpoint{
1568+
FunctionName: fname,
1569+
},
1570+
Tracepoint: true,
1571+
RootFuncName: rootstr,
1572+
Stacktrace: 20,
1573+
TraceFollowCalls: followCalls,
1574+
}
1575+
1576+
err := createBrkForTracepoint(lbp)
1577+
if err != nil {
1578+
return fmt.Errorf("error creating breakpoint at function %s: %w", fname, err)
1579+
}
1580+
1581+
// Create tracepoints for function return locations
1582+
raddrs, _ := d.functionReturnLocationsInternal(fname)
1583+
for i := range raddrs {
1584+
retLbp := &proc.LogicalBreakpoint{
1585+
Set: proc.SetBreakpoint{
1586+
PidAddrs: []proc.PidAddr{{Pid: d.target.Selected.Pid(), Addr: raddrs[i]}},
1587+
},
1588+
TraceReturn: true,
1589+
RootFuncName: rootstr,
1590+
Stacktrace: 20,
1591+
TraceFollowCalls: followCalls,
1592+
}
1593+
1594+
err := createBrkForTracepoint(retLbp)
1595+
if err != nil {
1596+
return fmt.Errorf("error creating breakpoint at function return %s: %w", fname, err)
1597+
}
1598+
}
1599+
return nil
1600+
}
1601+
14521602
// Types returns all type information in the binary.
14531603
func (d *Debugger) Types(filter string) ([]string, error) {
14541604
d.targetMutex.Lock()

0 commit comments

Comments
 (0)