summaryrefslogtreecommitdiffstats
path: root/src/debug/gosym/pclntab_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'src/debug/gosym/pclntab_test.go')
-rw-r--r--src/debug/gosym/pclntab_test.go407
1 files changed, 407 insertions, 0 deletions
diff --git a/src/debug/gosym/pclntab_test.go b/src/debug/gosym/pclntab_test.go
new file mode 100644
index 0000000..e380bb5
--- /dev/null
+++ b/src/debug/gosym/pclntab_test.go
@@ -0,0 +1,407 @@
+// Copyright 2009 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 gosym
+
+import (
+ "bytes"
+ "compress/gzip"
+ "debug/elf"
+ "internal/testenv"
+ "io"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "strings"
+ "testing"
+)
+
+var (
+ pclineTempDir string
+ pclinetestBinary string
+)
+
+func dotest(t *testing.T) {
+ testenv.MustHaveGoBuild(t)
+ // For now, only works on amd64 platforms.
+ if runtime.GOARCH != "amd64" {
+ t.Skipf("skipping on non-AMD64 system %s", runtime.GOARCH)
+ }
+ // This test builds a Linux/AMD64 binary. Skipping in short mode if cross compiling.
+ if runtime.GOOS != "linux" && testing.Short() {
+ t.Skipf("skipping in short mode on non-Linux system %s", runtime.GOARCH)
+ }
+ var err error
+ pclineTempDir, err = os.MkdirTemp("", "pclinetest")
+ if err != nil {
+ t.Fatal(err)
+ }
+ pclinetestBinary = filepath.Join(pclineTempDir, "pclinetest")
+ cmd := exec.Command(testenv.GoToolPath(t), "build", "-o", pclinetestBinary)
+ cmd.Dir = "testdata"
+ cmd.Env = append(os.Environ(), "GOOS=linux")
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+ if err := cmd.Run(); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func endtest() {
+ if pclineTempDir != "" {
+ os.RemoveAll(pclineTempDir)
+ pclineTempDir = ""
+ pclinetestBinary = ""
+ }
+}
+
+// skipIfNotELF skips the test if we are not running on an ELF system.
+// These tests open and examine the test binary, and use elf.Open to do so.
+func skipIfNotELF(t *testing.T) {
+ switch runtime.GOOS {
+ case "dragonfly", "freebsd", "linux", "netbsd", "openbsd", "solaris", "illumos":
+ // OK.
+ default:
+ t.Skipf("skipping on non-ELF system %s", runtime.GOOS)
+ }
+}
+
+func getTable(t *testing.T) *Table {
+ f, tab := crack(os.Args[0], t)
+ f.Close()
+ return tab
+}
+
+func crack(file string, t *testing.T) (*elf.File, *Table) {
+ // Open self
+ f, err := elf.Open(file)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return parse(file, f, t)
+}
+
+func parse(file string, f *elf.File, t *testing.T) (*elf.File, *Table) {
+ s := f.Section(".gosymtab")
+ if s == nil {
+ t.Skip("no .gosymtab section")
+ }
+ symdat, err := s.Data()
+ if err != nil {
+ f.Close()
+ t.Fatalf("reading %s gosymtab: %v", file, err)
+ }
+ pclndat, err := f.Section(".gopclntab").Data()
+ if err != nil {
+ f.Close()
+ t.Fatalf("reading %s gopclntab: %v", file, err)
+ }
+
+ pcln := NewLineTable(pclndat, f.Section(".text").Addr)
+ tab, err := NewTable(symdat, pcln)
+ if err != nil {
+ f.Close()
+ t.Fatalf("parsing %s gosymtab: %v", file, err)
+ }
+
+ return f, tab
+}
+
+func TestLineFromAline(t *testing.T) {
+ skipIfNotELF(t)
+
+ tab := getTable(t)
+ if tab.go12line != nil {
+ // aline's don't exist in the Go 1.2 table.
+ t.Skip("not relevant to Go 1.2 symbol table")
+ }
+
+ // Find the sym package
+ pkg := tab.LookupFunc("debug/gosym.TestLineFromAline").Obj
+ if pkg == nil {
+ t.Fatalf("nil pkg")
+ }
+
+ // Walk every absolute line and ensure that we hit every
+ // source line monotonically
+ lastline := make(map[string]int)
+ final := -1
+ for i := 0; i < 10000; i++ {
+ path, line := pkg.lineFromAline(i)
+ // Check for end of object
+ if path == "" {
+ if final == -1 {
+ final = i - 1
+ }
+ continue
+ } else if final != -1 {
+ t.Fatalf("reached end of package at absolute line %d, but absolute line %d mapped to %s:%d", final, i, path, line)
+ }
+ // It's okay to see files multiple times (e.g., sys.a)
+ if line == 1 {
+ lastline[path] = 1
+ continue
+ }
+ // Check that the is the next line in path
+ ll, ok := lastline[path]
+ if !ok {
+ t.Errorf("file %s starts on line %d", path, line)
+ } else if line != ll+1 {
+ t.Fatalf("expected next line of file %s to be %d, got %d", path, ll+1, line)
+ }
+ lastline[path] = line
+ }
+ if final == -1 {
+ t.Errorf("never reached end of object")
+ }
+}
+
+func TestLineAline(t *testing.T) {
+ skipIfNotELF(t)
+
+ tab := getTable(t)
+ if tab.go12line != nil {
+ // aline's don't exist in the Go 1.2 table.
+ t.Skip("not relevant to Go 1.2 symbol table")
+ }
+
+ for _, o := range tab.Files {
+ // A source file can appear multiple times in a
+ // object. alineFromLine will always return alines in
+ // the first file, so track which lines we've seen.
+ found := make(map[string]int)
+ for i := 0; i < 1000; i++ {
+ path, line := o.lineFromAline(i)
+ if path == "" {
+ break
+ }
+
+ // cgo files are full of 'Z' symbols, which we don't handle
+ if len(path) > 4 && path[len(path)-4:] == ".cgo" {
+ continue
+ }
+
+ if minline, ok := found[path]; path != "" && ok {
+ if minline >= line {
+ // We've already covered this file
+ continue
+ }
+ }
+ found[path] = line
+
+ a, err := o.alineFromLine(path, line)
+ if err != nil {
+ 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)
+ } else if a != i {
+ 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)
+ }
+ }
+ }
+}
+
+func TestPCLine(t *testing.T) {
+ dotest(t)
+ defer endtest()
+
+ f, tab := crack(pclinetestBinary, t)
+ defer f.Close()
+ text := f.Section(".text")
+ textdat, err := text.Data()
+ if err != nil {
+ t.Fatalf("reading .text: %v", err)
+ }
+
+ // Test PCToLine
+ sym := tab.LookupFunc("main.linefrompc")
+ wantLine := 0
+ for pc := sym.Entry; pc < sym.End; pc++ {
+ off := pc - text.Addr // TODO(rsc): should not need off; bug in 8g
+ if textdat[off] == 255 {
+ break
+ }
+ wantLine += int(textdat[off])
+ t.Logf("off is %d %#x (max %d)", off, textdat[off], sym.End-pc)
+ file, line, fn := tab.PCToLine(pc)
+ if fn == nil {
+ t.Errorf("failed to get line of PC %#x", pc)
+ } else if !strings.HasSuffix(file, "pclinetest.s") || line != wantLine || fn != sym {
+ t.Errorf("PCToLine(%#x) = %s:%d (%s), want %s:%d (%s)", pc, file, line, fn.Name, "pclinetest.s", wantLine, sym.Name)
+ }
+ }
+
+ // Test LineToPC
+ sym = tab.LookupFunc("main.pcfromline")
+ lookupline := -1
+ wantLine = 0
+ off := uint64(0) // TODO(rsc): should not need off; bug in 8g
+ for pc := sym.Value; pc < sym.End; pc += 2 + uint64(textdat[off]) {
+ file, line, fn := tab.PCToLine(pc)
+ off = pc - text.Addr
+ if textdat[off] == 255 {
+ break
+ }
+ wantLine += int(textdat[off])
+ if line != wantLine {
+ t.Errorf("expected line %d at PC %#x in pcfromline, got %d", wantLine, pc, line)
+ off = pc + 1 - text.Addr
+ continue
+ }
+ if lookupline == -1 {
+ lookupline = line
+ }
+ for ; lookupline <= line; lookupline++ {
+ pc2, fn2, err := tab.LineToPC(file, lookupline)
+ if lookupline != line {
+ // Should be nothing on this line
+ if err == nil {
+ t.Errorf("expected no PC at line %d, got %#x (%s)", lookupline, pc2, fn2.Name)
+ }
+ } else if err != nil {
+ t.Errorf("failed to get PC of line %d: %s", lookupline, err)
+ } else if pc != pc2 {
+ t.Errorf("expected PC %#x (%s) at line %d, got PC %#x (%s)", pc, fn.Name, line, pc2, fn2.Name)
+ }
+ }
+ off = pc + 1 - text.Addr
+ }
+}
+
+func TestSymVersion(t *testing.T) {
+ skipIfNotELF(t)
+
+ table := getTable(t)
+ if table.go12line == nil {
+ t.Skip("not relevant to Go 1.2+ symbol table")
+ }
+ for _, fn := range table.Funcs {
+ if fn.goVersion == verUnknown {
+ t.Fatalf("unexpected symbol version: %v", fn)
+ }
+ }
+}
+
+// read115Executable returns a hello world executable compiled by Go 1.15.
+//
+// The file was compiled in /tmp/hello.go:
+//
+// package main
+//
+// func main() {
+// println("hello")
+// }
+func read115Executable(tb testing.TB) []byte {
+ zippedDat, err := os.ReadFile("testdata/pcln115.gz")
+ if err != nil {
+ tb.Fatal(err)
+ }
+ var gzReader *gzip.Reader
+ gzReader, err = gzip.NewReader(bytes.NewBuffer(zippedDat))
+ if err != nil {
+ tb.Fatal(err)
+ }
+ var dat []byte
+ dat, err = io.ReadAll(gzReader)
+ if err != nil {
+ tb.Fatal(err)
+ }
+ return dat
+}
+
+// Test that we can parse a pclntab from 1.15.
+func Test115PclnParsing(t *testing.T) {
+ dat := read115Executable(t)
+ const textStart = 0x1001000
+ pcln := NewLineTable(dat, textStart)
+ tab, err := NewTable(nil, pcln)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var f *Func
+ var pc uint64
+ pc, f, err = tab.LineToPC("/tmp/hello.go", 3)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if pcln.version != ver12 {
+ t.Fatal("Expected pcln to parse as an older version")
+ }
+ if pc != 0x105c280 {
+ t.Fatalf("expect pc = 0x105c280, got 0x%x", pc)
+ }
+ if f.Name != "main.main" {
+ t.Fatalf("expected to parse name as main.main, got %v", f.Name)
+ }
+}
+
+var (
+ sinkLineTable *LineTable
+ sinkTable *Table
+)
+
+func Benchmark115(b *testing.B) {
+ dat := read115Executable(b)
+ const textStart = 0x1001000
+
+ b.Run("NewLineTable", func(b *testing.B) {
+ b.ReportAllocs()
+ for i := 0; i < b.N; i++ {
+ sinkLineTable = NewLineTable(dat, textStart)
+ }
+ })
+
+ pcln := NewLineTable(dat, textStart)
+ b.Run("NewTable", func(b *testing.B) {
+ b.ReportAllocs()
+ for i := 0; i < b.N; i++ {
+ var err error
+ sinkTable, err = NewTable(nil, pcln)
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+
+ tab, err := NewTable(nil, pcln)
+ if err != nil {
+ b.Fatal(err)
+ }
+
+ b.Run("LineToPC", func(b *testing.B) {
+ b.ReportAllocs()
+ for i := 0; i < b.N; i++ {
+ var f *Func
+ var pc uint64
+ pc, f, err = tab.LineToPC("/tmp/hello.go", 3)
+ if err != nil {
+ b.Fatal(err)
+ }
+ if pcln.version != ver12 {
+ b.Fatalf("want version=%d, got %d", ver12, pcln.version)
+ }
+ if pc != 0x105c280 {
+ b.Fatalf("want pc=0x105c280, got 0x%x", pc)
+ }
+ if f.Name != "main.main" {
+ b.Fatalf("want name=main.main, got %q", f.Name)
+ }
+ }
+ })
+
+ b.Run("PCToLine", func(b *testing.B) {
+ b.ReportAllocs()
+ for i := 0; i < b.N; i++ {
+ file, line, fn := tab.PCToLine(0x105c280)
+ if file != "/tmp/hello.go" {
+ b.Fatalf("want name=/tmp/hello.go, got %q", file)
+ }
+ if line != 3 {
+ b.Fatalf("want line=3, got %d", line)
+ }
+ if fn.Name != "main.main" {
+ b.Fatalf("want name=main.main, got %q", fn.Name)
+ }
+ }
+ })
+}