pclntab_test.go 9.3 KB

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