| // 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) | |
| } | |
| } | |
| }) | |
| } |