1// Copyright 2009 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
5package gosym
6
7import (
8	"bytes"
9	"compress/gzip"
10	"debug/elf"
11	"internal/testenv"
12	"io"
13	"os"
14	"os/exec"
15	"path/filepath"
16	"runtime"
17	"strings"
18	"testing"
19)
20
21var (
22	pclineTempDir    string
23	pclinetestBinary string
24)
25
26func dotest(t *testing.T) {
27	testenv.MustHaveGoBuild(t)
28	// For now, only works on amd64 platforms.
29	if runtime.GOARCH != "amd64" {
30		t.Skipf("skipping on non-AMD64 system %s", runtime.GOARCH)
31	}
32	// This test builds a Linux/AMD64 binary. Skipping in short mode if cross compiling.
33	if runtime.GOOS != "linux" && testing.Short() {
34		t.Skipf("skipping in short mode on non-Linux system %s", runtime.GOARCH)
35	}
36	var err error
37	pclineTempDir, err = os.MkdirTemp("", "pclinetest")
38	if err != nil {
39		t.Fatal(err)
40	}
41	pclinetestBinary = filepath.Join(pclineTempDir, "pclinetest")
42	cmd := exec.Command(testenv.GoToolPath(t), "build", "-o", pclinetestBinary)
43	cmd.Dir = "testdata"
44	cmd.Env = append(os.Environ(), "GOOS=linux")
45	cmd.Stdout = os.Stdout
46	cmd.Stderr = os.Stderr
47	if err := cmd.Run(); err != nil {
48		t.Fatal(err)
49	}
50}
51
52func endtest() {
53	if pclineTempDir != "" {
54		os.RemoveAll(pclineTempDir)
55		pclineTempDir = ""
56		pclinetestBinary = ""
57	}
58}
59
60// skipIfNotELF skips the test if we are not running on an ELF system.
61// These tests open and examine the test binary, and use elf.Open to do so.
62func skipIfNotELF(t *testing.T) {
63	switch runtime.GOOS {
64	case "dragonfly", "freebsd", "linux", "netbsd", "openbsd", "solaris", "illumos":
65		// OK.
66	default:
67		t.Skipf("skipping on non-ELF system %s", runtime.GOOS)
68	}
69}
70
71func getTable(t *testing.T) *Table {
72	f, tab := crack(os.Args[0], t)
73	f.Close()
74	return tab
75}
76
77func crack(file string, t *testing.T) (*elf.File, *Table) {
78	// Open self
79	f, err := elf.Open(file)
80	if err != nil {
81		t.Fatal(err)
82	}
83	return parse(file, f, t)
84}
85
86func parse(file string, f *elf.File, t *testing.T) (*elf.File, *Table) {
87	s := f.Section(".gosymtab")
88	if s == nil {
89		t.Skip("no .gosymtab section")
90	}
91	symdat, err := s.Data()
92	if err != nil {
93		f.Close()
94		t.Fatalf("reading %s gosymtab: %v", file, err)
95	}
96	pclndat, err := f.Section(".gopclntab").Data()
97	if err != nil {
98		f.Close()
99		t.Fatalf("reading %s gopclntab: %v", file, err)
100	}
101
102	pcln := NewLineTable(pclndat, f.Section(".text").Addr)
103	tab, err := NewTable(symdat, pcln)
104	if err != nil {
105		f.Close()
106		t.Fatalf("parsing %s gosymtab: %v", file, err)
107	}
108
109	return f, tab
110}
111
112func TestLineFromAline(t *testing.T) {
113	skipIfNotELF(t)
114
115	tab := getTable(t)
116	if tab.go12line != nil {
117		// aline's don't exist in the Go 1.2 table.
118		t.Skip("not relevant to Go 1.2 symbol table")
119	}
120
121	// Find the sym package
122	pkg := tab.LookupFunc("debug/gosym.TestLineFromAline").Obj
123	if pkg == nil {
124		t.Fatalf("nil pkg")
125	}
126
127	// Walk every absolute line and ensure that we hit every
128	// source line monotonically
129	lastline := make(map[string]int)
130	final := -1
131	for i := 0; i < 10000; i++ {
132		path, line := pkg.lineFromAline(i)
133		// Check for end of object
134		if path == "" {
135			if final == -1 {
136				final = i - 1
137			}
138			continue
139		} else if final != -1 {
140			t.Fatalf("reached end of package at absolute line %d, but absolute line %d mapped to %s:%d", final, i, path, line)
141		}
142		// It's okay to see files multiple times (e.g., sys.a)
143		if line == 1 {
144			lastline[path] = 1
145			continue
146		}
147		// Check that the is the next line in path
148		ll, ok := lastline[path]
149		if !ok {
150			t.Errorf("file %s starts on line %d", path, line)
151		} else if line != ll+1 {
152			t.Fatalf("expected next line of file %s to be %d, got %d", path, ll+1, line)
153		}
154		lastline[path] = line
155	}
156	if final == -1 {
157		t.Errorf("never reached end of object")
158	}
159}
160
161func TestLineAline(t *testing.T) {
162	skipIfNotELF(t)
163
164	tab := getTable(t)
165	if tab.go12line != nil {
166		// aline's don't exist in the Go 1.2 table.
167		t.Skip("not relevant to Go 1.2 symbol table")
168	}
169
170	for _, o := range tab.Files {
171		// A source file can appear multiple times in a
172		// object.  alineFromLine will always return alines in
173		// the first file, so track which lines we've seen.
174		found := make(map[string]int)
175		for i := 0; i < 1000; i++ {
176			path, line := o.lineFromAline(i)
177			if path == "" {
178				break
179			}
180
181			// cgo files are full of 'Z' symbols, which we don't handle
182			if len(path) > 4 && path[len(path)-4:] == ".cgo" {
183				continue
184			}
185
186			if minline, ok := found[path]; path != "" && ok {
187				if minline >= line {
188					// We've already covered this file
189					continue
190				}
191			}
192			found[path] = line
193
194			a, err := o.alineFromLine(path, line)
195			if err != nil {
196				t.Errorf("absolute line %d in object %s maps to %s:%d, but mapping that back gives error %s", i, o.Paths[0].Name, path, line, err)
197			} else if a != i {
198				t.Errorf("absolute line %d in object %s maps to %s:%d, which maps back to absolute line %d\n", i, o.Paths[0].Name, path, line, a)
199			}
200		}
201	}
202}
203
204func TestPCLine(t *testing.T) {
205	dotest(t)
206	defer endtest()
207
208	f, tab := crack(pclinetestBinary, t)
209	defer f.Close()
210	text := f.Section(".text")
211	textdat, err := text.Data()
212	if err != nil {
213		t.Fatalf("reading .text: %v", err)
214	}
215
216	// Test PCToLine
217	sym := tab.LookupFunc("main.linefrompc")
218	wantLine := 0
219	for pc := sym.Entry; pc < sym.End; pc++ {
220		off := pc - text.Addr // TODO(rsc): should not need off; bug in 8g
221		if textdat[off] == 255 {
222			break
223		}
224		wantLine += int(textdat[off])
225		t.Logf("off is %d %#x (max %d)", off, textdat[off], sym.End-pc)
226		file, line, fn := tab.PCToLine(pc)
227		if fn == nil {
228			t.Errorf("failed to get line of PC %#x", pc)
229		} else if !strings.HasSuffix(file, "pclinetest.s") || line != wantLine || fn != sym {
230			t.Errorf("PCToLine(%#x) = %s:%d (%s), want %s:%d (%s)", pc, file, line, fn.Name, "pclinetest.s", wantLine, sym.Name)
231		}
232	}
233
234	// Test LineToPC
235	sym = tab.LookupFunc("main.pcfromline")
236	lookupline := -1
237	wantLine = 0
238	off := uint64(0) // TODO(rsc): should not need off; bug in 8g
239	for pc := sym.Value; pc < sym.End; pc += 2 + uint64(textdat[off]) {
240		file, line, fn := tab.PCToLine(pc)
241		off = pc - text.Addr
242		if textdat[off] == 255 {
243			break
244		}
245		wantLine += int(textdat[off])
246		if line != wantLine {
247			t.Errorf("expected line %d at PC %#x in pcfromline, got %d", wantLine, pc, line)
248			off = pc + 1 - text.Addr
249			continue
250		}
251		if lookupline == -1 {
252			lookupline = line
253		}
254		for ; lookupline <= line; lookupline++ {
255			pc2, fn2, err := tab.LineToPC(file, lookupline)
256			if lookupline != line {
257				// Should be nothing on this line
258				if err == nil {
259					t.Errorf("expected no PC at line %d, got %#x (%s)", lookupline, pc2, fn2.Name)
260				}
261			} else if err != nil {
262				t.Errorf("failed to get PC of line %d: %s", lookupline, err)
263			} else if pc != pc2 {
264				t.Errorf("expected PC %#x (%s) at line %d, got PC %#x (%s)", pc, fn.Name, line, pc2, fn2.Name)
265			}
266		}
267		off = pc + 1 - text.Addr
268	}
269}
270
271func TestSymVersion(t *testing.T) {
272	skipIfNotELF(t)
273
274	table := getTable(t)
275	if table.go12line == nil {
276		t.Skip("not relevant to Go 1.2+ symbol table")
277	}
278	for _, fn := range table.Funcs {
279		if fn.goVersion == verUnknown {
280			t.Fatalf("unexpected symbol version: %v", fn)
281		}
282	}
283}
284
285// read115Executable returns a hello world executable compiled by Go 1.15.
286//
287// The file was compiled in /tmp/hello.go:
288//
289//	package main
290//
291//	func main() {
292//		println("hello")
293//	}
294func read115Executable(tb testing.TB) []byte {
295	zippedDat, err := os.ReadFile("testdata/pcln115.gz")
296	if err != nil {
297		tb.Fatal(err)
298	}
299	var gzReader *gzip.Reader
300	gzReader, err = gzip.NewReader(bytes.NewBuffer(zippedDat))
301	if err != nil {
302		tb.Fatal(err)
303	}
304	var dat []byte
305	dat, err = io.ReadAll(gzReader)
306	if err != nil {
307		tb.Fatal(err)
308	}
309	return dat
310}
311
312// Test that we can parse a pclntab from 1.15.
313func Test115PclnParsing(t *testing.T) {
314	dat := read115Executable(t)
315	const textStart = 0x1001000
316	pcln := NewLineTable(dat, textStart)
317	tab, err := NewTable(nil, pcln)
318	if err != nil {
319		t.Fatal(err)
320	}
321	var f *Func
322	var pc uint64
323	pc, f, err = tab.LineToPC("/tmp/hello.go", 3)
324	if err != nil {
325		t.Fatal(err)
326	}
327	if pcln.version != ver12 {
328		t.Fatal("Expected pcln to parse as an older version")
329	}
330	if pc != 0x105c280 {
331		t.Fatalf("expect pc = 0x105c280, got 0x%x", pc)
332	}
333	if f.Name != "main.main" {
334		t.Fatalf("expected to parse name as main.main, got %v", f.Name)
335	}
336}
337
338var (
339	sinkLineTable *LineTable
340	sinkTable     *Table
341)
342
343func Benchmark115(b *testing.B) {
344	dat := read115Executable(b)
345	const textStart = 0x1001000
346
347	b.Run("NewLineTable", func(b *testing.B) {
348		b.ReportAllocs()
349		for i := 0; i < b.N; i++ {
350			sinkLineTable = NewLineTable(dat, textStart)
351		}
352	})
353
354	pcln := NewLineTable(dat, textStart)
355	b.Run("NewTable", func(b *testing.B) {
356		b.ReportAllocs()
357		for i := 0; i < b.N; i++ {
358			var err error
359			sinkTable, err = NewTable(nil, pcln)
360			if err != nil {
361				b.Fatal(err)
362			}
363		}
364	})
365
366	tab, err := NewTable(nil, pcln)
367	if err != nil {
368		b.Fatal(err)
369	}
370
371	b.Run("LineToPC", func(b *testing.B) {
372		b.ReportAllocs()
373		for i := 0; i < b.N; i++ {
374			var f *Func
375			var pc uint64
376			pc, f, err = tab.LineToPC("/tmp/hello.go", 3)
377			if err != nil {
378				b.Fatal(err)
379			}
380			if pcln.version != ver12 {
381				b.Fatalf("want version=%d, got %d", ver12, pcln.version)
382			}
383			if pc != 0x105c280 {
384				b.Fatalf("want pc=0x105c280, got 0x%x", pc)
385			}
386			if f.Name != "main.main" {
387				b.Fatalf("want name=main.main, got %q", f.Name)
388			}
389		}
390	})
391
392	b.Run("PCToLine", func(b *testing.B) {
393		b.ReportAllocs()
394		for i := 0; i < b.N; i++ {
395			file, line, fn := tab.PCToLine(0x105c280)
396			if file != "/tmp/hello.go" {
397				b.Fatalf("want name=/tmp/hello.go, got %q", file)
398			}
399			if line != 3 {
400				b.Fatalf("want line=3, got %d", line)
401			}
402			if fn.Name != "main.main" {
403				b.Fatalf("want name=main.main, got %q", fn.Name)
404			}
405		}
406	})
407}
408