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