mirror of
https://github.com/go-delve/delve.git
synced 2025-10-29 01:27:16 +08:00
proc: support inlining
Go 1.10 added inlined calls to debug_info, this commit adds support for DW_TAG_inlined_call to delve, both for stack traces (where inlined calls will appear as normal stack frames) and to correct the behavior of next, step and stepout. The calls to Next and Frame of stackIterator continue to work unchanged and only return real stack frames, after reading each line appendInlinedCalls is called to unpacked all the inlined calls that involve the current PC. The fake stack frames produced by appendInlinedCalls are distinguished from real stack frames by having the Inlined attribute set to true. Also their Current and Call locations are treated differently. The Call location will be changed to represent the position inside the inlined call, while the Current location will always reference the real stack frame. This is done because: * next, step and stepout need to access the debug_info entry of the real function they are stepping through * we are already manipulating Call in different ways while Current is just what we read from the call stack The strategy remains mostly the same, we disassemble the function and we set a breakpoint on each instruction corresponding to a different file:line. The function in question will be the one corresponding to the first real (i.e. non-inlined) stack frame. * If the current function contains inlined calls, 'next' will not set any breakpoints on instructions that belong to inlined calls. We do not do this for 'step'. * If we are inside an inlined call that makes other inlined functions, 'next' will not set any breakpoints that belong to inlined calls that are children of the current inlined call. * If the current function is inlined the breakpoint on the return address won't be set, because inlined frames don't have a return address. * The code we use for stepout doesn't work at all if we are inside an inlined call, instead we call 'next' but instruct it to remove all PCs belonging to the current inlined call.
This commit is contained in:
@ -11,6 +11,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/derekparker/delve/pkg/dwarf/godwarf"
|
||||
"github.com/derekparker/delve/pkg/dwarf/reader"
|
||||
)
|
||||
|
||||
// Thread represents a thread.
|
||||
@ -100,7 +101,17 @@ func (err *NoSourceForPCError) Error() string {
|
||||
// - a breakpoint on the return address of the function, with a condition
|
||||
// checking that we move to the previous stack frame and stay on the same
|
||||
// goroutine.
|
||||
func next(dbp Process, stepInto bool) error {
|
||||
//
|
||||
// The breakpoint on the return address is *not* set if the current frame is
|
||||
// an inlined call. For inlined calls topframe.Current.Fn is the function
|
||||
// where the inlining happened and the second set of breakpoints will also
|
||||
// cover the "return address".
|
||||
//
|
||||
// If inlinedStepOut is true this function implements the StepOut operation
|
||||
// for an inlined function call. Everything works the same as normal except
|
||||
// when removing instructions belonging to inlined calls we also remove all
|
||||
// instructions belonging to the current inlined call.
|
||||
func next(dbp Process, stepInto, inlinedStepOut bool) error {
|
||||
selg := dbp.SelectedGoroutine()
|
||||
curthread := dbp.CurrentThread()
|
||||
topframe, retframe, err := topframe(selg, curthread)
|
||||
@ -112,6 +123,11 @@ func next(dbp Process, stepInto bool) error {
|
||||
return &NoSourceForPCError{topframe.Current.PC}
|
||||
}
|
||||
|
||||
// sanity check
|
||||
if inlinedStepOut && !topframe.Inlined {
|
||||
panic("next called with inlinedStepOut but topframe was not inlined")
|
||||
}
|
||||
|
||||
success := false
|
||||
defer func() {
|
||||
if !success {
|
||||
@ -141,14 +157,18 @@ func next(dbp Process, stepInto bool) error {
|
||||
sameFrameCond := andFrameoffCondition(sameGCond, topframe.FrameOffset())
|
||||
var sameOrRetFrameCond ast.Expr
|
||||
if sameGCond != nil {
|
||||
sameOrRetFrameCond = &ast.BinaryExpr{
|
||||
Op: token.LAND,
|
||||
X: sameGCond,
|
||||
Y: &ast.BinaryExpr{
|
||||
Op: token.LOR,
|
||||
X: frameoffCondition(topframe.FrameOffset()),
|
||||
Y: frameoffCondition(retframe.FrameOffset()),
|
||||
},
|
||||
if topframe.Inlined {
|
||||
sameOrRetFrameCond = sameFrameCond
|
||||
} else {
|
||||
sameOrRetFrameCond = &ast.BinaryExpr{
|
||||
Op: token.LAND,
|
||||
X: sameGCond,
|
||||
Y: &ast.BinaryExpr{
|
||||
Op: token.LOR,
|
||||
X: frameoffCondition(topframe.FrameOffset()),
|
||||
Y: frameoffCondition(retframe.FrameOffset()),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -216,6 +236,18 @@ func next(dbp Process, stepInto bool) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if !stepInto {
|
||||
// Removing any PC range belonging to an inlined call
|
||||
frame := topframe
|
||||
if inlinedStepOut {
|
||||
frame = retframe
|
||||
}
|
||||
pcs, err = removeInlinedCalls(dbp, pcs, frame)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !csource {
|
||||
var covered bool
|
||||
for i := range pcs {
|
||||
@ -233,7 +265,6 @@ func next(dbp Process, stepInto bool) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Add a breakpoint on the return address for the current frame
|
||||
for _, pc := range pcs {
|
||||
if _, err := dbp.SetBreakpoint(pc, NextBreakpoint, sameFrameCond); err != nil {
|
||||
if _, ok := err.(BreakpointExistsError); !ok {
|
||||
@ -243,18 +274,24 @@ func next(dbp Process, stepInto bool) error {
|
||||
}
|
||||
|
||||
}
|
||||
if bp, err := dbp.SetBreakpoint(topframe.Ret, NextBreakpoint, retFrameCond); err != nil {
|
||||
if _, isexists := err.(BreakpointExistsError); isexists {
|
||||
if bp.Kind == NextBreakpoint {
|
||||
// If the return address shares the same address with one of the lines
|
||||
// of the function (because we are stepping through a recursive
|
||||
// function) then the corresponding breakpoint should be active both on
|
||||
// this frame and on the return frame.
|
||||
bp.Cond = sameOrRetFrameCond
|
||||
if !topframe.Inlined {
|
||||
// Add a breakpoint on the return address for the current frame.
|
||||
// For inlined functions there is no need to do this, the set of PCs
|
||||
// returned by the AllPCsBetween call above already cover all instructions
|
||||
// of the containing function.
|
||||
if bp, err := dbp.SetBreakpoint(topframe.Ret, NextBreakpoint, retFrameCond); err != nil {
|
||||
if _, isexists := err.(BreakpointExistsError); isexists {
|
||||
if bp.Kind == NextBreakpoint {
|
||||
// If the return address shares the same address with one of the lines
|
||||
// of the function (because we are stepping through a recursive
|
||||
// function) then the corresponding breakpoint should be active both on
|
||||
// this frame and on the return frame.
|
||||
bp.Cond = sameOrRetFrameCond
|
||||
}
|
||||
}
|
||||
// Return address could be wrong, if we are unable to set a breakpoint
|
||||
// there it's ok.
|
||||
}
|
||||
// Return address could be wrong, if we are unable to set a breakpoint
|
||||
// there it's ok.
|
||||
}
|
||||
|
||||
if bp := curthread.Breakpoint(); bp.Breakpoint == nil {
|
||||
@ -264,6 +301,39 @@ func next(dbp Process, stepInto bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
func removeInlinedCalls(dbp Process, pcs []uint64, topframe Stackframe) ([]uint64, error) {
|
||||
bi := dbp.BinInfo()
|
||||
irdr := reader.InlineStack(bi.dwarf, topframe.Call.Fn.offset, 0)
|
||||
for irdr.Next() {
|
||||
e := irdr.Entry()
|
||||
if e.Offset == topframe.Call.Fn.offset {
|
||||
continue
|
||||
}
|
||||
ranges, err := bi.dwarf.Ranges(e)
|
||||
if err != nil {
|
||||
return pcs, err
|
||||
}
|
||||
for _, rng := range ranges {
|
||||
pcs = removePCsBetween(pcs, rng[0], rng[1])
|
||||
}
|
||||
irdr.SkipChildren()
|
||||
}
|
||||
return pcs, irdr.Err()
|
||||
}
|
||||
|
||||
func removePCsBetween(pcs []uint64, start, end uint64) []uint64 {
|
||||
out := pcs[:0]
|
||||
for _, pc := range pcs {
|
||||
if pc < start || pc >= end {
|
||||
out = append(out, pc)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func setStepIntoBreakpoint(dbp Process, text []AsmInstruction, cond ast.Expr) error {
|
||||
if len(text) <= 0 {
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user