1
2
3
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
26 line string
27
28
29
30
31
32
33
34
35
36
37 scopes []int
38
39
40
41
42
43 vars []string
44
45
46 decl []string
47
48
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"}},
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
215
216
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
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
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
389
390
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
436
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
487
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