diff --git a/_fixtures/testinline.go b/_fixtures/testinline.go new file mode 100644 index 0000000000000000000000000000000000000000..f8ef92078e3b68901645377329536b933f259a34 --- /dev/null +++ b/_fixtures/testinline.go @@ -0,0 +1,21 @@ +package main + +import "fmt" + +func inlineThis(a int) int { + z := a * a + return z + a/a +} + +func initialize(a, b *int) { + *a = 3 + *b = 4 +} + +func main() { + var a, b int + initialize(&a, &b) + a = inlineThis(a) + b = inlineThis(b) + fmt.Printf("%d %d\n", a, b) +} diff --git a/pkg/dwarf/line/state_machine.go b/pkg/dwarf/line/state_machine.go index e8273c133d907fb2537e6bb128634acf014cb81b..881c115ef8a6eec71ae54364fddcbe7042a842ba 100644 --- a/pkg/dwarf/line/state_machine.go +++ b/pkg/dwarf/line/state_machine.go @@ -119,7 +119,6 @@ func (lineInfo *DebugLineInfo) AllPCsForFileLine(f string, l int) (pcs []uint64) if sm.valid { pcs = append(pcs, sm.address) } - line := sm.line // Keep going until we're on a different line. We only care about // when a line comes back around (i.e. for loop) so get to next line, // and try to find the line we care about again. @@ -127,7 +126,7 @@ func (lineInfo *DebugLineInfo) AllPCsForFileLine(f string, l int) (pcs []uint64) if err := sm.next(); err != nil { break } - if line < sm.line { + if l != sm.line { break } } diff --git a/pkg/dwarf/reader/reader.go b/pkg/dwarf/reader/reader.go index e3c49dfd1f597a7077feb89540b146d93efc6267..ed056fbf89b0ed0f1c62d5bb9f16a7775113c10c 100755 --- a/pkg/dwarf/reader/reader.go +++ b/pkg/dwarf/reader/reader.go @@ -365,3 +365,82 @@ func LoadAbstractOrigin(entry *dwarf.Entry, aordr *dwarf.Reader) (Entry, dwarf.O return compositeEntry(r), entry.Offset } + +// InlineStackReader provides a way to read the stack of inlined calls at a +// specified PC address. +type InlineStackReader struct { + dwarf *dwarf.Data + reader *dwarf.Reader + entry *dwarf.Entry + depth int + pc uint64 + err error +} + +// InlineStack returns an InlineStackReader for the specified function and +// PC address. +// If pc is 0 then all inlined calls will be returned. +func InlineStack(dwarf *dwarf.Data, fnoff dwarf.Offset, pc uint64) *InlineStackReader { + reader := dwarf.Reader() + reader.Seek(fnoff) + return &InlineStackReader{dwarf: dwarf, reader: reader, entry: nil, depth: 0, pc: pc} +} + +// Next reads next inlined call in the stack, returns false if there aren't any. +func (irdr *InlineStackReader) Next() bool { + if irdr.err != nil { + return false + } + + for { + irdr.entry, irdr.err = irdr.reader.Next() + if irdr.entry == nil || irdr.err != nil { + return false + } + + switch irdr.entry.Tag { + case 0: + irdr.depth-- + if irdr.depth == 0 { + return false + } + + case dwarf.TagLexDwarfBlock, dwarf.TagSubprogram, dwarf.TagInlinedSubroutine: + var recur bool + if irdr.pc != 0 { + recur, irdr.err = entryRangesContains(irdr.dwarf, irdr.entry, irdr.pc) + } else { + recur = true + } + if recur { + irdr.depth++ + if irdr.entry.Tag == dwarf.TagInlinedSubroutine { + return true + } + } else { + if irdr.depth == 0 { + return false + } + irdr.reader.SkipChildren() + } + + default: + irdr.reader.SkipChildren() + } + } +} + +// Entry returns the DIE for the current inlined call. +func (irdr *InlineStackReader) Entry() *dwarf.Entry { + return irdr.entry +} + +// Err returns an error, if any was encountered. +func (irdr *InlineStackReader) Err() error { + return irdr.err +} + +// SkipChildren skips all children of the current inlined call. +func (irdr *InlineStackReader) SkipChildren() { + irdr.reader.SkipChildren() +} diff --git a/pkg/dwarf/reader/variables.go b/pkg/dwarf/reader/variables.go index be35d34df1349913560f08cde66338fc71c75d99..ca93e721c578e987587ddb8b56b4e88a737b5d40 100644 --- a/pkg/dwarf/reader/variables.go +++ b/pkg/dwarf/reader/variables.go @@ -47,10 +47,10 @@ func (vrdr *VariableReader) Next() bool { return false } - case dwarf.TagLexDwarfBlock, dwarf.TagSubprogram: + case dwarf.TagLexDwarfBlock, dwarf.TagSubprogram, dwarf.TagInlinedSubroutine: recur := true if vrdr.onlyVisible { - recur, vrdr.err = vrdr.entryRangesContains() + recur, vrdr.err = entryRangesContains(vrdr.dwarf, vrdr.entry, vrdr.pc) if vrdr.err != nil { return false } @@ -77,13 +77,13 @@ func (vrdr *VariableReader) Next() bool { } } -func (vrdr *VariableReader) entryRangesContains() (bool, error) { - rngs, err := vrdr.dwarf.Ranges(vrdr.entry) +func entryRangesContains(dwarf *dwarf.Data, entry *dwarf.Entry, pc uint64) (bool, error) { + rngs, err := dwarf.Ranges(entry) if err != nil { return false, err } for _, rng := range rngs { - if vrdr.pc >= rng[0] && vrdr.pc < rng[1] { + if pc >= rng[0] && pc < rng[1] { return true, nil } } diff --git a/pkg/proc/bininfo.go b/pkg/proc/bininfo.go index 233432210adaea1396fbd1776cb4f6b9929576f9..c37eca40dd302a848877b080c3e9fa6159ee0189 100644 --- a/pkg/proc/bininfo.go +++ b/pkg/proc/bininfo.go @@ -296,6 +296,17 @@ func (bi *BinaryInfo) LineToPC(filename string, lineno int) (pc uint64, fn *Func return } +// AllPCsForFileLine returns all PC addresses for the given filename:lineno. +func (bi *BinaryInfo) AllPCsForFileLine(filename string, lineno int) []uint64 { + r := make([]uint64, 0, 1) + for _, cu := range bi.compileUnits { + if cu.lineInfo.Lookup[filename] != nil { + r = append(r, cu.lineInfo.AllPCsForFileLine(filename, lineno)...) + } + } + return r +} + // PCToFunc returns the function containing the given PC address func (bi *BinaryInfo) PCToFunc(pc uint64) *Function { i := sort.Search(len(bi.Functions), func(i int) bool { diff --git a/pkg/proc/dwarf_expr_test.go b/pkg/proc/dwarf_expr_test.go index 7205376e934b382b583c7aaa269814dc4c92429e..c9a9f1887002687bfd107267b404a00fe4bfc768 100644 --- a/pkg/proc/dwarf_expr_test.go +++ b/pkg/proc/dwarf_expr_test.go @@ -79,8 +79,8 @@ func uintExprCheck(t *testing.T, scope *proc.EvalScope, expr string, tgt uint64) } } -func dwarfExprCheck(t *testing.T, mem proc.MemoryReadWriter, regs op.DwarfRegisters, bi *proc.BinaryInfo, testCases map[string]uint16) *proc.EvalScope { - scope := &proc.EvalScope{PC: 0x40100, Regs: regs, Mem: mem, Gvar: nil, BinInfo: bi} +func dwarfExprCheck(t *testing.T, mem proc.MemoryReadWriter, regs op.DwarfRegisters, bi *proc.BinaryInfo, testCases map[string]uint16, fn *proc.Function) *proc.EvalScope { + scope := &proc.EvalScope{Location: proc.Location{PC: 0x40100, Fn: fn}, Regs: regs, Mem: mem, Gvar: nil, BinInfo: bi} for name, value := range testCases { uintExprCheck(t, scope, name, uint64(value)) } @@ -116,12 +116,14 @@ func TestDwarfExprRegisters(t *testing.T) { bi := fakeBinaryInfo(t, dwb) + mainfn := bi.LookupFunc["main.main"] + mem := newFakeMemory(defaultCFA, uint64(0), uint64(testCases["b"]), uint16(testCases["pair.v"])) regs := core.Registers{LinuxCoreRegisters: &core.LinuxCoreRegisters{}} regs.Rax = uint64(testCases["a"]) regs.Rdx = uint64(testCases["c"]) - dwarfExprCheck(t, mem, dwarfRegisters(®s), bi, testCases) + dwarfExprCheck(t, mem, dwarfRegisters(®s), bi, testCases, mainfn) } func TestDwarfExprComposite(t *testing.T) { @@ -169,6 +171,8 @@ func TestDwarfExprComposite(t *testing.T) { bi := fakeBinaryInfo(t, dwb) + mainfn := bi.LookupFunc["main.main"] + mem := newFakeMemory(defaultCFA, uint64(0), uint64(0), uint16(testCases["pair.v"]), []byte(stringVal)) var regs core.Registers regs.LinuxCoreRegisters = &core.LinuxCoreRegisters{} @@ -177,7 +181,7 @@ func TestDwarfExprComposite(t *testing.T) { regs.Rcx = uint64(testCases["pair.k"]) regs.Rbx = uint64(testCases["n"]) - scope := dwarfExprCheck(t, mem, dwarfRegisters(®s), bi, testCases) + scope := dwarfExprCheck(t, mem, dwarfRegisters(®s), bi, testCases, mainfn) thevar, err := scope.EvalExpression("s", normalLoadConfig) assertNoError(err, t, fmt.Sprintf("EvalExpression(%s)", "s")) @@ -207,10 +211,12 @@ func TestDwarfExprLoclist(t *testing.T) { bi := fakeBinaryInfo(t, dwb) + mainfn := bi.LookupFunc["main.main"] + mem := newFakeMemory(defaultCFA, uint16(before), uint16(after)) regs := core.Registers{LinuxCoreRegisters: &core.LinuxCoreRegisters{}} - scope := &proc.EvalScope{PC: 0x40100, Regs: dwarfRegisters(®s), Mem: mem, Gvar: nil, BinInfo: bi} + scope := &proc.EvalScope{Location: proc.Location{PC: 0x40100, Fn: mainfn}, Regs: dwarfRegisters(®s), Mem: mem, Gvar: nil, BinInfo: bi} uintExprCheck(t, scope, "a", before) scope.PC = 0x40800 diff --git a/pkg/proc/eval.go b/pkg/proc/eval.go index c4f156652044dee0abc34c8c2e74a2b6575521e7..bc1693f98aec4117567b46e2c3e5ae405e940f99 100644 --- a/pkg/proc/eval.go +++ b/pkg/proc/eval.go @@ -584,9 +584,8 @@ func (scope *EvalScope) evalIdent(node *ast.Ident) (*Variable, error) { } // if it's not a local variable then it could be a package variable w/o explicit package name - _, _, fn := scope.BinInfo.PCToLine(scope.PC) - if fn != nil { - if v, err := scope.findGlobal(fn.PackageName() + "." + node.Name); err == nil { + if scope.Fn != nil { + if v, err := scope.findGlobal(scope.Fn.PackageName() + "." + node.Name); err == nil { v.Name = node.Name return v, nil } diff --git a/pkg/proc/proc.go b/pkg/proc/proc.go index c121c4774612709804bb44355d5706673eedf7a7..fb58e3a4a5ab9a136adf7b02159637800ef84852 100644 --- a/pkg/proc/proc.go +++ b/pkg/proc/proc.go @@ -72,7 +72,7 @@ func Next(dbp Process) (err error) { return fmt.Errorf("next while nexting") } - if err = next(dbp, false); err != nil { + if err = next(dbp, false, false); err != nil { dbp.ClearInternalBreakpoints() return } @@ -226,7 +226,7 @@ func Step(dbp Process) (err error) { return fmt.Errorf("next while nexting") } - if err = next(dbp, true); err != nil { + if err = next(dbp, true, false); err != nil { switch err.(type) { case ThreadBlockedError: // Noop default: @@ -293,6 +293,22 @@ func StepOut(dbp Process) error { return err } + success := false + defer func() { + if !success { + dbp.ClearInternalBreakpoints() + } + }() + + if topframe.Inlined { + if err := next(dbp, false, true); err != nil { + return err + } + + success = true + return Continue(dbp) + } + sameGCond := SameGoroutineCondition(selg) retFrameCond := andFrameoffCondition(sameGCond, retframe.FrameOffset()) @@ -310,15 +326,10 @@ func StepOut(dbp Process) error { } } - if topframe.Ret == 0 && deferpc == 0 { - return errors.New("nothing to stepout to") - } - if deferpc != 0 && deferpc != topframe.Current.PC { bp, err := dbp.SetBreakpoint(deferpc, NextDeferBreakpoint, sameGCond) if err != nil { if _, ok := err.(BreakpointExistsError); !ok { - dbp.ClearInternalBreakpoints() return err } } @@ -330,11 +341,14 @@ func StepOut(dbp Process) error { } } + if topframe.Ret == 0 && deferpc == 0 { + return errors.New("nothing to stepout to") + } + if topframe.Ret != 0 { _, err := dbp.SetBreakpoint(topframe.Ret, NextBreakpoint, retFrameCond) if err != nil { if _, isexists := err.(BreakpointExistsError); !isexists { - dbp.ClearInternalBreakpoints() return err } } @@ -344,6 +358,7 @@ func StepOut(dbp Process) error { curthread.SetCurrentBreakpoint() } + success = true return Continue(dbp) } @@ -499,6 +514,7 @@ func FrameToScope(bi *BinaryInfo, thread MemoryReadWriter, g *G, frame Stackfram if g != nil { gvar = g.variable } - s := &EvalScope{PC: frame.Call.PC, Regs: frame.Regs, Mem: thread, Gvar: gvar, BinInfo: bi, frameOffset: frame.FrameOffset()} + s := &EvalScope{Location: frame.Call, Regs: frame.Regs, Mem: thread, Gvar: gvar, BinInfo: bi, frameOffset: frame.FrameOffset()} + s.PC = frame.lastpc return s } diff --git a/pkg/proc/proc_test.go b/pkg/proc/proc_test.go index c9feb096b918fb463fc63efc6276b6c8e2a24546..4b450aefad08344dfdc9ae225c06de8eed5ec019 100644 --- a/pkg/proc/proc_test.go +++ b/pkg/proc/proc_test.go @@ -3473,3 +3473,191 @@ func TestDisassembleGlobalVars(t *testing.T) { } }) } + +func checkFrame(frame proc.Stackframe, fnname, file string, line int, inlined bool) error { + if frame.Call.Fn == nil || frame.Call.Fn.Name != fnname { + return fmt.Errorf("wrong function name: %s", fnname) + } + if frame.Call.File != file || frame.Call.Line != line { + return fmt.Errorf("wrong file:line %s:%d", frame.Call.File, frame.Call.Line) + } + if frame.Inlined != inlined { + if inlined { + return fmt.Errorf("not inlined") + } else { + return fmt.Errorf("inlined") + } + } + return nil +} + +func TestInlinedStacktraceAndVariables(t *testing.T) { + if ver, _ := goversion.Parse(runtime.Version()); ver.Major >= 0 && !ver.AfterOrEqual(goversion.GoVersion{1, 10, -1, 0, 0, ""}) { + // Versions of go before 1.10 do not have DWARF information for inlined calls + t.Skip("inlining not supported") + } + + firstCallCheck := &scopeCheck{ + line: 7, + ok: false, + varChecks: []varCheck{ + varCheck{ + name: "a", + typ: "int", + kind: reflect.Int, + hasVal: true, + intVal: 3, + }, + varCheck{ + name: "z", + typ: "int", + kind: reflect.Int, + hasVal: true, + intVal: 9, + }, + }, + } + + secondCallCheck := &scopeCheck{ + line: 7, + ok: false, + varChecks: []varCheck{ + varCheck{ + name: "a", + typ: "int", + kind: reflect.Int, + hasVal: true, + intVal: 4, + }, + varCheck{ + name: "z", + typ: "int", + kind: reflect.Int, + hasVal: true, + intVal: 16, + }, + }, + } + + withTestProcessArgs("testinline", t, ".", []string{}, protest.EnableInlining, func(p proc.Process, fixture protest.Fixture) { + pcs := p.BinInfo().AllPCsForFileLine(fixture.Source, 7) + if len(pcs) < 2 { + t.Fatalf("expected at least two locations for %s:%d (got %d: %#x)", fixture.Source, 6, len(pcs), pcs) + } + for _, pc := range pcs { + _, err := p.SetBreakpoint(pc, proc.UserBreakpoint, nil) + assertNoError(err, t, fmt.Sprintf("SetBreakpoint(%#x)", pc)) + } + + // first inlined call + assertNoError(proc.Continue(p), t, "Continue") + frames, err := proc.ThreadStacktrace(p.CurrentThread(), 20) + assertNoError(err, t, "ThreadStacktrace") + t.Logf("Stacktrace:\n") + for i := range frames { + t.Logf("\t%s at %s:%d\n", frames[i].Call.Fn.Name, frames[i].Call.File, frames[i].Call.Line) + } + + if err := checkFrame(frames[0], "main.inlineThis", fixture.Source, 7, true); err != nil { + t.Fatalf("Wrong frame 0: %v", err) + } + if err := checkFrame(frames[1], "main.main", fixture.Source, 18, false); err != nil { + t.Fatalf("Wrong frame 1: %v", err) + } + + if avar, _ := constant.Int64Val(evalVariable(p, t, "a").Value); avar != 3 { + t.Fatalf("value of 'a' variable is not 3 (%d)", avar) + } + if zvar, _ := constant.Int64Val(evalVariable(p, t, "z").Value); zvar != 9 { + t.Fatalf("value of 'z' variable is not 9 (%d)", zvar) + } + + if _, ok := firstCallCheck.checkLocalsAndArgs(p, t); !ok { + t.Fatalf("exiting for past errors") + } + + // second inlined call + assertNoError(proc.Continue(p), t, "Continue") + frames, err = proc.ThreadStacktrace(p.CurrentThread(), 20) + assertNoError(err, t, "ThreadStacktrace (2)") + t.Logf("Stacktrace 2:\n") + for i := range frames { + t.Logf("\t%s at %s:%d\n", frames[i].Call.Fn.Name, frames[i].Call.File, frames[i].Call.Line) + } + + if err := checkFrame(frames[0], "main.inlineThis", fixture.Source, 7, true); err != nil { + t.Fatalf("Wrong frame 0: %v", err) + } + if err := checkFrame(frames[1], "main.main", fixture.Source, 19, false); err != nil { + t.Fatalf("Wrong frame 1: %v", err) + } + + if avar, _ := constant.Int64Val(evalVariable(p, t, "a").Value); avar != 4 { + t.Fatalf("value of 'a' variable is not 3 (%d)", avar) + } + if zvar, _ := constant.Int64Val(evalVariable(p, t, "z").Value); zvar != 16 { + t.Fatalf("value of 'z' variable is not 9 (%d)", zvar) + } + if bvar, err := evalVariableOrError(p, "b"); err == nil { + t.Fatalf("expected error evaluating 'b', but it succeeded instead: %v", bvar) + } + + if _, ok := secondCallCheck.checkLocalsAndArgs(p, t); !ok { + t.Fatalf("exiting for past errors") + } + }) +} + +func TestInlineStep(t *testing.T) { + if ver, _ := goversion.Parse(runtime.Version()); ver.Major >= 0 && !ver.AfterOrEqual(goversion.GoVersion{1, 10, -1, 0, 0, ""}) { + // Versions of go before 1.10 do not have DWARF information for inlined calls + t.Skip("inlining not supported") + } + testseq2Args(".", []string{}, protest.EnableInlining, t, "testinline", "", []seqTest{ + {contContinue, 18}, + {contStep, 6}, + {contStep, 7}, + {contStep, 18}, + {contStep, 19}, + }) +} + +func TestInlineNext(t *testing.T) { + if ver, _ := goversion.Parse(runtime.Version()); ver.Major >= 0 && !ver.AfterOrEqual(goversion.GoVersion{1, 10, -1, 0, 0, ""}) { + // Versions of go before 1.10 do not have DWARF information for inlined calls + t.Skip("inlining not supported") + } + testseq2Args(".", []string{}, protest.EnableInlining, t, "testinline", "", []seqTest{ + {contContinue, 18}, + {contStep, 6}, + {contNext, 7}, + {contNext, 18}, + {contNext, 19}, + }) +} + +func TestInlineStepOver(t *testing.T) { + if ver, _ := goversion.Parse(runtime.Version()); ver.Major >= 0 && !ver.AfterOrEqual(goversion.GoVersion{1, 10, -1, 0, 0, ""}) { + // Versions of go before 1.10 do not have DWARF information for inlined calls + t.Skip("inlining not supported") + } + testseq2Args(".", []string{}, protest.EnableInlining, t, "testinline", "", []seqTest{ + {contContinue, 18}, + {contNext, 18}, + {contNext, 19}, + {contNext, 19}, + {contNext, 20}, + }) +} + +func TestInlineStepOut(t *testing.T) { + if ver, _ := goversion.Parse(runtime.Version()); ver.Major >= 0 && !ver.AfterOrEqual(goversion.GoVersion{1, 10, -1, 0, 0, ""}) { + // Versions of go before 1.10 do not have DWARF information for inlined calls + t.Skip("inlining not supported") + } + testseq2Args(".", []string{}, protest.EnableInlining, t, "testinline", "", []seqTest{ + {contContinue, 18}, + {contStep, 6}, + {contStepout, 18}, + }) +} diff --git a/pkg/proc/scope_test.go b/pkg/proc/scope_test.go index c1f55d44b2119fa8c488393b099229334af8eecb..424113097579f020b02d12fd6eb96e351e84a9b9 100644 --- a/pkg/proc/scope_test.go +++ b/pkg/proc/scope_test.go @@ -92,27 +92,7 @@ func TestScope(t *testing.T) { t.Errorf("unknown stop position %s:%d %#x", bp.File, bp.Line, bp.Addr) } - scope, err := proc.GoroutineScope(p.CurrentThread()) - assertNoError(err, t, "GoroutineScope()") - - args, err := scope.FunctionArguments(normalLoadConfig) - assertNoError(err, t, "FunctionArguments()") - locals, err := scope.LocalVariables(normalLoadConfig) - assertNoError(err, t, "LocalVariables()") - - for _, arg := range args { - scopeCheck.checkVar(arg, t) - } - - for _, local := range locals { - scopeCheck.checkVar(local, t) - } - - for i := range scopeCheck.varChecks { - if !scopeCheck.varChecks[i].ok { - t.Errorf("%d: variable %s not found", scopeCheck.line, scopeCheck.varChecks[i].name) - } - } + scope, _ := scopeCheck.checkLocalsAndArgs(p, t) var prev *varCheck for i := range scopeCheck.varChecks { @@ -127,7 +107,7 @@ func TestScope(t *testing.T) { } scopeCheck.ok = true - _, err = p.ClearBreakpoint(bp.Addr) + _, err := p.ClearBreakpoint(bp.Addr) assertNoError(err, t, "ClearBreakpoint") } }) @@ -254,6 +234,35 @@ func (check *scopeCheck) Parse(descr string, t *testing.T) { } } +func (scopeCheck *scopeCheck) checkLocalsAndArgs(p proc.Process, t *testing.T) (*proc.EvalScope, bool) { + scope, err := proc.GoroutineScope(p.CurrentThread()) + assertNoError(err, t, "GoroutineScope()") + + ok := true + + args, err := scope.FunctionArguments(normalLoadConfig) + assertNoError(err, t, "FunctionArguments()") + locals, err := scope.LocalVariables(normalLoadConfig) + assertNoError(err, t, "LocalVariables()") + + for _, arg := range args { + scopeCheck.checkVar(arg, t) + } + + for _, local := range locals { + scopeCheck.checkVar(local, t) + } + + for i := range scopeCheck.varChecks { + if !scopeCheck.varChecks[i].ok { + t.Errorf("%d: variable %s not found", scopeCheck.line, scopeCheck.varChecks[i].name) + ok = false + } + } + + return scope, ok +} + func (check *scopeCheck) checkVar(v *proc.Variable, t *testing.T) { var varCheck *varCheck for i := range check.varChecks { diff --git a/pkg/proc/stack.go b/pkg/proc/stack.go index 4d9e181f2e2adf45bda29e4969b9a456d433e01a..6958d7b3d05472a7bdca8d57c9cf6d03b7537e18 100644 --- a/pkg/proc/stack.go +++ b/pkg/proc/stack.go @@ -8,17 +8,30 @@ import ( "github.com/derekparker/delve/pkg/dwarf/frame" "github.com/derekparker/delve/pkg/dwarf/op" + "github.com/derekparker/delve/pkg/dwarf/reader" ) // This code is partly adapted from runtime.gentraceback in // $GOROOT/src/runtime/traceback.go // Stackframe represents a frame in a system stack. +// +// Each stack frame has two locations Current and Call. +// +// For the topmost stackframe Current and Call are the same location. +// +// For stackframes after the first Current is the location corresponding to +// the return address and Call is the location of the CALL instruction that +// was last executed on the frame. Note however that Call.PC is always equal +// to Current.PC, because finding the correct value for Call.PC would +// require disassembling each function in the stacktrace. +// +// For synthetic stackframes generated for inlined function calls Current.Fn +// is the function containing the inlining and Call.Fn in the inlined +// function. type Stackframe struct { - // Address the function above this one on the call stack will return to. - Current Location - // Address of the call instruction for the function above on the call stack. - Call Location + Current, Call Location + // Frame registers. Regs op.DwarfRegisters // High address of the stack. @@ -31,6 +44,19 @@ type Stackframe struct { Err error // SystemStack is true if this frame belongs to a system stack. SystemStack bool + // Inlined is true if this frame is actually an inlined call. + Inlined bool + + // lastpc is a memory address guaranteed to belong to the last instruction + // executed in this stack frame. + // For the topmost stack frame this will be the same as Current.PC and + // Call.PC, for other stack frames it will usually be Current.PC-1, but + // could be different when inlined calls are involved in the stacktrace. + // Note that this address isn't guaranteed to belong to the start of an + // instruction and, for this reason, should not be propagated outside of + // pkg/proc. + // Use this value to determine active lexical scopes for the stackframe. + lastpc uint64 } // FrameOffset returns the address of the stack frame, absolute for system @@ -123,6 +149,8 @@ type stackIterator struct { g *G // the goroutine being stacktraced, nil if we are stacktracing a goroutine-less thread g0_sched_sp uint64 // value of g0.sched.sp (see comments around its use) + + dwarfReader *dwarf.Reader } type savedLR struct { @@ -162,7 +190,7 @@ func newStackIterator(bi *BinaryInfo, mem MemoryReadWriter, regs op.DwarfRegiste } } } - return &stackIterator{pc: regs.PC(), regs: regs, top: true, bi: bi, mem: mem, err: nil, atend: false, stackhi: stackhi, stackBarrierPC: stackBarrierPC, stkbar: stkbar, systemstack: systemstack, g: g, g0_sched_sp: g0_sched_sp} + return &stackIterator{pc: regs.PC(), regs: regs, top: true, bi: bi, mem: mem, err: nil, atend: false, stackhi: stackhi, stackBarrierPC: stackBarrierPC, stkbar: stkbar, systemstack: systemstack, g: g, g0_sched_sp: g0_sched_sp, dwarfReader: bi.dwarf.Reader()} } // Next points the iterator to the next stack frame. @@ -305,9 +333,8 @@ func (it *stackIterator) Err() error { // frameBase calculates the frame base pseudo-register for DWARF for fn and // the current frame. func (it *stackIterator) frameBase(fn *Function) int64 { - rdr := it.bi.dwarf.Reader() - rdr.Seek(fn.offset) - e, err := rdr.Next() + it.dwarfReader.Seek(fn.offset) + e, err := it.dwarfReader.Next() if err != nil { return 0 } @@ -327,7 +354,7 @@ func (it *stackIterator) newStackframe(ret, retaddr uint64) Stackframe { } else { it.regs.FrameBase = it.frameBase(fn) } - r := Stackframe{Current: Location{PC: it.pc, File: f, Line: l, Fn: fn}, Regs: it.regs, Ret: ret, addrret: retaddr, stackHi: it.stackhi, SystemStack: it.systemstack} + r := Stackframe{Current: Location{PC: it.pc, File: f, Line: l, Fn: fn}, Regs: it.regs, Ret: ret, addrret: retaddr, stackHi: it.stackhi, SystemStack: it.systemstack, lastpc: it.pc} if !it.top { fnname := "" if r.Current.Fn != nil { @@ -339,6 +366,7 @@ func (it *stackIterator) newStackframe(ret, retaddr uint64) Stackframe { // instruction to look for at pc - 1 r.Call = r.Current default: + r.lastpc = it.pc - 1 r.Call.File, r.Call.Line, r.Call.Fn = it.bi.PCToLine(it.pc - 1) if r.Call.Fn == nil { r.Call.File = "?" @@ -358,7 +386,7 @@ func (it *stackIterator) stacktrace(depth int) ([]Stackframe, error) { } frames := make([]Stackframe, 0, depth+1) for it.Next() { - frames = append(frames, it.Frame()) + frames = it.appendInlineCalls(frames, it.Frame()) if len(frames) >= depth+1 { break } @@ -372,6 +400,60 @@ func (it *stackIterator) stacktrace(depth int) ([]Stackframe, error) { return frames, nil } +func (it *stackIterator) appendInlineCalls(frames []Stackframe, frame Stackframe) []Stackframe { + if frame.Call.Fn == nil { + return append(frames, frame) + } + if frame.Call.Fn.cu.lineInfo == nil { + return append(frames, frame) + } + + callpc := frame.Call.PC + if len(frames) > 0 { + callpc-- + } + + irdr := reader.InlineStack(it.bi.dwarf, frame.Call.Fn.offset, callpc) + for irdr.Next() { + entry, offset := reader.LoadAbstractOrigin(irdr.Entry(), it.dwarfReader) + + fnname, okname := entry.Val(dwarf.AttrName).(string) + fileidx, okfileidx := entry.Val(dwarf.AttrCallFile).(int64) + line, okline := entry.Val(dwarf.AttrCallLine).(int64) + + if !okname || !okfileidx || !okline { + break + } + if fileidx-1 < 0 || fileidx-1 >= int64(len(frame.Current.Fn.cu.lineInfo.FileNames)) { + break + } + + inlfn := &Function{Name: fnname, Entry: frame.Call.Fn.Entry, End: frame.Call.Fn.End, offset: offset, cu: frame.Call.Fn.cu} + frames = append(frames, Stackframe{ + Current: frame.Current, + Call: Location{ + frame.Call.PC, + frame.Call.File, + frame.Call.Line, + inlfn, + }, + Regs: frame.Regs, + stackHi: frame.stackHi, + Ret: frame.Ret, + addrret: frame.addrret, + Err: frame.Err, + SystemStack: frame.SystemStack, + Inlined: true, + lastpc: frame.lastpc, + }) + + frame.Call.File = frame.Current.Fn.cu.lineInfo.FileNames[fileidx-1].Path + frame.Call.Line = int(line) + } + + return append(frames, frame) +} + // advanceRegs calculates it.callFrameRegs using it.regs and the frame // descriptor entry for the current stack frame. // it.regs.CallFrameCFA is updated. diff --git a/pkg/proc/test/support.go b/pkg/proc/test/support.go index c4c41aaf43df6a219da71cd8692940270c31a769..7ab98ed5dbc3c34400edd39eabc46731a3ebc227 100644 --- a/pkg/proc/test/support.go +++ b/pkg/proc/test/support.go @@ -48,6 +48,7 @@ type BuildFlags uint32 const ( LinkStrip BuildFlags = 1 << iota EnableCGOOptimization + EnableInlining ) func BuildFixture(name string, flags BuildFlags) Fixture { @@ -81,7 +82,11 @@ func BuildFixture(name string, flags BuildFlags) Fixture { if flags&LinkStrip != 0 { buildFlags = append(buildFlags, "-ldflags=-s") } - buildFlags = append(buildFlags, "-gcflags=-N -l", "-o", tmpfile) + gcflags := "-gcflags=-N -l" + if flags&EnableInlining != 0 { + gcflags = "-gcflags=-N" + } + buildFlags = append(buildFlags, gcflags, "-o", tmpfile) if *EnableRace { buildFlags = append(buildFlags, "-race") } diff --git a/pkg/proc/threads.go b/pkg/proc/threads.go index 32bb60eb48c912748796c844a1ed2bee4f6e1609..1c6cc0c5b8cbd5347e73d50a7482cf836198f323 100644 --- a/pkg/proc/threads.go +++ b/pkg/proc/threads.go @@ -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 diff --git a/pkg/proc/variables.go b/pkg/proc/variables.go index c2efc9dc5356d8a5569c1c0ed4cf01ff235c1c4f..a7ec8c2ebe5a2df6b4c9645cd46c96742fdb9963 100644 --- a/pkg/proc/variables.go +++ b/pkg/proc/variables.go @@ -156,13 +156,15 @@ type G struct { // EvalScope is the scope for variable evaluation. Contains the thread, // current location (PC), and canonical frame address. type EvalScope struct { - PC uint64 // Current instruction of the evaluation frame + Location Regs op.DwarfRegisters Mem MemoryReadWriter // Target's memory Gvar *Variable BinInfo *BinaryInfo frameOffset int64 + + aordr *dwarf.Reader // extra reader to load DW_AT_abstract_origin entries, do not initialize } // IsNilErr is returned when a variable is nil. @@ -175,7 +177,7 @@ func (err *IsNilErr) Error() string { } func globalScope(bi *BinaryInfo, mem MemoryReadWriter) *EvalScope { - return &EvalScope{PC: 0, Regs: op.DwarfRegisters{}, Mem: mem, Gvar: nil, BinInfo: bi, frameOffset: 0} + return &EvalScope{Location: Location{}, Regs: op.DwarfRegisters{}, Mem: mem, Gvar: nil, BinInfo: bi, frameOffset: 0} } func (scope *EvalScope) newVariable(name string, addr uintptr, dwarfType godwarf.Type, mem MemoryReadWriter) *Variable { @@ -1873,16 +1875,13 @@ func (v *variablesByDepth) Swap(i int, j int) { // Fetches all variables of a specific type in the current function scope func (scope *EvalScope) variablesByTag(tag dwarf.Tag, cfg *LoadConfig) ([]*Variable, error) { - fn := scope.BinInfo.PCToFunc(scope.PC) - if fn == nil { + if scope.Fn == nil { return nil, errors.New("unable to find function context") } - _, line, _ := scope.BinInfo.PCToLine(scope.PC) - var vars []*Variable var depths []int - varReader := reader.Variables(scope.BinInfo.dwarf, fn.offset, scope.PC, line, tag == dwarf.TagVariable) + varReader := reader.Variables(scope.BinInfo.dwarf, scope.Fn.offset, scope.PC, scope.Line, tag == dwarf.TagVariable) hasScopes := false for varReader.Next() { entry := varReader.Entry()