proc: support stepping through range-over-func statements with inlining (#3755)

Extends support for stepping through range-over-func statement to
programs compiled with inlining enabled.

Updates #3733
This commit is contained in:
Alessandro Arzilli 2024-07-01 20:22:59 +02:00 committed by GitHub
parent faac701e9f
commit 608eaa3d7c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 616 additions and 39 deletions

@ -33,15 +33,15 @@ const (
// returned. If the VariablesSkipInlinedSubroutines is set, variables from
// inlined subroutines will be skipped.
func Variables(root *godwarf.Tree, pc uint64, line int, flags VariablesFlags) []Variable {
return variablesInternal(nil, root, 0, pc, line, flags)
return variablesInternal(nil, root, 0, pc, line, flags, true)
}
// variablesInternal appends to 'v' variables from 'root'. The function calls
// itself with an incremented scope for all sub-blocks in 'root'.
func variablesInternal(v []Variable, root *godwarf.Tree, depth int, pc uint64, line int, flags VariablesFlags) []Variable {
func variablesInternal(v []Variable, root *godwarf.Tree, depth int, pc uint64, line int, flags VariablesFlags, first bool) []Variable {
switch root.Tag {
case dwarf.TagInlinedSubroutine:
if flags&VariablesSkipInlinedSubroutines != 0 {
if !first && flags&VariablesSkipInlinedSubroutines != 0 {
return v
}
fallthrough
@ -50,7 +50,7 @@ func variablesInternal(v []Variable, root *godwarf.Tree, depth int, pc uint64, l
// pc (or if we don't care about visibility).
if (flags&VariablesOnlyVisible == 0) || root.ContainsPC(pc) {
for _, child := range root.Children {
v = variablesInternal(v, child, depth+1, pc, line, flags)
v = variablesInternal(v, child, depth+1, pc, line, flags, false)
}
}
return v

@ -394,7 +394,7 @@ func (scope *EvalScope) simpleLocals(flags localsFlags, wantedName string) ([]*V
return nil, err
}
variablesFlags := reader.VariablesOnlyVisible
variablesFlags := reader.VariablesOnlyVisible | reader.VariablesSkipInlinedSubroutines
if flags&localsNoDeclLineCheck != 0 {
variablesFlags = reader.VariablesNoDeclLineCheck
}

@ -6817,6 +6817,532 @@ func TestRangeOverFuncStepOut(t *testing.T) {
})
}
func TestRangeOverFuncNextInlined(t *testing.T) {
if !goversion.VersionAfterOrEqual(runtime.Version(), 1, 23) {
t.Skip("N/A")
}
funcBreak := func(t *testing.T, fnname string) seqTest {
return seqTest{
contNothing,
func(p *proc.Target) {
setFunctionBreakpoint(p, t, fnname)
}}
}
nx := func(n int) seqTest {
return seqTest{contNext, n}
}
assertLocals := func(t *testing.T, varnames ...string) seqTest {
return seqTest{
contNothing,
func(p *proc.Target) {
scope, err := proc.GoroutineScope(p, p.CurrentThread())
assertNoError(err, t, "GoroutineScope")
vars, err := scope.Locals(0, "")
assertNoError(err, t, "Locals")
gotnames := make([]string, len(vars))
for i := range vars {
gotnames[i] = vars[i].Name
}
ok := true
if len(vars) != len(varnames) {
ok = false
} else {
for i := range vars {
if vars[i].Name != varnames[i] {
ok = false
break
}
}
}
if !ok {
t.Errorf("Wrong variable names, expected %q, got %q", varnames, gotnames)
}
},
}
}
assertEval := func(t *testing.T, exprvals ...string) seqTest {
return seqTest{
contNothing,
func(p *proc.Target) {
scope, err := proc.GoroutineScope(p, p.CurrentThread())
assertNoError(err, t, "GoroutineScope")
for i := 0; i < len(exprvals); i += 2 {
expr, tgt := exprvals[i], exprvals[i+1]
v, err := scope.EvalExpression(expr, normalLoadConfig)
if err != nil {
t.Errorf("Could not evaluate %q: %v", expr, err)
} else {
out := api.ConvertVar(v).SinglelineString()
if out != tgt {
t.Errorf("Wrong value for %q, got %q expected %q", expr, out, tgt)
}
}
}
},
}
}
withTestProcessArgs("rangeoverfunc", t, ".", []string{}, protest.EnableInlining, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
t.Run("TestTrickyIterAll1", func(t *testing.T) {
testseq2intl(t, fixture, grp, p, nil, []seqTest{
funcBreak(t, "main.TestTrickyIterAll"),
{contContinue, 24}, // TestTrickyIterAll
nx(25),
nx(26),
nx(27), // for _, x := range ...
assertLocals(t, "trickItAll", "i"),
assertEval(t, "i", "0"),
nx(27), // for _, x := range ... (TODO: this probably shouldn't be here but it's also very hard to skip stopping here a second time)
nx(28), // i += x
assertLocals(t, "trickItAll", "i", "x"),
assertEval(t,
"i", "0",
"x", "30"),
nx(29), // if i >= 36 {
nx(32),
nx(27), // for _, x := range ...
nx(28), // i += x
assertEval(t,
"i", "30",
"x", "7"),
nx(29), // if i >= 36 {
nx(30), // break
nx(27),
nx(32),
nx(34), // fmt.Println
})
})
t.Run("TestTrickyIterAll2", func(t *testing.T) {
testseq2intl(t, fixture, grp, p, nil, []seqTest{
funcBreak(t, "main.TestTrickyIterAll2"),
{contContinue, 37}, // TestTrickyIterAll2
nx(38),
nx(39),
nx(40), // for _, x := range...
nx(41),
nx(42),
nx(40),
nx(41),
nx(42),
nx(40),
nx(42),
nx(43),
})
})
t.Run("TestBreak1", func(t *testing.T) {
testseq2intl(t, fixture, grp, p, nil, []seqTest{
funcBreak(t, "main.TestBreak1"),
{contContinue, 46}, // TestBreak1
nx(47),
nx(48), // for _, x := range... (x == -1)
nx(48),
nx(49), // if x == -4
assertLocals(t, "result", "x"),
assertEval(t,
"result", "[]int len: 0, cap: 0, nil",
"x", "-1"),
nx(52), // for _, y := range... (y == 1)
nx(52),
nx(53), // if y == 3
assertLocals(t, "result", "x", "y"),
assertEval(t,
"result", "[]int len: 0, cap: 0, nil",
"x", "-1",
"y", "1"),
nx(56), // result = append(result, y)
nx(57),
nx(52), // for _, y := range... (y == 2)
nx(53), // if y == 3
assertEval(t,
"x", "-1",
"y", "2"),
nx(56), // result = append(result, y)
nx(57),
nx(52), // for _, y := range... (y == 3)
nx(53), // if y == 3
assertEval(t,
"x", "-1",
"y", "3"),
nx(54), // break
nx(52),
nx(57),
nx(58), // result = append(result, x)
nx(59),
nx(48), // for _, x := range... (x == -2)
nx(49), // if x == -4
assertEval(t,
"result", "[]int len: 3, cap: 4, [1,2,-1]",
"x", "-2"),
nx(52), // for _, y := range... (y == 1)
nx(52),
nx(53), // if y == 3
nx(56), // result = append(result, y)
nx(57),
nx(52), // for _, y := range... (y == 2)
nx(53), // if y == 3
nx(56), // result = append(result, y)
nx(57),
nx(52), // for _, y := range... (y == 3)
nx(53), // if y == 3
nx(54), // break
nx(52),
nx(57),
nx(58), // result = append(result, x)
nx(59),
nx(48), // for _, x := range... (x == -4)
assertEval(t,
"result", "[]int len: 6, cap: 8, [1,2,-1,1,2,-2]",
"x", "-4"),
nx(49), // if x == -4
nx(50), // break
nx(48),
nx(59),
nx(60),
nx(61),
})
})
t.Run("TestBreak2", func(t *testing.T) {
testseq2intl(t, fixture, grp, p, nil, []seqTest{
funcBreak(t, "main.TestBreak2"),
{contContinue, 63}, // TestBreak2
nx(64),
nx(65),
nx(66), // for _, x := range (x == -1)
nx(66),
nx(67), // for _, y := range (y == 1)
nx(67),
nx(68), // if y == 3
nx(71), // if x == -4
nx(74), // result = append(result, y)
nx(75),
nx(67), // for _, y := range (y == 2)
nx(68), // if y == 3
nx(71), // if x == -4
nx(74), // result = append(result, y)
nx(75),
nx(67), // for _, y := range (y == 3)
nx(68), // if y == 3
nx(69), // break
nx(67),
nx(75),
nx(76), // result = append(result, x)
nx(77),
nx(66), // for _, x := range (x == -2)
nx(67), // for _, y := range (y == 1)
nx(67),
nx(68), // if y == 3
nx(71), // if x == -4
nx(74), // result = append(result, y)
nx(75),
nx(67), // for _, y := range (y == 2)
nx(68), // if y == 3
nx(71), // if x == -4
nx(74), // result = append(result, y)
nx(75),
nx(67), // for _, y := range (y == 3)
nx(68), // if y == 3
nx(69), // break
nx(67),
nx(75),
nx(76), // result = append(result, x)
nx(77),
nx(66), // for _, x := range (x == -4)
nx(67), // for _, y := range (y == 1)
nx(67),
nx(68), // if y == 3
nx(71), // if x == -4
nx(72), // break outer
nx(67),
nx(75),
nx(66),
nx(77),
nx(78),
nx(79),
})
})
t.Run("TestMultiCont0", func(t *testing.T) {
testseq2intl(t, fixture, grp, p, nil, []seqTest{
funcBreak(t, "main.TestMultiCont0"),
{contContinue, 81},
nx(82),
nx(84),
nx(85), // for _, w := range (w == 1000)
nx(85),
nx(86), // result = append(result, w)
assertEval(t,
"w", "1000",
"result", "[]int len: 0, cap: 0, nil"),
nx(87), // if w == 2000
assertLocals(t, "result", "w"),
assertEval(t, "result", "[]int len: 1, cap: 1, [1000]"),
nx(90), // for _, x := range (x == 100)
nx(90),
nx(91), // for _, y := range (y == 10)
nx(91),
nx(92), // result = append(result, y)
assertLocals(t, "result", "w", "x", "y"),
assertEval(t,
"w", "1000",
"x", "100",
"y", "10"),
nx(93), // for _, z := range (z == 1)
nx(93),
nx(94), // if z&1 == 1
assertLocals(t, "result", "w", "x", "y", "z"),
assertEval(t,
"w", "1000",
"x", "100",
"y", "10",
"z", "1"),
nx(95), // continue
nx(93), // for _, z := range (z == 2)
nx(94), // if z&1 == 1
assertEval(t, "z", "2"),
nx(97), // result = append(result, z)
nx(98), // if z >= 4 {
nx(101),
nx(93), // for _, z := range (z == 3)
nx(94), // if z&1 == 1
assertEval(t, "z", "3"),
nx(95), // continue
nx(93), // for _, z := range (z == 4)
nx(94), // if z&1 == 1
assertEval(t, "z", "4"),
nx(97), // result = append(result, z)
assertEval(t, "result", "[]int len: 3, cap: 4, [1000,10,2]"),
nx(98), // if z >= 4 {
nx(99), // continue W
nx(93),
nx(101),
nx(91),
nx(103),
nx(90),
nx(105),
nx(85), // for _, w := range (w == 2000)
nx(86), // result = append(result, w)
nx(87), // if w == 2000
assertEval(t,
"w", "2000",
"result", "[]int len: 5, cap: 8, [1000,10,2,4,2000]"),
nx(88), // break
nx(85),
nx(106),
nx(107), // fmt.Println
})
})
t.Run("TestPanickyIterator1", func(t *testing.T) {
testseq2intl(t, fixture, grp, p, nil, []seqTest{
funcBreak(t, "main.TestPanickyIterator1"),
{contContinue, 110},
nx(111),
nx(112),
nx(116), // for _, z := range (z == 1)
nx(116),
nx(117), // result = append(result, z)
nx(118), // if z == 4
nx(121),
nx(116), // for _, z := range (z == 2)
nx(117), // result = append(result, z)
nx(118), // if z == 4
nx(121),
nx(116), // for _, z := range (z == 3)
nx(117), // result = append(result, z)
nx(118), // if z == 4
nx(121),
nx(116), // for _, z := range (z == 4)
nx(117), // result = append(result, z)
nx(118), // if z == 4
nx(119), // break
nx(112), // defer func()
nx(113), // r := recover()
nx(114), // fmt.Println
})
})
t.Run("TestPanickyIterator2", func(t *testing.T) {
testseq2intl(t, fixture, grp, p, nil, []seqTest{
funcBreak(t, "main.TestPanickyIterator2"),
{contContinue, 125},
nx(126),
nx(127),
nx(131), // for _, x := range (x == 100)
nx(131),
nx(132),
nx(133),
nx(135), // for _, y := range (y == 10)
nx(135),
nx(136), // result = append(result, y)
nx(139), // for k, z := range (k == 0, z == 1)
nx(139),
nx(140), // result = append(result, z)
nx(141), // if k == 1
nx(144),
nx(139), // for k, z := range (k == 1, z == 2)
nx(140), // result = append(result, z)
nx(141), // if k == 1
nx(142), // break Y
nx(135),
nx(145),
nx(127), // defer func()
nx(128), // r := recover()
nx(129), // fmt.Println
})
})
t.Run("TestPanickyIteratorWithNewDefer", func(t *testing.T) {
testseq2intl(t, fixture, grp, p, nil, []seqTest{
funcBreak(t, "main.TestPanickyIteratorWithNewDefer"),
{contContinue, 149},
nx(150),
nx(151),
nx(155), // for _, x := range (x == 100)
nx(155),
nx(156),
nx(157),
nx(159), // for _, y := range (y == 10)
nx(159),
nx(160),
nx(163), // result = append(result, y)
nx(166), // for k, z := range (k == 0, z == 1)
nx(166),
nx(167), // result = append(result, z)
nx(168), // if k == 1
nx(171),
nx(166), // for k, z := range (k == 0, z == 1)
nx(167), // result = append(result, z)
nx(168), // if k == 1
nx(169), // break Y
nx(159),
nx(172),
nx(160), // defer func()
nx(161), // fmt.Println
})
})
t.Run("TestLongReturn", func(t *testing.T) {
testseq2intl(t, fixture, grp, p, nil, []seqTest{
funcBreak(t, "main.TestLongReturn"),
{contContinue, 181},
nx(182), // for _, x := range (x == 1)
nx(182),
nx(183), // for _, y := range (y == 10)
nx(183),
nx(184), // if y == 10
nx(185), // return
nx(183),
nx(187),
nx(182),
nx(189),
nx(178), // into TestLongReturnWrapper, fmt.Println
})
})
t.Run("TestGotoA1", func(t *testing.T) {
testseq2intl(t, fixture, grp, p, nil, []seqTest{
funcBreak(t, "main.TestGotoA1"),
{contContinue, 192},
nx(193),
nx(194), // for _, x := range (x == -1)
nx(194),
nx(195), // result = append(result, x)
nx(196), // if x == -4
nx(199), // for _, y := range (y == 1)
nx(199),
nx(200), // if y == 3
nx(203), // result = append(result, y)
nx(204),
nx(199), // for _, y := range (y == 2)
nx(200), // if y == 3
nx(203), // result = append(result, y)
nx(204),
nx(199), // for _, y := range (y == 3)
nx(200), // if y == 3
nx(201), // goto A
nx(199),
nx(204),
nx(206), // result = append(result, x)
nx(207),
nx(194), // for _, x := range (x == -4)
nx(195), // result = append(result, x)
nx(196), // if x == -4
nx(197), // break
nx(194),
nx(207),
nx(208), // fmt.Println
})
})
t.Run("TestGotoB1", func(t *testing.T) {
testseq2intl(t, fixture, grp, p, nil, []seqTest{
funcBreak(t, "main.TestGotoB1"),
{contContinue, 211},
nx(212),
nx(213), // for _, x := range (x == -1)
nx(213),
nx(214), // result = append(result, x)
nx(215), // if x == -4
nx(218), // for _, y := range (y == 1)
nx(218),
nx(219), // if y == 3
nx(222), // result = append(result, y)
nx(223),
nx(218), // for _, y := range (y == 2)
nx(219), // if y == 3
nx(222), // result = append(result, y)
nx(223),
nx(218), // for _, y := range (y == 3)
nx(219), // if y == 3
nx(220), // goto B
nx(218),
nx(223),
nx(213),
nx(225),
nx(227), // result = append(result, 999)
nx(228), // fmt.Println
})
})
})
}
func TestStackwatchClearBug(t *testing.T) {
skipOn(t, "not implemented", "freebsd")
skipOn(t, "not implemented", "386")

@ -49,6 +49,8 @@ type Stackframe struct {
SystemStack bool
// Inlined is true if this frame is actually an inlined call.
Inlined bool
// hasInlines is true if this frame is a concrete function that is executing inlined calls (i.e. if there is at least one inlined call frame on top of this one).
hasInlines bool
// Bottom is true if this is the bottom of the stack
Bottom bool
@ -403,6 +405,7 @@ func (it *stackIterator) appendInlineCalls(callback func(Stackframe) bool, frame
}
for _, entry := range reader.InlineStack(dwarfTree, callpc) {
frame.hasInlines = true
fnname, okname := entry.Val(dwarf.AttrName).(string)
fileidx, okfileidx := entry.Val(dwarf.AttrCallFile).(int64)
line, okline := entry.Val(dwarf.AttrCallLine).(int64)

@ -14,6 +14,7 @@ import (
"golang.org/x/arch/ppc64/ppc64asm"
"github.com/go-delve/delve/pkg/astutil"
"github.com/go-delve/delve/pkg/dwarf/godwarf"
"github.com/go-delve/delve/pkg/dwarf/reader"
"github.com/go-delve/delve/pkg/logflags"
)
@ -779,7 +780,7 @@ func next(dbp *Target, stepInto, inlinedStepOut bool) error {
if inlinedStepOut {
frame = retframe
}
pcs, err = removeInlinedCalls(pcs, frame, bi)
pcs, err = removeInlinedCalls(pcs, &frame, bi)
if err != nil {
return err
}
@ -843,8 +844,22 @@ func next(dbp *Target, stepInto, inlinedStepOut bool) error {
// Set step-out breakpoints for range-over-func body closures
if !stepInto && selg != nil && topframe.Current.Fn.extra(bi).rangeParent != nil {
for _, fr := range rangeFrames[:len(rangeFrames)-1] {
if !fr.Inlined {
dbp.SetBreakpoint(0, fr.Current.PC, NextBreakpoint, astutil.And(sameGCond, frameoffCondition(&fr)))
retframecond := astutil.And(sameGCond, frameoffCondition(&fr))
if !fr.hasInlines {
dbp.SetBreakpoint(0, fr.Current.PC, NextBreakpoint, retframecond)
} else {
// fr.Current.PC does not belong to fr.Call.Fn, because there are inlined calls, therefore set a breakpoint on every statement of fr.Call.Fn
pcs, err := fr.Current.Fn.AllPCs("", 0)
if err != nil {
return err
}
pcs, err = removeInlinedCalls(pcs, &fr, bi)
if err != nil {
return err
}
for _, pc := range pcs {
dbp.SetBreakpoint(0, pc, NextBreakpoint, retframecond)
}
}
}
topframe, retframe = rangeFrames[len(rangeFrames)-2], rangeFrames[len(rangeFrames)-1]
@ -976,56 +991,89 @@ func FindDeferReturnCalls(text []AsmInstruction) []uint64 {
}
// Removes instructions belonging to inlined calls of topframe from pcs.
func removeInlinedCalls(pcs []uint64, topframe Stackframe, bi *BinaryInfo) ([]uint64, error) {
// Inlined calls that belong to range-over-func bodies are not removed.
func removeInlinedCalls(pcs []uint64, topframe *Stackframe, bi *BinaryInfo) ([]uint64, error) {
// TODO(derekparker) it should be possible to still use some internal
// runtime information to do this.
if topframe.Call.Fn == nil || topframe.Call.Fn.cu.image.Stripped() {
return pcs, nil
}
topframeRangeParentName := ""
topframeRangeParentName := topframe.Call.Fn.Name
if topframe.Call.Fn.extra(bi).rangeParent != nil {
topframeRangeParentName = topframe.Call.Fn.extra(bi).rangeParent.Name
}
dwarfTree, err := topframe.Call.Fn.cu.image.getDwarfTree(topframe.Call.Fn.offset)
dwarfTree, err := topframe.Current.Fn.cu.image.getDwarfTree(topframe.Current.Fn.offset)
if err != nil {
return pcs, err
}
for _, e := range reader.InlineStack(dwarfTree, 0) {
// keep all PCs that belong to topframe
if e.Offset == topframe.Call.Fn.offset {
continue
}
// also keep all PCs that belong to a range-over-func body closure that
// belongs to the same function as topframe or to the range parent of
// topframe.
fnname, _ := e.Val(dwarf.AttrName).(string)
ridx := rangeParentName(fnname)
var rpn string
if ridx == -1 {
rpn = fnname
} else {
rpn = fnname[:ridx]
}
if rpn == topframeRangeParentName {
continue
}
for _, rng := range e.Ranges {
pcs = removePCsBetween(pcs, rng[0], rng[1])
color := make([]removePC, len(pcs))
removeInlinedCallsColor(topframe, topframeRangeParentName, pcs, color, dwarfTree)
out := make([]uint64, 0, len(pcs))
for i := range pcs {
if color[i] != removePCRemove {
out = append(out, pcs[i])
}
}
return pcs, nil
return out, nil
}
func removePCsBetween(pcs []uint64, start, end uint64) []uint64 {
out := pcs[:0]
for _, pc := range pcs {
if pc < start || pc >= end {
out = append(out, pc)
type removePC uint8
const (
removePCUnknown removePC = iota
removePCRemove
removePCKeep
)
// removeInlinedCallsColor sets color[i] to removePCRemove or removePCKeep
// depending on whether pcs[i] should be removed by removeInlinedCalls.
// This determination is made by checking, for each PC, what is the topmost
// inlined call.
func removeInlinedCallsColor(topframe *Stackframe, topframeRangeParentName string, pcs []uint64, color []removePC, e *godwarf.Tree) {
switch e.Tag {
case dwarf.TagSubprogram, dwarf.TagInlinedSubroutine, dwarf.TagLexDwarfBlock:
// ok
default:
return
}
for _, child := range e.Children {
removeInlinedCallsColor(topframe, topframeRangeParentName, pcs, color, child)
}
switch e.Tag {
case dwarf.TagInlinedSubroutine:
c := removePCRemove
if e.Offset == topframe.Call.Fn.offset {
c = removePCKeep
} else {
fnname, _ := e.Val(dwarf.AttrName).(string)
ridx := rangeParentName(fnname)
var rpn string
if ridx == -1 {
rpn = fnname
} else {
rpn = fnname[:ridx]
}
if rpn == topframeRangeParentName {
c = removePCKeep
}
}
for _, rng := range e.Ranges {
colorPCsBetween(pcs, color, c, rng[0], rng[1])
}
}
}
// colorPCsBetween sets color[i] to c if start <= pcs[i] < end
func colorPCsBetween(pcs []uint64, color []removePC, c removePC, start, end uint64) {
for i, pc := range pcs {
if color[i] == removePCUnknown && pc >= start && pc < end {
color[i] = c
}
}
return out
}
func setStepIntoBreakpoint(dbp *Target, curfn *Function, text []AsmInstruction, cond ast.Expr) error {