Source file src/cmd/compile/internal/dwarfgen/scope_test.go

     1  // Copyright 2016 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package dwarfgen
     6  
     7  import (
     8  	"cmp"
     9  	"debug/dwarf"
    10  	"fmt"
    11  	"internal/platform"
    12  	"internal/testenv"
    13  	"os"
    14  	"path/filepath"
    15  	"runtime"
    16  	"slices"
    17  	"strconv"
    18  	"strings"
    19  	"testing"
    20  
    21  	"cmd/internal/objfile"
    22  )
    23  
    24  type testline struct {
    25  	// line is one line of go source
    26  	line string
    27  
    28  	// scopes is a list of scope IDs of all the lexical scopes that this line
    29  	// of code belongs to.
    30  	// Scope IDs are assigned by traversing the tree of lexical blocks of a
    31  	// function in pre-order
    32  	// Scope IDs are function specific, i.e. scope 0 is always the root scope
    33  	// of the function that this line belongs to. Empty scopes are not assigned
    34  	// an ID (because they are not saved in debug_info).
    35  	// Scope 0 is always omitted from this list since all lines always belong
    36  	// to it.
    37  	scopes []int
    38  
    39  	// vars is the list of variables that belong in scopes[len(scopes)-1].
    40  	// Local variables are prefixed with "var ", formal parameters with "arg ".
    41  	// Must be ordered alphabetically.
    42  	// Set to nil to skip the check.
    43  	vars []string
    44  
    45  	// decl is the list of variables declared at this line.
    46  	decl []string
    47  
    48  	// declBefore is the list of variables declared at or before this line.
    49  	declBefore []string
    50  }
    51  
    52  var testfile = []testline{
    53  	{line: "package main"},
    54  	{line: "var sink any"},
    55  	{line: "func f1(x int) { }"},
    56  	{line: "func f2(x int) { }"},
    57  	{line: "func f3(x int) { }"},
    58  	{line: "func f4(x int) { }"},
    59  	{line: "func f5(x int) { }"},
    60  	{line: "func f6(x int) { }"},
    61  	{line: "func leak(x interface{}) { sink = x }"},
    62  	{line: "func gret1() int { return 2 }"},
    63  	{line: "func gretbool() bool { return true }"},
    64  	{line: "func gret3() (int, int, int) { return 0, 1, 2 }"},
    65  	{line: "var v = []int{ 0, 1, 2 }"},
    66  	{line: "var ch = make(chan int)"},
    67  	{line: "var floatch = make(chan float64)"},
    68  	{line: "var iface interface{}"},
    69  	{line: "func TestNestedFor() {", vars: []string{"var a int"}},
    70  	{line: "	a := 0", decl: []string{"a"}},
    71  	{line: "	f1(a)"},
    72  	{line: "	for i := 0; i < 5; i++ {", scopes: []int{1}, vars: []string{"var i int"}, decl: []string{"i"}},
    73  	{line: "		f2(i)", scopes: []int{1}},
    74  	{line: "		for i := 0; i < 5; i++ {", scopes: []int{1, 2}, vars: []string{"var i int"}, decl: []string{"i"}},
    75  	{line: "			f3(i)", scopes: []int{1, 2}},
    76  	{line: "		}"},
    77  	{line: "		f4(i)", scopes: []int{1}},
    78  	{line: "	}"},
    79  	{line: "	f5(a)"},
    80  	{line: "}"},
    81  	{line: "func TestOas2() {", vars: []string{}},
    82  	{line: "	if a, b, c := gret3(); a != 1 {", scopes: []int{1}, vars: []string{"var a int", "var b int", "var c int"}},
    83  	{line: "		f1(a)", scopes: []int{1}},
    84  	{line: "		f1(b)", scopes: []int{1}},
    85  	{line: "		f1(c)", scopes: []int{1}},
    86  	{line: "	}"},
    87  	{line: "	for i, x := range v {", scopes: []int{2}, vars: []string{"var i int", "var x int"}},
    88  	{line: "		f1(i)", scopes: []int{2}},
    89  	{line: "		f1(x)", scopes: []int{2}},
    90  	{line: "	}"},
    91  	{line: "	if a, ok := <- ch; ok {", scopes: []int{3}, vars: []string{"var a int", "var ok bool"}},
    92  	{line: "		f1(a)", scopes: []int{3}},
    93  	{line: "	}"},
    94  	{line: "	if a, ok := iface.(int); ok {", scopes: []int{4}, vars: []string{"var a int", "var ok bool"}},
    95  	{line: "		f1(a)", scopes: []int{4}},
    96  	{line: "	}"},
    97  	{line: "}"},
    98  	{line: "func TestIfElse() {"},
    99  	{line: "	if x := gret1(); x != 0 {", scopes: []int{1}, vars: []string{"var x int"}},
   100  	{line: "		a := 0", scopes: []int{1, 2}, vars: []string{"var a int"}},
   101  	{line: "		f1(a); f1(x)", scopes: []int{1, 2}},
   102  	{line: "	} else {"},
   103  	{line: "		b := 1", scopes: []int{1, 3}, vars: []string{"var b int"}},
   104  	{line: "		f1(b); f1(x+1)", scopes: []int{1, 3}},
   105  	{line: "	}"},
   106  	{line: "}"},
   107  	{line: "func TestSwitch() {", vars: []string{}},
   108  	{line: "	switch x := gret1(); x {", scopes: []int{1}, vars: []string{"var x int"}},
   109  	{line: "	case 0:", scopes: []int{1, 2}},
   110  	{line: "		i := x + 5", scopes: []int{1, 2}, vars: []string{"var i int"}},
   111  	{line: "		f1(x); f1(i)", scopes: []int{1, 2}},
   112  	{line: "	case 1:", scopes: []int{1, 3}},
   113  	{line: "		j := x + 10", scopes: []int{1, 3}, vars: []string{"var j int"}},
   114  	{line: "		f1(x); f1(j)", scopes: []int{1, 3}},
   115  	{line: "	case 2:", scopes: []int{1, 4}},
   116  	{line: "		k := x + 2", scopes: []int{1, 4}, vars: []string{"var k int"}},
   117  	{line: "		f1(x); f1(k)", scopes: []int{1, 4}},
   118  	{line: "	}"},
   119  	{line: "}"},
   120  	{line: "func TestTypeSwitch() {", vars: []string{}},
   121  	{line: "	switch x := iface.(type) {"},
   122  	{line: "	case int:", scopes: []int{1}},
   123  	{line: "		f1(x)", scopes: []int{1}, vars: []string{"var x int"}},
   124  	{line: "	case uint8:", scopes: []int{2}},
   125  	{line: "		f1(int(x))", scopes: []int{2}, vars: []string{"var x uint8"}},
   126  	{line: "	case float64:", scopes: []int{3}},
   127  	{line: "		f1(int(x)+1)", scopes: []int{3}, vars: []string{"var x float64"}},
   128  	{line: "	}"},
   129  	{line: "}"},
   130  	{line: "func TestSelectScope() {"},
   131  	{line: "	select {"},
   132  	{line: "	case i := <- ch:", scopes: []int{1}},
   133  	{line: "		f1(i)", scopes: []int{1}, vars: []string{"var i int"}},
   134  	{line: "	case f := <- floatch:", scopes: []int{2}},
   135  	{line: "		f1(int(f))", scopes: []int{2}, vars: []string{"var f float64"}},
   136  	{line: "	}"},
   137  	{line: "}"},
   138  	{line: "func TestBlock() {", vars: []string{"var a int"}},
   139  	{line: "	a := 1"},
   140  	{line: "	{"},
   141  	{line: "		b := 2", scopes: []int{1}, vars: []string{"var b int"}},
   142  	{line: "		f1(b)", scopes: []int{1}},
   143  	{line: "		f1(a)", scopes: []int{1}},
   144  	{line: "	}"},
   145  	{line: "}"},
   146  	{line: "func TestDiscontiguousRanges() {", vars: []string{"var a int"}},
   147  	{line: "	a := 0"},
   148  	{line: "	f1(a)"},
   149  	{line: "	{"},
   150  	{line: "		b := 0", scopes: []int{1}, vars: []string{"var b int"}},
   151  	{line: "		f2(b)", scopes: []int{1}},
   152  	{line: "		if gretbool() {", scopes: []int{1}},
   153  	{line: "			c := 0", scopes: []int{1, 2}, vars: []string{"var c int"}},
   154  	{line: "			f3(c)", scopes: []int{1, 2}},
   155  	{line: "		} else {"},
   156  	{line: "			c := 1.1", scopes: []int{1, 3}, vars: []string{"var c float64"}},
   157  	{line: "			f4(int(c))", scopes: []int{1, 3}},
   158  	{line: "		}"},
   159  	{line: "		f5(b)", scopes: []int{1}},
   160  	{line: "	}"},
   161  	{line: "	f6(a)"},
   162  	{line: "}"},
   163  	{line: "func TestClosureScope() {", vars: []string{"var a int", "var b int", "var f func(int)"}},
   164  	{line: "	a := 1; b := 1"},
   165  	{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"}},
   166  	{line: "		d := 3"},
   167  	{line: "		f1(c); f1(d)"},
   168  	{line: "		if e := 3; e != 0 {", scopes: []int{1}, vars: []string{"var e int"}},
   169  	{line: "			f1(e)", scopes: []int{1}},
   170  	{line: "			f1(a)", scopes: []int{1}},
   171  	{line: "			b = 2", scopes: []int{1}},
   172  	{line: "		}"},
   173  	{line: "	}"},
   174  	{line: "	f(3); f1(b)"},
   175  	{line: "}"},
   176  	{line: "func TestEscape() {"},
   177  	{line: "	a := 1", vars: []string{"var a int"}},
   178  	{line: "	{"},
   179  	{line: "		b := 2", scopes: []int{1}, vars: []string{"var &b *int", "var p *int"}},
   180  	{line: "		p := &b", scopes: []int{1}},
   181  	{line: "		f1(a)", scopes: []int{1}},
   182  	{line: "		leak(p)", scopes: []int{1}},
   183  	{line: "	}"},
   184  	{line: "}"},
   185  	{line: "var fglob func() int"},
   186  	{line: "func TestCaptureVar(flag bool) {"},
   187  	{line: "	a := 1", vars: []string{"arg flag bool", "var a int"}}, // TODO(register args) restore "arg ~r1 func() int",
   188  	{line: "	if flag {"},
   189  	{line: "		b := 2", scopes: []int{1}, vars: []string{"var b int", "var f func() int"}},
   190  	{line: "		f := func() int {", scopes: []int{1, 0}},
   191  	{line: "			return b + 1"},
   192  	{line: "		}"},
   193  	{line: "		fglob = f", scopes: []int{1}},
   194  	{line: "	}"},
   195  	{line: "	f1(a)"},
   196  	{line: "}"},
   197  	{line: "func main() {"},
   198  	{line: "	TestNestedFor()"},
   199  	{line: "	TestOas2()"},
   200  	{line: "	TestIfElse()"},
   201  	{line: "	TestSwitch()"},
   202  	{line: "	TestTypeSwitch()"},
   203  	{line: "	TestSelectScope()"},
   204  	{line: "	TestBlock()"},
   205  	{line: "	TestDiscontiguousRanges()"},
   206  	{line: "	TestClosureScope()"},
   207  	{line: "	TestEscape()"},
   208  	{line: "	TestCaptureVar(true)"},
   209  	{line: "}"},
   210  }
   211  
   212  const detailOutput = false
   213  
   214  // Compiles testfile checks that the description of lexical blocks emitted
   215  // by the linker in debug_info, for each function in the main package,
   216  // corresponds to what we expect it to be.
   217  func TestScopeRanges(t *testing.T) {
   218  	testenv.MustHaveGoBuild(t)
   219  	t.Parallel()
   220  
   221  	if !platform.ExecutableHasDWARF(runtime.GOOS, runtime.GOARCH) {
   222  		t.Skipf("skipping on %s/%s: no DWARF symbol table in executables", runtime.GOOS, runtime.GOARCH)
   223  	}
   224  
   225  	src, f := gobuild(t, t.TempDir(), false, testfile)
   226  	defer f.Close()
   227  
   228  	// the compiler uses forward slashes for paths even on windows
   229  	src = strings.Replace(src, "\\", "/", -1)
   230  
   231  	pcln, err := f.PCLineTable()
   232  	if err != nil {
   233  		t.Fatal(err)
   234  	}
   235  	dwarfData, err := f.DWARF()
   236  	if err != nil {
   237  		t.Fatal(err)
   238  	}
   239  	dwarfReader := dwarfData.Reader()
   240  
   241  	lines := make(map[line][]*lexblock)
   242  
   243  	for {
   244  		entry, err := dwarfReader.Next()
   245  		if err != nil {
   246  			t.Fatal(err)
   247  		}
   248  		if entry == nil {
   249  			break
   250  		}
   251  
   252  		if entry.Tag != dwarf.TagSubprogram {
   253  			continue
   254  		}
   255  
   256  		name, ok := entry.Val(dwarf.AttrName).(string)
   257  		if !ok || !strings.HasPrefix(name, "main.Test") {
   258  			continue
   259  		}
   260  
   261  		var scope lexblock
   262  		ctxt := scopexplainContext{
   263  			dwarfData:   dwarfData,
   264  			dwarfReader: dwarfReader,
   265  			scopegen:    1,
   266  		}
   267  
   268  		readScope(&ctxt, &scope, entry)
   269  
   270  		scope.markLines(pcln, lines)
   271  	}
   272  
   273  	anyerror := false
   274  	for i := range testfile {
   275  		tgt := testfile[i].scopes
   276  		out := lines[line{src, i + 1}]
   277  
   278  		if detailOutput {
   279  			t.Logf("%s // %v", testfile[i].line, out)
   280  		}
   281  
   282  		scopesok := checkScopes(tgt, out)
   283  		if !scopesok {
   284  			t.Logf("mismatch at line %d %q: expected: %v got: %v\n", i, testfile[i].line, tgt, scopesToString(out))
   285  		}
   286  
   287  		varsok := true
   288  		if testfile[i].vars != nil {
   289  			if len(out) > 0 {
   290  				varsok = checkVars(testfile[i].vars, out[len(out)-1].vars)
   291  				if !varsok {
   292  					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)
   293  				}
   294  				for j := range testfile[i].decl {
   295  					if line := declLineForVar(out[len(out)-1].vars, testfile[i].decl[j]); line != i+1 {
   296  						t.Errorf("wrong declaration line for variable %s, expected %d got: %d", testfile[i].decl[j], i+1, line)
   297  					}
   298  				}
   299  
   300  				for j := range testfile[i].declBefore {
   301  					if line := declLineForVar(out[len(out)-1].vars, testfile[i].declBefore[j]); line > i+1 {
   302  						t.Errorf("wrong declaration line for variable %s, expected %d (or less) got: %d", testfile[i].declBefore[j], i+1, line)
   303  					}
   304  				}
   305  			}
   306  		}
   307  
   308  		anyerror = anyerror || !scopesok || !varsok
   309  	}
   310  
   311  	if anyerror {
   312  		t.Fatalf("mismatched output")
   313  	}
   314  }
   315  
   316  func scopesToString(v []*lexblock) string {
   317  	r := make([]string, len(v))
   318  	for i, s := range v {
   319  		r[i] = strconv.Itoa(s.id)
   320  	}
   321  	return "[ " + strings.Join(r, ", ") + " ]"
   322  }
   323  
   324  func checkScopes(tgt []int, out []*lexblock) bool {
   325  	if len(out) > 0 {
   326  		// omit scope 0
   327  		out = out[1:]
   328  	}
   329  	if len(tgt) != len(out) {
   330  		return false
   331  	}
   332  	for i := range tgt {
   333  		if tgt[i] != out[i].id {
   334  			return false
   335  		}
   336  	}
   337  	return true
   338  }
   339  
   340  func checkVars(tgt []string, out []variable) bool {
   341  	if len(tgt) != len(out) {
   342  		return false
   343  	}
   344  	for i := range tgt {
   345  		if tgt[i] != out[i].expr {
   346  			return false
   347  		}
   348  	}
   349  	return true
   350  }
   351  
   352  func declLineForVar(scope []variable, name string) int {
   353  	for i := range scope {
   354  		if scope[i].name() == name {
   355  			return scope[i].declLine
   356  		}
   357  	}
   358  	return -1
   359  }
   360  
   361  type lexblock struct {
   362  	id     int
   363  	ranges [][2]uint64
   364  	vars   []variable
   365  	scopes []lexblock
   366  }
   367  
   368  type variable struct {
   369  	expr     string
   370  	declLine int
   371  }
   372  
   373  func (v *variable) name() string {
   374  	return strings.Split(v.expr, " ")[1]
   375  }
   376  
   377  type line struct {
   378  	file   string
   379  	lineno int
   380  }
   381  
   382  type scopexplainContext struct {
   383  	dwarfData   *dwarf.Data
   384  	dwarfReader *dwarf.Reader
   385  	scopegen    int
   386  }
   387  
   388  // readScope reads the DW_TAG_lexical_block or the DW_TAG_subprogram in
   389  // entry and writes a description in scope.
   390  // Nested DW_TAG_lexical_block entries are read recursively.
   391  func readScope(ctxt *scopexplainContext, scope *lexblock, entry *dwarf.Entry) {
   392  	var err error
   393  	scope.ranges, err = ctxt.dwarfData.Ranges(entry)
   394  	if err != nil {
   395  		panic(err)
   396  	}
   397  	for {
   398  		e, err := ctxt.dwarfReader.Next()
   399  		if err != nil {
   400  			panic(err)
   401  		}
   402  		switch e.Tag {
   403  		case 0:
   404  			slices.SortFunc(scope.vars, func(a, b variable) int {
   405  				return cmp.Compare(a.expr, b.expr)
   406  			})
   407  			return
   408  		case dwarf.TagFormalParameter:
   409  			typ, err := ctxt.dwarfData.Type(e.Val(dwarf.AttrType).(dwarf.Offset))
   410  			if err != nil {
   411  				panic(err)
   412  			}
   413  			scope.vars = append(scope.vars, entryToVar(e, "arg", typ))
   414  		case dwarf.TagVariable:
   415  			typ, err := ctxt.dwarfData.Type(e.Val(dwarf.AttrType).(dwarf.Offset))
   416  			if err != nil {
   417  				panic(err)
   418  			}
   419  			scope.vars = append(scope.vars, entryToVar(e, "var", typ))
   420  		case dwarf.TagLexDwarfBlock:
   421  			scope.scopes = append(scope.scopes, lexblock{id: ctxt.scopegen})
   422  			ctxt.scopegen++
   423  			readScope(ctxt, &scope.scopes[len(scope.scopes)-1], e)
   424  		}
   425  	}
   426  }
   427  
   428  func entryToVar(e *dwarf.Entry, kind string, typ dwarf.Type) variable {
   429  	return variable{
   430  		fmt.Sprintf("%s %s %s", kind, e.Val(dwarf.AttrName).(string), typ.String()),
   431  		int(e.Val(dwarf.AttrDeclLine).(int64)),
   432  	}
   433  }
   434  
   435  // markLines marks all lines that belong to this scope with this scope
   436  // Recursively calls markLines for all children scopes.
   437  func (scope *lexblock) markLines(pcln objfile.Liner, lines map[line][]*lexblock) {
   438  	for _, r := range scope.ranges {
   439  		for pc := r[0]; pc < r[1]; pc++ {
   440  			file, lineno, _ := pcln.PCToLine(pc)
   441  			l := line{file, lineno}
   442  			if len(lines[l]) == 0 || lines[l][len(lines[l])-1] != scope {
   443  				lines[l] = append(lines[l], scope)
   444  			}
   445  		}
   446  	}
   447  
   448  	for i := range scope.scopes {
   449  		scope.scopes[i].markLines(pcln, lines)
   450  	}
   451  }
   452  
   453  func gobuild(t *testing.T, dir string, optimized bool, testfile []testline) (string, *objfile.File) {
   454  	src := filepath.Join(dir, "test.go")
   455  	dst := filepath.Join(dir, "out.o")
   456  
   457  	f, err := os.Create(src)
   458  	if err != nil {
   459  		t.Fatal(err)
   460  	}
   461  	for i := range testfile {
   462  		f.Write([]byte(testfile[i].line))
   463  		f.Write([]byte{'\n'})
   464  	}
   465  	f.Close()
   466  
   467  	args := []string{"build"}
   468  	if !optimized {
   469  		args = append(args, "-gcflags=-N -l")
   470  	}
   471  	args = append(args, "-o", dst, src)
   472  
   473  	cmd := testenv.Command(t, testenv.GoToolPath(t), args...)
   474  	if b, err := cmd.CombinedOutput(); err != nil {
   475  		t.Logf("build: %s\n", string(b))
   476  		t.Fatal(err)
   477  	}
   478  
   479  	pkg, err := objfile.Open(dst)
   480  	if err != nil {
   481  		t.Fatal(err)
   482  	}
   483  	return src, pkg
   484  }
   485  
   486  // TestEmptyDwarfRanges tests that no list entry in debug_ranges has start == end.
   487  // See issue #23928.
   488  func TestEmptyDwarfRanges(t *testing.T) {
   489  	testenv.MustHaveGoRun(t)
   490  	t.Parallel()
   491  
   492  	if !platform.ExecutableHasDWARF(runtime.GOOS, runtime.GOARCH) {
   493  		t.Skipf("skipping on %s/%s: no DWARF symbol table in executables", runtime.GOOS, runtime.GOARCH)
   494  	}
   495  
   496  	_, f := gobuild(t, t.TempDir(), true, []testline{{line: "package main"}, {line: "func main(){ println(\"hello\") }"}})
   497  	defer f.Close()
   498  
   499  	dwarfData, err := f.DWARF()
   500  	if err != nil {
   501  		t.Fatal(err)
   502  	}
   503  	dwarfReader := dwarfData.Reader()
   504  
   505  	for {
   506  		entry, err := dwarfReader.Next()
   507  		if err != nil {
   508  			t.Fatal(err)
   509  		}
   510  		if entry == nil {
   511  			break
   512  		}
   513  
   514  		ranges, err := dwarfData.Ranges(entry)
   515  		if err != nil {
   516  			t.Fatal(err)
   517  		}
   518  		if ranges == nil {
   519  			continue
   520  		}
   521  
   522  		for _, rng := range ranges {
   523  			if rng[0] == rng[1] {
   524  				t.Errorf("range entry with start == end: %v", rng)
   525  			}
   526  		}
   527  	}
   528  }
   529  

View as plain text