proc: initial stepping with range-over-func support (#3736)

Initial support for stepping in functions that use the new
range-over-func statement in go1.23.
Does not support:

- inlining
- viewing variables of the enclosing function from a range-over-func
  body closure
- the correct way to find the enclosing function from a range-over-func
  body closure (but it should work most of the time).

Updates #3733
This commit is contained in:
Alessandro Arzilli
2024-06-04 21:52:30 +02:00
committed by GitHub
parent 40670aadc2
commit 2ec2e831d6
6 changed files with 1218 additions and 165 deletions

328
_fixtures/rangeoverfunc.go Normal file
View File

@ -0,0 +1,328 @@
package main
// The tests here are adapted from $GOROOT/src/cmd/compile/internal/rangefunc/rangefunc_test.go
import (
"fmt"
)
/*
THESE
LINES
INTENTIONALLY
LEFT
BLANK
*/
func TestTrickyIterAll() {
trickItAll := TrickyIterator{}
i := 0
for _, x := range trickItAll.iterAll([]int{30, 7, 8, 9, 10}) {
i += x
if i >= 36 {
break
}
}
fmt.Println("Got i = ", i)
}
func TestTrickyIterAll2() {
trickItAll := TrickyIterator{}
i := 0
for _, x := range trickItAll.iterAll([]int{42, 47}) {
i += x
}
fmt.Println(i)
}
func TestBreak1() {
var result []int
for _, x := range OfSliceIndex([]int{-1, -2, -4}) {
if x == -4 {
break
}
for _, y := range OfSliceIndex([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}) {
if y == 3 {
break
}
result = append(result, y)
}
result = append(result, x)
}
fmt.Println(result)
}
func TestBreak2() {
var result []int
outer:
for _, x := range OfSliceIndex([]int{-1, -2, -4}) {
for _, y := range OfSliceIndex([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}) {
if y == 3 {
break
}
if x == -4 {
break outer
}
result = append(result, y)
}
result = append(result, x)
}
fmt.Println(result)
}
func TestMultiCont0() {
var result []int
W:
for _, w := range OfSliceIndex([]int{1000, 2000}) {
result = append(result, w)
if w == 2000 {
break
}
for _, x := range OfSliceIndex([]int{100, 200, 300, 400}) {
for _, y := range OfSliceIndex([]int{10, 20, 30, 40}) {
result = append(result, y)
for _, z := range OfSliceIndex([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}) {
if z&1 == 1 {
continue
}
result = append(result, z)
if z >= 4 {
continue W // modified to be multilevel
}
}
result = append(result, -y) // should never be executed
}
result = append(result, x)
}
}
fmt.Println(result)
}
func TestPanickyIterator1() {
var result []int
defer func() {
r := recover()
fmt.Println("Recovering", r)
}()
for _, z := range PanickyOfSliceIndex([]int{1, 2, 3, 4}) {
result = append(result, z)
if z == 4 {
break
}
}
fmt.Println(result)
}
func TestPanickyIterator2() {
var result []int
defer func() {
r := recover()
fmt.Println("Recovering ", r)
}()
for _, x := range OfSliceIndex([]int{100, 200}) {
result = append(result, x)
Y:
// swallows panics and iterates to end BUT `break Y` disables the body, so--> 10, 1, 2
for _, y := range VeryBadOfSliceIndex([]int{10, 20}) {
result = append(result, y)
// converts early exit into a panic --> 1, 2
for k, z := range PanickyOfSliceIndex([]int{1, 2}) { // iterator panics
result = append(result, z)
if k == 1 {
break Y
}
}
}
}
}
func TestPanickyIteratorWithNewDefer() {
var result []int
defer func() {
r := recover()
fmt.Println("Recovering ", r)
}()
for _, x := range OfSliceIndex([]int{100, 200}) {
result = append(result, x)
Y:
// swallows panics and iterates to end BUT `break Y` disables the body, so--> 10, 1, 2
for _, y := range VeryBadOfSliceIndex([]int{10, 20}) {
defer func() { // This defer will be set on TestPanickyIteratorWithNewDefer from TestPanickyIteratorWithNewDefer-range2
fmt.Println("y loop defer")
}()
result = append(result, y)
// converts early exit into a panic --> 1, 2
for k, z := range PanickyOfSliceIndex([]int{1, 2}) { // iterator panics
result = append(result, z)
if k == 1 {
break Y
}
}
}
}
}
func TestLongReturnWrapper() {
TestLongReturn()
fmt.Println("returned")
}
func TestLongReturn() {
for _, x := range OfSliceIndex([]int{1, 2, 3}) {
for _, y := range OfSliceIndex([]int{10, 20, 30}) {
if y == 10 {
return
}
}
fmt.Println(x)
}
}
func TestGotoA1() {
result := []int{}
for _, x := range OfSliceIndex([]int{-1, -4, -5}) {
result = append(result, x)
if x == -4 {
break
}
for _, y := range OfSliceIndex([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}) {
if y == 3 {
goto A
}
result = append(result, y)
}
A:
result = append(result, x)
}
fmt.Println(result)
}
func TestGotoB1() {
result := []int{}
for _, x := range OfSliceIndex([]int{-1, -2, -3, -4, -5}) {
result = append(result, x)
if x == -4 {
break
}
for _, y := range OfSliceIndex([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}) {
if y == 3 {
goto B
}
result = append(result, y)
}
result = append(result, x)
}
B:
result = append(result, 999)
fmt.Println(result)
}
func main() {
TestTrickyIterAll()
TestTrickyIterAll2()
TestBreak1()
TestBreak2()
TestMultiCont0()
TestPanickyIterator1()
TestPanickyIterator2()
TestPanickyIteratorWithNewDefer()
TestLongReturnWrapper()
TestGotoA1()
TestGotoB1()
}
type Seq[T any] func(yield func(T) bool)
type Seq2[T1, T2 any] func(yield func(T1, T2) bool)
type TrickyIterator struct {
yield func(int, int) bool
}
func (ti *TrickyIterator) iterEcho(s []int) Seq2[int, int] {
return func(yield func(int, int) bool) {
for i, v := range s {
if !yield(i, v) {
ti.yield = yield
return
}
if ti.yield != nil && !ti.yield(i, v) {
return
}
}
ti.yield = yield
return
}
}
func (ti *TrickyIterator) iterAll(s []int) Seq2[int, int] {
return func(yield func(int, int) bool) {
ti.yield = yield // Save yield for future abuse
for i, v := range s {
if !yield(i, v) {
return
}
}
return
}
}
func (ti *TrickyIterator) iterZero(s []int) Seq2[int, int] {
return func(yield func(int, int) bool) {
ti.yield = yield // Save yield for future abuse
// Don't call it at all, maybe it won't escape
return
}
}
// OfSliceIndex returns a Seq2 over the elements of s. It is equivalent
// to range s.
func OfSliceIndex[T any, S ~[]T](s S) Seq2[int, T] {
return func(yield func(int, T) bool) {
for i, v := range s {
if !yield(i, v) {
return
}
}
return
}
}
// PanickyOfSliceIndex iterates the slice but panics if it exits the loop early
func PanickyOfSliceIndex[T any, S ~[]T](s S) Seq2[int, T] {
return func(yield func(int, T) bool) {
for i, v := range s {
if !yield(i, v) {
panic(fmt.Errorf("Panicky iterator panicking"))
}
}
return
}
}
// VeryBadOfSliceIndex is "very bad" because it ignores the return value from yield
// and just keeps on iterating, and also wraps that call in a defer-recover so it can
// keep on trying after the first panic.
func VeryBadOfSliceIndex[T any, S ~[]T](s S) Seq2[int, T] {
return func(yield func(int, T) bool) {
for i, v := range s {
func() {
defer func() {
recover()
}()
yield(i, v)
}()
}
return
}
}

View File

@ -501,9 +501,24 @@ type Function struct {
trampoline bool // DW_AT_trampoline attribute set to true
// InlinedCalls lists all inlined calls to this function
InlinedCalls []InlinedCall
InlinedCalls []InlinedCall
rangeParentNameCache int // see rangeParentName
// extraCache contains informations about this function that is only needed for
// some operations and is expensive to compute or store for every function.
extraCache *functionExtra
}
type functionExtra struct {
// closureStructType is the cached struct type for closures for this function
closureStructTypeCached *godwarf.StructType
closureStructType *godwarf.StructType
// rangeParent is set when this function is a range-over-func body closure
// and points to the function that the closure was generated from.
rangeParent *Function
// rangeBodies is the list of range-over-func body closures for this
// function. Only one between rangeParent and rangeBodies should be set at
// any given time.
rangeBodies []*Function
}
// instRange returns the indexes in fn.Name of the type parameter
@ -640,43 +655,104 @@ func (fn *Function) privateRuntime() bool {
return len(name) > n && name[:n] == "runtime." && !('A' <= name[n] && name[n] <= 'Z')
}
func (fn *Function) closureStructType(bi *BinaryInfo) *godwarf.StructType {
if fn.closureStructTypeCached != nil {
return fn.closureStructTypeCached
func rangeParentName(fnname string) int {
const rangeSuffix = "-range"
ridx := strings.Index(fnname, rangeSuffix)
if ridx <= 0 {
return -1
}
dwarfTree, err := fn.cu.image.getDwarfTree(fn.offset)
if err != nil {
return nil
ok := true
for i := ridx + len(rangeSuffix); i < len(fnname); i++ {
if fnname[i] < '0' || fnname[i] > '9' {
ok = false
break
}
}
st := &godwarf.StructType{
Kind: "struct",
if !ok {
return -1
}
vars := reader.Variables(dwarfTree, 0, 0, reader.VariablesNoDeclLineCheck|reader.VariablesSkipInlinedSubroutines)
for _, v := range vars {
off, ok := v.Val(godwarf.AttrGoClosureOffset).(int64)
if ok {
n, _ := v.Val(dwarf.AttrName).(string)
typ, err := v.Type(fn.cu.image.dwarf, fn.cu.image.index, fn.cu.image.typeCache)
if err == nil {
sz := typ.Common().ByteSize
st.Field = append(st.Field, &godwarf.StructField{
Name: n,
Type: typ,
ByteOffset: off,
ByteSize: sz,
BitOffset: off * 8,
BitSize: sz * 8,
})
return ridx
}
// rangeParentName, if this function is a range-over-func body closure
// returns the name of the parent function, otherwise returns ""
func (fn *Function) rangeParentName() string {
if fn.rangeParentNameCache == 0 {
ridx := rangeParentName(fn.Name)
fn.rangeParentNameCache = ridx
}
if fn.rangeParentNameCache < 0 {
return ""
}
return fn.Name[:fn.rangeParentNameCache]
}
// extra loads informations about fn that is expensive to compute and we
// only need for a minority of the functions.
func (fn *Function) extra(bi *BinaryInfo) *functionExtra {
if fn.extraCache != nil {
return fn.extraCache
}
if fn.cu.image.Stripped() {
fn.extraCache = &functionExtra{}
return fn.extraCache
}
fn.extraCache = &functionExtra{}
// Calculate closureStructType
{
dwarfTree, err := fn.cu.image.getDwarfTree(fn.offset)
if err != nil {
return nil
}
st := &godwarf.StructType{
Kind: "struct",
}
vars := reader.Variables(dwarfTree, 0, 0, reader.VariablesNoDeclLineCheck|reader.VariablesSkipInlinedSubroutines)
for _, v := range vars {
off, ok := v.Val(godwarf.AttrGoClosureOffset).(int64)
if ok {
n, _ := v.Val(dwarf.AttrName).(string)
typ, err := v.Type(fn.cu.image.dwarf, fn.cu.image.index, fn.cu.image.typeCache)
if err == nil {
sz := typ.Common().ByteSize
st.Field = append(st.Field, &godwarf.StructField{
Name: n,
Type: typ,
ByteOffset: off,
ByteSize: sz,
BitOffset: off * 8,
BitSize: sz * 8,
})
}
}
}
if len(st.Field) > 0 {
lf := st.Field[len(st.Field)-1]
st.ByteSize = lf.ByteOffset + lf.Type.Common().ByteSize
}
fn.extraCache.closureStructType = st
}
// Find rangeParent for this function (if it is a range-over-func body closure)
if rangeParentName := fn.rangeParentName(); rangeParentName != "" {
fn.extraCache.rangeParent = bi.lookupOneFunc(rangeParentName)
}
// Find range-over-func bodies of this function
if fn.extraCache.rangeParent == nil {
for i := range bi.Functions {
fn2 := &bi.Functions[i]
if strings.HasPrefix(fn2.Name, fn.Name) && fn2.rangeParentName() == fn.Name {
fn.extraCache.rangeBodies = append(fn.extraCache.rangeBodies, fn2)
}
}
}
if len(st.Field) > 0 {
lf := st.Field[len(st.Field)-1]
st.ByteSize = lf.ByteOffset + lf.Type.Common().ByteSize
}
fn.closureStructTypeCached = st
return st
return fn.extraCache
}
type constantsMap map[dwarfRef]*constantType

View File

@ -488,120 +488,128 @@ func testseq2(t *testing.T, program string, initialLocation string, testcases []
func testseq2Args(wd string, args []string, buildFlags protest.BuildFlags, t *testing.T, program string, initialLocation string, testcases []seqTest) {
protest.AllowRecording(t)
t.Helper()
withTestProcessArgs(program, t, wd, args, buildFlags, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
checkBreakpointClear := true
var bp *proc.Breakpoint
if initialLocation != "" {
bp = setFunctionBreakpoint(p, t, initialLocation)
} else if testcases[0].cf == contContinue {
bp = setFileBreakpoint(p, t, fixture.Source, testcases[0].pos.(int))
} else if testcases[0].cf == contNothing {
// Do nothing
checkBreakpointClear = false
} else {
panic("testseq2 can not set initial breakpoint")
}
if traceTestseq2 {
t.Logf("initial breakpoint %v", bp)
}
regs, err := p.CurrentThread().Registers()
assertNoError(err, t, "Registers")
f, ln := currentLineNumber(p, t)
for i, tc := range testcases {
switch tc.cf {
case contNext:
if traceTestseq2 {
t.Log("next")
}
assertNoError(grp.Next(), t, "Next() returned an error")
case contStep:
if traceTestseq2 {
t.Log("step")
}
assertNoError(grp.Step(), t, "Step() returned an error")
case contStepout:
if traceTestseq2 {
t.Log("stepout")
}
assertNoError(grp.StepOut(), t, "StepOut() returned an error")
case contContinue:
if traceTestseq2 {
t.Log("continue")
}
assertNoError(grp.Continue(), t, "Continue() returned an error")
if i == 0 {
if traceTestseq2 {
t.Log("clearing initial breakpoint")
}
err := p.ClearBreakpoint(bp.Addr)
assertNoError(err, t, "ClearBreakpoint() returned an error")
}
case contReverseNext:
if traceTestseq2 {
t.Log("reverse-next")
}
assertNoError(grp.ChangeDirection(proc.Backward), t, "direction switch")
assertNoError(grp.Next(), t, "reverse Next() returned an error")
assertNoError(grp.ChangeDirection(proc.Forward), t, "direction switch")
case contReverseStep:
if traceTestseq2 {
t.Log("reverse-step")
}
assertNoError(grp.ChangeDirection(proc.Backward), t, "direction switch")
assertNoError(grp.Step(), t, "reverse Step() returned an error")
assertNoError(grp.ChangeDirection(proc.Forward), t, "direction switch")
case contReverseStepout:
if traceTestseq2 {
t.Log("reverse-stepout")
}
assertNoError(grp.ChangeDirection(proc.Backward), t, "direction switch")
assertNoError(grp.StepOut(), t, "reverse StepOut() returned an error")
assertNoError(grp.ChangeDirection(proc.Forward), t, "direction switch")
case contContinueToBreakpoint:
bp := setFileBreakpoint(p, t, fixture.Source, tc.pos.(int))
if traceTestseq2 {
t.Log("continue")
}
assertNoError(grp.Continue(), t, "Continue() returned an error")
err := p.ClearBreakpoint(bp.Addr)
assertNoError(err, t, "ClearBreakpoint() returned an error")
case contNothing:
// do nothing
}
testseq2intl(t, fixture, grp, p, bp, testcases)
if err := p.CurrentThread().Breakpoint().CondError; err != nil {
t.Logf("breakpoint condition error: %v", err)
}
f, ln = currentLineNumber(p, t)
regs, _ = p.CurrentThread().Registers()
pc := regs.PC()
if traceTestseq2 {
t.Logf("at %#x %s:%d", pc, f, ln)
fmt.Printf("at %#x %s:%d\n", pc, f, ln)
}
switch pos := tc.pos.(type) {
case int:
if pos >= 0 && ln != pos {
t.Fatalf("Program did not continue to correct next location expected %d was %s:%d (%#x) (testcase %d)", pos, filepath.Base(f), ln, pc, i)
}
case string:
v := strings.Split(pos, ":")
tgtln, _ := strconv.Atoi(v[1])
if !strings.HasSuffix(f, v[0]) || (ln != tgtln) {
t.Fatalf("Program did not continue to correct next location, expected %s was %s:%d (%#x) (testcase %d)", pos, filepath.Base(f), ln, pc, i)
}
case func(*proc.Target):
pos(p)
default:
panic(fmt.Errorf("unexpected type %T", pos))
}
}
if countBreakpoints(p) != 0 {
if countBreakpoints(p) != 0 && checkBreakpointClear {
t.Fatal("Not all breakpoints were cleaned up", len(p.Breakpoints().M))
}
})
}
func testseq2intl(t *testing.T, fixture protest.Fixture, grp *proc.TargetGroup, p *proc.Target, bp *proc.Breakpoint, testcases []seqTest) {
f, ln := currentLineNumber(p, t)
for i, tc := range testcases {
switch tc.cf {
case contNext:
if traceTestseq2 {
t.Log("next")
}
assertNoError(grp.Next(), t, "Next() returned an error")
case contStep:
if traceTestseq2 {
t.Log("step")
}
assertNoError(grp.Step(), t, "Step() returned an error")
case contStepout:
if traceTestseq2 {
t.Log("stepout")
}
assertNoError(grp.StepOut(), t, "StepOut() returned an error")
case contContinue:
if traceTestseq2 {
t.Log("continue")
}
assertNoError(grp.Continue(), t, "Continue() returned an error")
if i == 0 {
if traceTestseq2 {
t.Log("clearing initial breakpoint")
}
err := p.ClearBreakpoint(bp.Addr)
assertNoError(err, t, "ClearBreakpoint() returned an error")
}
case contReverseNext:
if traceTestseq2 {
t.Log("reverse-next")
}
assertNoError(grp.ChangeDirection(proc.Backward), t, "direction switch")
assertNoError(grp.Next(), t, "reverse Next() returned an error")
assertNoError(grp.ChangeDirection(proc.Forward), t, "direction switch")
case contReverseStep:
if traceTestseq2 {
t.Log("reverse-step")
}
assertNoError(grp.ChangeDirection(proc.Backward), t, "direction switch")
assertNoError(grp.Step(), t, "reverse Step() returned an error")
assertNoError(grp.ChangeDirection(proc.Forward), t, "direction switch")
case contReverseStepout:
if traceTestseq2 {
t.Log("reverse-stepout")
}
assertNoError(grp.ChangeDirection(proc.Backward), t, "direction switch")
assertNoError(grp.StepOut(), t, "reverse StepOut() returned an error")
assertNoError(grp.ChangeDirection(proc.Forward), t, "direction switch")
case contContinueToBreakpoint:
bp := setFileBreakpoint(p, t, fixture.Source, tc.pos.(int))
if traceTestseq2 {
t.Log("continue")
}
assertNoError(grp.Continue(), t, "Continue() returned an error")
err := p.ClearBreakpoint(bp.Addr)
assertNoError(err, t, "ClearBreakpoint() returned an error")
case contNothing:
// do nothing
}
if err := p.CurrentThread().Breakpoint().CondError; err != nil {
t.Logf("breakpoint condition error: %v", err)
}
f, ln = currentLineNumber(p, t)
regs, _ := p.CurrentThread().Registers()
pc := regs.PC()
_, _, fn := p.BinInfo().PCToLine(pc)
if traceTestseq2 {
t.Logf("at %#x (%s) %s:%d", pc, fn.Name, f, ln)
//fmt.Printf("at %#x %s:%d\n", pc, f, ln)
}
switch pos := tc.pos.(type) {
case int:
if pos >= 0 && ln != pos {
t.Fatalf("Program did not continue to correct next location expected %d was %s:%d (%#x) (testcase %d)", pos, filepath.Base(f), ln, pc, i)
}
case string:
v := strings.Split(pos, ":")
tgtln, _ := strconv.Atoi(v[1])
if !strings.HasSuffix(f, v[0]) || (ln != tgtln) {
t.Fatalf("Program did not continue to correct next location, expected %s was %s:%d (%#x) (testcase %d)", pos, filepath.Base(f), ln, pc, i)
}
case func(*proc.Target):
pos(p)
default:
panic(fmt.Errorf("unexpected type %T", pos))
}
}
}
func TestNextGeneral(t *testing.T) {
var testcases []nextTest
@ -6246,3 +6254,429 @@ func TestStepIntoGoroutine(t *testing.T) {
}},
})
}
func TestRangeOverFuncNext(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)
}}
}
notAtEntryPoint := func(t *testing.T) seqTest {
return seqTest{contNothing, func(p *proc.Target) {
pc := currentPC(p, t)
fn := p.BinInfo().PCToFunc(pc)
if pc == fn.Entry {
t.Fatalf("current PC is entry point")
}
}}
}
nx := func(n int) seqTest {
return seqTest{contNext, n}
}
withTestProcessArgs("rangeoverfunc", t, ".", []string{}, 0, 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 ...
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
nx(29), // if i >= 36 {
nx(32),
nx(27), // for _, x := range ...
notAtEntryPoint(t),
nx(28), // i += x
nx(29), // if i >= 36 {
nx(30), // break
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(40),
nx(41),
nx(42),
nx(40),
notAtEntryPoint(t),
nx(41),
nx(42),
nx(42), // different function from the one above...
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
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)
notAtEntryPoint(t),
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(57),
nx(58), // result = append(result, x)
nx(59),
nx(48), // for _, x := range... (x == -2)
nx(49), // if x == -4
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)
notAtEntryPoint(t),
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(57),
nx(58), // result = append(result, x)
nx(59),
nx(48), // for _, x := range... (x == -4)
nx(49), // if x == -4
nx(50), // break
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(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(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(75),
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)
nx(87), // if w == 2000
nx(90), // for _, x := range (x == 100)
nx(90),
nx(91), // for _, y := range (y == 10)
nx(91),
nx(92), // result = append(result, y)
nx(93), // for _, z := range (z == 1)
nx(93),
nx(94), // if z&1 == 1
nx(95), // continue
nx(93), // for _, z := range (z == 2)
nx(94), // if z&1 == 1
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
nx(95), // continue
nx(93), // for _, z := range (z == 4)
nx(94), // if z&1 == 1
nx(97), // result = append(result, z)
nx(98), // if z >= 4 {
nx(99), // continue W
nx(101),
nx(103),
nx(105),
nx(85), // for _, w := range (w == 2000)
nx(86), // result = append(result, w)
nx(87), // if w == 2000
nx(88), // break
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(187),
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(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(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(223),
nx(225),
nx(227), // result = append(result, 999)
nx(228), // fmt.Println
})
})
})
}
func TestRangeOverFuncStepOut(t *testing.T) {
if !goversion.VersionAfterOrEqual(runtime.Version(), 1, 23) {
t.Skip("N/A")
}
testseq2(t, "rangeoverfunc", "", []seqTest{
{contContinue, 97},
{contStepout, 237},
})
}

View File

@ -197,6 +197,8 @@ type stackIterator struct {
g0_sched_sp uint64 // value of g0.sched.sp (see comments around its use)
g0_sched_sp_loaded bool // g0_sched_sp was loaded from g0
count int
opts StacktraceOptions
}
@ -353,17 +355,11 @@ func (it *stackIterator) stacktrace(depth int) ([]Stackframe, error) {
if depth < 0 {
return nil, errors.New("negative maximum stack depth")
}
if it.opts&StacktraceG != 0 && it.g != nil {
it.switchToGoroutineStack()
it.top = true
}
frames := make([]Stackframe, 0, depth+1)
for it.Next() {
frames = it.appendInlineCalls(frames, it.Frame())
if len(frames) >= depth+1 {
break
}
}
it.stacktraceFunc(func(frame Stackframe) bool {
frames = append(frames, frame)
return len(frames) < depth+1
})
if err := it.Err(); err != nil {
if len(frames) == 0 {
return nil, err
@ -373,22 +369,37 @@ func (it *stackIterator) stacktrace(depth int) ([]Stackframe, error) {
return frames, nil
}
func (it *stackIterator) appendInlineCalls(frames []Stackframe, frame Stackframe) []Stackframe {
func (it *stackIterator) stacktraceFunc(callback func(Stackframe) bool) {
if it.opts&StacktraceG != 0 && it.g != nil {
it.switchToGoroutineStack()
it.top = true
}
for it.Next() {
if !it.appendInlineCalls(callback, it.Frame()) {
break
}
}
}
func (it *stackIterator) appendInlineCalls(callback func(Stackframe) bool, frame Stackframe) bool {
if frame.Call.Fn == nil {
return append(frames, frame)
it.count++
return callback(frame)
}
if frame.Call.Fn.cu.lineInfo == nil {
return append(frames, frame)
it.count++
return callback(frame)
}
callpc := frame.Call.PC
if len(frames) > 0 {
if it.count > 0 {
callpc--
}
dwarfTree, err := frame.Call.Fn.cu.image.getDwarfTree(frame.Call.Fn.offset)
if err != nil {
return append(frames, frame)
it.count++
return callback(frame)
}
for _, entry := range reader.InlineStack(dwarfTree, callpc) {
@ -406,7 +417,8 @@ func (it *stackIterator) appendInlineCalls(frames []Stackframe, frame Stackframe
}
inlfn := &Function{Name: fnname, Entry: frame.Call.Fn.Entry, End: frame.Call.Fn.End, offset: entry.Offset, cu: frame.Call.Fn.cu}
frames = append(frames, Stackframe{
it.count++
callback(Stackframe{
Current: frame.Current,
Call: Location{
frame.Call.PC,
@ -427,7 +439,8 @@ func (it *stackIterator) appendInlineCalls(frames []Stackframe, frame Stackframe
frame.Call.Line = int(line)
}
return append(frames, frame)
it.count++
return callback(frame)
}
// advanceRegs calculates the DwarfRegisters for a next stack frame
@ -618,6 +631,8 @@ type Defer struct {
link *Defer // Next deferred function
argSz int64 // Always 0 in Go >=1.17
rangefunc []*Defer // See explanation in $GOROOT/src/runtime/panic.go, comment to function runtime.deferrangefunc (this is the equivalent of the rangefunc variable and head fields, combined)
variable *Variable
Unreadable error
}
@ -644,7 +659,7 @@ func (g *G) readDefers(frames []Stackframe) {
}
if frames[i].TopmostDefer == nil {
frames[i].TopmostDefer = curdefer
frames[i].TopmostDefer = curdefer.topdefer()
}
if frames[i].SystemStack || curdefer.SP >= uint64(frames[i].Regs.CFA) {
@ -660,13 +675,19 @@ func (g *G) readDefers(frames []Stackframe) {
// compared with deferred frames.
i++
} else {
frames[i].Defers = append(frames[i].Defers, curdefer)
if len(curdefer.rangefunc) > 0 {
frames[i].Defers = append(frames[i].Defers, curdefer.rangefunc...)
} else {
frames[i].Defers = append(frames[i].Defers, curdefer)
}
curdefer = curdefer.Next()
}
}
}
func (d *Defer) load() {
const maxRangeFuncDefers = 10
func (d *Defer) load(canrecur bool) {
v := d.variable // +rtype _defer
v.loadValue(LoadConfig{false, 1, 0, 0, -1, 0})
if v.Unreadable != nil {
@ -701,6 +722,34 @@ func (d *Defer) load() {
if linkvar.Addr != 0 {
d.link = &Defer{variable: linkvar}
}
if canrecur {
h := v
for _, fieldname := range []string{"head", "u", "value"} {
if h == nil {
return
}
h = h.loadFieldNamed(fieldname)
}
if h != nil {
h := h.newVariable("", h.Addr, pointerTo(linkvar.DwarfType, h.bi.Arch), h.mem).maybeDereference()
if h.Addr != 0 {
hd := &Defer{variable: h}
for {
hd.load(false)
d.rangefunc = append(d.rangefunc, hd)
if hd.link == nil {
break
}
if len(d.rangefunc) > maxRangeFuncDefers {
// We don't have a way to know for sure that we haven't gone completely off-road while loading this list so limit it to an arbitrary maximum size.
break
}
hd = hd.link
}
}
}
}
}
// errSPDecreased is used when (*Defer).Next detects a corrupted linked
@ -715,13 +764,20 @@ func (d *Defer) Next() *Defer {
if d.link == nil {
return nil
}
d.link.load()
d.link.load(true)
if d.link.SP < d.SP {
d.link.Unreadable = errSPDecreased
}
return d.link
}
func (d *Defer) topdefer() *Defer {
if len(d.rangefunc) > 0 {
return d.rangefunc[0]
}
return d
}
// EvalScope returns an EvalScope relative to the argument frame of this deferred call.
// The argument frame of a deferred call is stored in memory immediately
// after the deferred header.
@ -813,3 +869,89 @@ func ruleString(rule *frame.DWRule, regnumToString func(uint64) string) string {
return fmt.Sprintf("unknown_rule(%d)", rule.Rule)
}
}
// rangeFuncStackTrace, if the topmost frame of the stack is a the body of a
// range-over-func statement, returns a slice containing the stack of range
// bodies on the stack, the frame of the function containing them and
// finally the function that called it.
//
// For example, given:
//
// func f() {
// for _ := range iterator1 {
// for _ := range iterator2 {
// fmt.Println() // <- YOU ARE HERE
// }
// }
// }
//
// It will return the following frames:
//
// 0. f-range2()
// 1. f-range1()
// 2. f()
// 3. function that called f()
//
// If the topmost frame of the stack is *not* the body closure of a
// range-over-func statement then nothing is returned.
func rangeFuncStackTrace(tgt *Target, g *G) ([]Stackframe, error) {
if g == nil {
return nil, nil
}
it, err := goroutineStackIterator(tgt, g, StacktraceSimple)
if err != nil {
return nil, err
}
frames := []Stackframe{}
stage := 0
var rangeParent *Function
nonMonotonicSP := false
it.stacktraceFunc(func(fr Stackframe) bool {
//TODO(range-over-func): this is a heuristic, we should use .closureptr instead
if len(frames) > 0 {
prev := &frames[len(frames)-1]
if fr.Regs.SP() <= prev.Regs.SP() {
nonMonotonicSP = true
return false
}
}
switch stage {
case 0:
frames = append(frames, fr)
rangeParent = fr.Call.Fn.extra(tgt.BinInfo()).rangeParent
stage++
if rangeParent == nil {
frames = nil
stage = 3
return false
}
case 1:
if fr.Call.Fn.offset == rangeParent.offset {
frames = append(frames, fr)
stage++
} else if fr.Call.Fn.extra(tgt.BinInfo()).rangeParent == rangeParent {
frames = append(frames, fr)
}
case 2:
frames = append(frames, fr)
stage++
return false
case 3:
return false
}
return true
})
if it.Err() != nil {
return nil, err
}
if nonMonotonicSP {
return nil, errors.New("corrupted stack (SP not monotonically decreasing)")
}
if stage != 3 {
return nil, errors.New("could not find range-over-func closure parent on the stack")
}
g.readDefers(frames)
return frames, nil
}

View File

@ -508,6 +508,16 @@ func (grp *TargetGroup) StepOut() error {
return err
}
rangeFrames, err := rangeFuncStackTrace(dbp, selg)
if err != nil {
return err
}
if rangeFrames != nil {
// There are range-over-func body closures skip all of them to the
// function containing them and its caller function.
topframe, retframe = rangeFrames[len(rangeFrames)-2], rangeFrames[len(rangeFrames)-1]
}
success := false
defer func() {
if !success {
@ -645,6 +655,7 @@ func next(dbp *Target, stepInto, inlinedStepOut bool) error {
backward := dbp.recman.GetDirection() == Backward
selg := dbp.SelectedGoroutine()
curthread := dbp.CurrentThread()
bi := dbp.BinInfo()
topframe, retframe, err := topframe(dbp, selg, curthread)
if err != nil {
return err
@ -663,6 +674,11 @@ func next(dbp *Target, stepInto, inlinedStepOut bool) error {
panic("next called with inlinedStepOut but topframe was not inlined")
}
rangeFrames, err := rangeFuncStackTrace(dbp, selg)
if err != nil {
return err
}
success := false
defer func() {
if !success {
@ -680,15 +696,14 @@ func next(dbp *Target, stepInto, inlinedStepOut bool) error {
}
}
sameGCond := sameGoroutineCondition(dbp.BinInfo(), selg, curthread.ThreadID())
sameGCond := sameGoroutineCondition(bi, selg, curthread.ThreadID())
var firstPCAfterPrologue uint64
firstPCAfterPrologue, err := FirstPCAfterPrologue(dbp, topframe.Current.Fn, false)
if err != nil {
return err
}
if backward {
firstPCAfterPrologue, err = FirstPCAfterPrologue(dbp, topframe.Current.Fn, false)
if err != nil {
return err
}
if firstPCAfterPrologue == topframe.Current.PC {
// We don't want to step into the prologue so we just execute a reverse step out instead
if err := stepOutReverse(dbp, topframe, retframe, sameGCond); err != nil {
@ -705,7 +720,7 @@ func next(dbp *Target, stepInto, inlinedStepOut bool) error {
}
}
text, err := disassemble(dbp.Memory(), regs, dbp.Breakpoints(), dbp.BinInfo(), topframe.Current.Fn.Entry, topframe.Current.Fn.End, false)
text, err := disassemble(dbp.Memory(), regs, dbp.Breakpoints(), bi, topframe.Current.Fn.Entry, topframe.Current.Fn.End, false)
if err != nil && stepInto {
return err
}
@ -720,7 +735,11 @@ func next(dbp *Target, stepInto, inlinedStepOut bool) error {
}
if !backward && !topframe.Current.Fn.cu.image.Stripped() {
_, err = setDeferBreakpoint(dbp, text, topframe, sameGCond, stepInto)
fr := topframe
if len(rangeFrames) != 0 && !stepInto {
fr = rangeFrames[len(rangeFrames)-2]
}
_, err = setDeferBreakpoint(dbp, text, fr, sameGCond, stepInto)
if err != nil {
return err
}
@ -752,7 +771,7 @@ func next(dbp *Target, stepInto, inlinedStepOut bool) error {
if inlinedStepOut {
frame = retframe
}
pcs, err = removeInlinedCalls(pcs, frame)
pcs, err = removeInlinedCalls(pcs, frame, bi)
if err != nil {
return err
}
@ -768,7 +787,7 @@ func next(dbp *Target, stepInto, inlinedStepOut bool) error {
}
if !covered {
fn := dbp.BinInfo().PCToFunc(topframe.Ret)
fn := bi.PCToFunc(topframe.Ret)
if selg != nil && fn != nil && fn.Name == "runtime.goexit" {
return nil
}
@ -776,8 +795,12 @@ func next(dbp *Target, stepInto, inlinedStepOut bool) error {
}
for _, pc := range pcs {
if !stepInto && topframe.Call.Fn.extra(bi).rangeParent != nil {
if pc < firstPCAfterPrologue {
continue
}
}
if _, err := allowDuplicateBreakpoint(dbp.SetBreakpoint(0, pc, NextBreakpoint, sameFrameCond)); err != nil {
dbp.ClearSteppingBreakpoints()
return err
}
}
@ -789,6 +812,37 @@ func next(dbp *Target, stepInto, inlinedStepOut bool) error {
}
}
// Stepping into range-over-func-bodies
if !stepInto && !inlinedStepOut {
rangeParent := topframe.Call.Fn.extra(bi).rangeParent
if rangeParent == nil {
rangeParent = topframe.Call.Fn
}
for _, fn := range rangeParent.extra(bi).rangeBodies {
if fn.Entry != 0 {
pc, err := FirstPCAfterPrologue(dbp, fn, false)
if err != nil {
return err
}
// TODO: this breakpoint must have a condition on .closureptr (https://go-review.googlesource.com/c/go/+/586975)
if _, err := allowDuplicateBreakpoint(dbp.SetBreakpoint(0, pc, NextBreakpoint, sameGCond)); err != nil {
return err
}
}
}
}
// 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)))
}
}
topframe, retframe = rangeFrames[len(rangeFrames)-2], rangeFrames[len(rangeFrames)-1]
}
// Step-out breakpoint
if !topframe.Inlined {
topframe, retframe := skipAutogeneratedWrappersOut(dbp, selg, curthread, &topframe, &retframe)
retFrameCond := astutil.And(sameGCond, frameoffCondition(retframe))
@ -801,7 +855,7 @@ func next(dbp *Target, stepInto, inlinedStepOut bool) error {
// Return address could be wrong, if we are unable to set a breakpoint
// there it's ok.
if bp != nil {
configureReturnBreakpoint(dbp.BinInfo(), bp, topframe, retFrameCond)
configureReturnBreakpoint(bi, bp, topframe, retFrameCond)
}
}
@ -914,22 +968,41 @@ func FindDeferReturnCalls(text []AsmInstruction) []uint64 {
}
// 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(pcs []uint64, topframe Stackframe) ([]uint64, error) {
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 := ""
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)
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])
}

View File

@ -494,7 +494,7 @@ func (g *G) Defer() *Defer {
return nil
}
d := &Defer{variable: dvar}
d.load()
d.load(true)
return d
}
@ -1948,7 +1948,7 @@ func (v *Variable) loadFunctionPtr(recurseLevel int, cfg LoadConfig) {
}
v.Value = constant.MakeString(fn.Name)
cst := fn.closureStructType(v.bi)
cst := fn.extra(v.bi).closureStructType
v.Len = int64(len(cst.Field))
if recurseLevel <= cfg.MaxVariableRecurse {