// Copyright 2016 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package dwarfgen import ( "cmp" "debug/dwarf" "fmt" "internal/platform" "internal/testenv" "os" "path/filepath" "runtime" "slices" "strconv" "strings" "testing" "cmd/internal/objfile" ) type testline struct { // line is one line of go source line string // scopes is a list of scope IDs of all the lexical scopes that this line // of code belongs to. // Scope IDs are assigned by traversing the tree of lexical blocks of a // function in pre-order // Scope IDs are function specific, i.e. scope 0 is always the root scope // of the function that this line belongs to. Empty scopes are not assigned // an ID (because they are not saved in debug_info). // Scope 0 is always omitted from this list since all lines always belong // to it. scopes []int // vars is the list of variables that belong in scopes[len(scopes)-1]. // Local variables are prefixed with "var ", formal parameters with "arg ". // Must be ordered alphabetically. // Set to nil to skip the check. vars []string // decl is the list of variables declared at this line. decl []string // declBefore is the list of variables declared at or before this line. declBefore []string } var testfile = []testline{ {line: "package main"}, {line: "var sink any"}, {line: "func f1(x int) { }"}, {line: "func f2(x int) { }"}, {line: "func f3(x int) { }"}, {line: "func f4(x int) { }"}, {line: "func f5(x int) { }"}, {line: "func f6(x int) { }"}, {line: "func leak(x interface{}) { sink = x }"}, {line: "func gret1() int { return 2 }"}, {line: "func gretbool() bool { return true }"}, {line: "func gret3() (int, int, int) { return 0, 1, 2 }"}, {line: "var v = []int{ 0, 1, 2 }"}, {line: "var ch = make(chan int)"}, {line: "var floatch = make(chan float64)"}, {line: "var iface interface{}"}, {line: "func TestNestedFor() {", vars: []string{"var a int"}}, {line: " a := 0", decl: []string{"a"}}, {line: " f1(a)"}, {line: " for i := 0; i < 5; i++ {", scopes: []int{1}, vars: []string{"var i int"}, decl: []string{"i"}}, {line: " f2(i)", scopes: []int{1}}, {line: " for i := 0; i < 5; i++ {", scopes: []int{1, 2}, vars: []string{"var i int"}, decl: []string{"i"}}, {line: " f3(i)", scopes: []int{1, 2}}, {line: " }"}, {line: " f4(i)", scopes: []int{1}}, {line: " }"}, {line: " f5(a)"}, {line: "}"}, {line: "func TestOas2() {", vars: []string{}}, {line: " if a, b, c := gret3(); a != 1 {", scopes: []int{1}, vars: []string{"var a int", "var b int", "var c int"}}, {line: " f1(a)", scopes: []int{1}}, {line: " f1(b)", scopes: []int{1}}, {line: " f1(c)", scopes: []int{1}}, {line: " }"}, {line: " for i, x := range v {", scopes: []int{2}, vars: []string{"var i int", "var x int"}}, {line: " f1(i)", scopes: []int{2}}, {line: " f1(x)", scopes: []int{2}}, {line: " }"}, {line: " if a, ok := <- ch; ok {", scopes: []int{3}, vars: []string{"var a int", "var ok bool"}}, {line: " f1(a)", scopes: []int{3}}, {line: " }"}, {line: " if a, ok := iface.(int); ok {", scopes: []int{4}, vars: []string{"var a int", "var ok bool"}}, {line: " f1(a)", scopes: []int{4}}, {line: " }"}, {line: "}"}, {line: "func TestIfElse() {"}, {line: " if x := gret1(); x != 0 {", scopes: []int{1}, vars: []string{"var x int"}}, {line: " a := 0", scopes: []int{1, 2}, vars: []string{"var a int"}}, {line: " f1(a); f1(x)", scopes: []int{1, 2}}, {line: " } else {"}, {line: " b := 1", scopes: []int{1, 3}, vars: []string{"var b int"}}, {line: " f1(b); f1(x+1)", scopes: []int{1, 3}}, {line: " }"}, {line: "}"}, {line: "func TestSwitch() {", vars: []string{}}, {line: " switch x := gret1(); x {", scopes: []int{1}, vars: []string{"var x int"}}, {line: " case 0:", scopes: []int{1, 2}}, {line: " i := x + 5", scopes: []int{1, 2}, vars: []string{"var i int"}}, {line: " f1(x); f1(i)", scopes: []int{1, 2}}, {line: " case 1:", scopes: []int{1, 3}}, {line: " j := x + 10", scopes: []int{1, 3}, vars: []string{"var j int"}}, {line: " f1(x); f1(j)", scopes: []int{1, 3}}, {line: " case 2:", scopes: []int{1, 4}}, {line: " k := x + 2", scopes: []int{1, 4}, vars: []string{"var k int"}}, {line: " f1(x); f1(k)", scopes: []int{1, 4}}, {line: " }"}, {line: "}"}, {line: "func TestTypeSwitch() {", vars: []string{}}, {line: " switch x := iface.(type) {"}, {line: " case int:", scopes: []int{1}}, {line: " f1(x)", scopes: []int{1}, vars: []string{"var x int"}}, {line: " case uint8:", scopes: []int{2}}, {line: " f1(int(x))", scopes: []int{2}, vars: []string{"var x uint8"}}, {line: " case float64:", scopes: []int{3}}, {line: " f1(int(x)+1)", scopes: []int{3}, vars: []string{"var x float64"}}, {line: " }"}, {line: "}"}, {line: "func TestSelectScope() {"}, {line: " select {"}, {line: " case i := <- ch:", scopes: []int{1}}, {line: " f1(i)", scopes: []int{1}, vars: []string{"var i int"}}, {line: " case f := <- floatch:", scopes: []int{2}}, {line: " f1(int(f))", scopes: []int{2}, vars: []string{"var f float64"}}, {line: " }"}, {line: "}"}, {line: "func TestBlock() {", vars: []string{"var a int"}}, {line: " a := 1"}, {line: " {"}, {line: " b := 2", scopes: []int{1}, vars: []string{"var b int"}}, {line: " f1(b)", scopes: []int{1}}, {line: " f1(a)", scopes: []int{1}}, {line: " }"}, {line: "}"}, {line: "func TestDiscontiguousRanges() {", vars: []string{"var a int"}}, {line: " a := 0"}, {line: " f1(a)"}, {line: " {"}, {line: " b := 0", scopes: []int{1}, vars: []string{"var b int"}}, {line: " f2(b)", scopes: []int{1}}, {line: " if gretbool() {", scopes: []int{1}}, {line: " c := 0", scopes: []int{1, 2}, vars: []string{"var c int"}}, {line: " f3(c)", scopes: []int{1, 2}}, {line: " } else {"}, {line: " c := 1.1", scopes: []int{1, 3}, vars: []string{"var c float64"}}, {line: " f4(int(c))", scopes: []int{1, 3}}, {line: " }"}, {line: " f5(b)", scopes: []int{1}}, {line: " }"}, {line: " f6(a)"}, {line: "}"}, {line: "func TestClosureScope() {", vars: []string{"var a int", "var b int", "var f func(int)"}}, {line: " a := 1; b := 1"}, {line: " f := func(c int) {", scopes: []int{0}, vars: []string{"arg c int", "var &b *int", "var a int", "var d int"}, declBefore: []string{"&b", "a"}}, {line: " d := 3"}, {line: " f1(c); f1(d)"}, {line: " if e := 3; e != 0 {", scopes: []int{1}, vars: []string{"var e int"}}, {line: " f1(e)", scopes: []int{1}}, {line: " f1(a)", scopes: []int{1}}, {line: " b = 2", scopes: []int{1}}, {line: " }"}, {line: " }"}, {line: " f(3); f1(b)"}, {line: "}"}, {line: "func TestEscape() {"}, {line: " a := 1", vars: []string{"var a int"}}, {line: " {"}, {line: " b := 2", scopes: []int{1}, vars: []string{"var &b *int", "var p *int"}}, {line: " p := &b", scopes: []int{1}}, {line: " f1(a)", scopes: []int{1}}, {line: " leak(p)", scopes: []int{1}}, {line: " }"}, {line: "}"}, {line: "var fglob func() int"}, {line: "func TestCaptureVar(flag bool) {"}, {line: " a := 1", vars: []string{"arg flag bool", "var a int"}}, // TODO(register args) restore "arg ~r1 func() int", {line: " if flag {"}, {line: " b := 2", scopes: []int{1}, vars: []string{"var b int", "var f func() int"}}, {line: " f := func() int {", scopes: []int{1, 0}}, {line: " return b + 1"}, {line: " }"}, {line: " fglob = f", scopes: []int{1}}, {line: " }"}, {line: " f1(a)"}, {line: "}"}, {line: "func main() {"}, {line: " TestNestedFor()"}, {line: " TestOas2()"}, {line: " TestIfElse()"}, {line: " TestSwitch()"}, {line: " TestTypeSwitch()"}, {line: " TestSelectScope()"}, {line: " TestBlock()"}, {line: " TestDiscontiguousRanges()"}, {line: " TestClosureScope()"}, {line: " TestEscape()"}, {line: " TestCaptureVar(true)"}, {line: "}"}, } const detailOutput = false // Compiles testfile checks that the description of lexical blocks emitted // by the linker in debug_info, for each function in the main package, // corresponds to what we expect it to be. func TestScopeRanges(t *testing.T) { testenv.MustHaveGoBuild(t) t.Parallel() if !platform.ExecutableHasDWARF(runtime.GOOS, runtime.GOARCH) { t.Skipf("skipping on %s/%s: no DWARF symbol table in executables", runtime.GOOS, runtime.GOARCH) } src, f := gobuild(t, t.TempDir(), false, testfile) defer f.Close() // the compiler uses forward slashes for paths even on windows src = strings.Replace(src, "\\", "/", -1) pcln, err := f.PCLineTable() if err != nil { t.Fatal(err) } dwarfData, err := f.DWARF() if err != nil { t.Fatal(err) } dwarfReader := dwarfData.Reader() lines := make(map[line][]*lexblock) for { entry, err := dwarfReader.Next() if err != nil { t.Fatal(err) } if entry == nil { break } if entry.Tag != dwarf.TagSubprogram { continue } name, ok := entry.Val(dwarf.AttrName).(string) if !ok || !strings.HasPrefix(name, "main.Test") { continue } var scope lexblock ctxt := scopexplainContext{ dwarfData: dwarfData, dwarfReader: dwarfReader, scopegen: 1, } readScope(&ctxt, &scope, entry) scope.markLines(pcln, lines) } anyerror := false for i := range testfile { tgt := testfile[i].scopes out := lines[line{src, i + 1}] if detailOutput { t.Logf("%s // %v", testfile[i].line, out) } scopesok := checkScopes(tgt, out) if !scopesok { t.Logf("mismatch at line %d %q: expected: %v got: %v\n", i, testfile[i].line, tgt, scopesToString(out)) } varsok := true if testfile[i].vars != nil { if len(out) > 0 { varsok = checkVars(testfile[i].vars, out[len(out)-1].vars) if !varsok { t.Logf("variable mismatch at line %d %q for scope %d: expected: %v got: %v\n", i+1, testfile[i].line, out[len(out)-1].id, testfile[i].vars, out[len(out)-1].vars) } for j := range testfile[i].decl { if line := declLineForVar(out[len(out)-1].vars, testfile[i].decl[j]); line != i+1 { t.Errorf("wrong declaration line for variable %s, expected %d got: %d", testfile[i].decl[j], i+1, line) } } for j := range testfile[i].declBefore { if line := declLineForVar(out[len(out)-1].vars, testfile[i].declBefore[j]); line > i+1 { t.Errorf("wrong declaration line for variable %s, expected %d (or less) got: %d", testfile[i].declBefore[j], i+1, line) } } } } anyerror = anyerror || !scopesok || !varsok } if anyerror { t.Fatalf("mismatched output") } } func scopesToString(v []*lexblock) string { r := make([]string, len(v)) for i, s := range v { r[i] = strconv.Itoa(s.id) } return "[ " + strings.Join(r, ", ") + " ]" } func checkScopes(tgt []int, out []*lexblock) bool { if len(out) > 0 { // omit scope 0 out = out[1:] } if len(tgt) != len(out) { return false } for i := range tgt { if tgt[i] != out[i].id { return false } } return true } func checkVars(tgt []string, out []variable) bool { if len(tgt) != len(out) { return false } for i := range tgt { if tgt[i] != out[i].expr { return false } } return true } func declLineForVar(scope []variable, name string) int { for i := range scope { if scope[i].name() == name { return scope[i].declLine } } return -1 } type lexblock struct { id int ranges [][2]uint64 vars []variable scopes []lexblock } type variable struct { expr string declLine int } func (v *variable) name() string { return strings.Split(v.expr, " ")[1] } type line struct { file string lineno int } type scopexplainContext struct { dwarfData *dwarf.Data dwarfReader *dwarf.Reader scopegen int } // readScope reads the DW_TAG_lexical_block or the DW_TAG_subprogram in // entry and writes a description in scope. // Nested DW_TAG_lexical_block entries are read recursively. func readScope(ctxt *scopexplainContext, scope *lexblock, entry *dwarf.Entry) { var err error scope.ranges, err = ctxt.dwarfData.Ranges(entry) if err != nil { panic(err) } for { e, err := ctxt.dwarfReader.Next() if err != nil { panic(err) } switch e.Tag { case 0: slices.SortFunc(scope.vars, func(a, b variable) int { return cmp.Compare(a.expr, b.expr) }) return case dwarf.TagFormalParameter: typ, err := ctxt.dwarfData.Type(e.Val(dwarf.AttrType).(dwarf.Offset)) if err != nil { panic(err) } scope.vars = append(scope.vars, entryToVar(e, "arg", typ)) case dwarf.TagVariable: typ, err := ctxt.dwarfData.Type(e.Val(dwarf.AttrType).(dwarf.Offset)) if err != nil { panic(err) } scope.vars = append(scope.vars, entryToVar(e, "var", typ)) case dwarf.TagLexDwarfBlock: scope.scopes = append(scope.scopes, lexblock{id: ctxt.scopegen}) ctxt.scopegen++ readScope(ctxt, &scope.scopes[len(scope.scopes)-1], e) } } } func entryToVar(e *dwarf.Entry, kind string, typ dwarf.Type) variable { return variable{ fmt.Sprintf("%s %s %s", kind, e.Val(dwarf.AttrName).(string), typ.String()), int(e.Val(dwarf.AttrDeclLine).(int64)), } } // markLines marks all lines that belong to this scope with this scope // Recursively calls markLines for all children scopes. func (scope *lexblock) markLines(pcln objfile.Liner, lines map[line][]*lexblock) { for _, r := range scope.ranges { for pc := r[0]; pc < r[1]; pc++ { file, lineno, _ := pcln.PCToLine(pc) l := line{file, lineno} if len(lines[l]) == 0 || lines[l][len(lines[l])-1] != scope { lines[l] = append(lines[l], scope) } } } for i := range scope.scopes { scope.scopes[i].markLines(pcln, lines) } } func gobuild(t *testing.T, dir string, optimized bool, testfile []testline) (string, *objfile.File) { src := filepath.Join(dir, "test.go") dst := filepath.Join(dir, "out.o") f, err := os.Create(src) if err != nil { t.Fatal(err) } for i := range testfile { f.Write([]byte(testfile[i].line)) f.Write([]byte{'\n'}) } f.Close() args := []string{"build"} if !optimized { args = append(args, "-gcflags=-N -l") } args = append(args, "-o", dst, src) cmd := testenv.Command(t, testenv.GoToolPath(t), args...) if b, err := cmd.CombinedOutput(); err != nil { t.Logf("build: %s\n", string(b)) t.Fatal(err) } pkg, err := objfile.Open(dst) if err != nil { t.Fatal(err) } return src, pkg } // TestEmptyDwarfRanges tests that no list entry in debug_ranges has start == end. // See issue #23928. func TestEmptyDwarfRanges(t *testing.T) { testenv.MustHaveGoRun(t) t.Parallel() if !platform.ExecutableHasDWARF(runtime.GOOS, runtime.GOARCH) { t.Skipf("skipping on %s/%s: no DWARF symbol table in executables", runtime.GOOS, runtime.GOARCH) } _, f := gobuild(t, t.TempDir(), true, []testline{{line: "package main"}, {line: "func main(){ println(\"hello\") }"}}) defer f.Close() dwarfData, err := f.DWARF() if err != nil { t.Fatal(err) } dwarfReader := dwarfData.Reader() for { entry, err := dwarfReader.Next() if err != nil { t.Fatal(err) } if entry == nil { break } ranges, err := dwarfData.Ranges(entry) if err != nil { t.Fatal(err) } if ranges == nil { continue } for _, rng := range ranges { if rng[0] == rng[1] { t.Errorf("range entry with start == end: %v", rng) } } } }