Home | History | Annotate | Download | only in gosym
      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 
      5 package gosym
      6 
      7 import (
      8 	"bytes"
      9 	"debug/elf"
     10 	"internal/testenv"
     11 	"io/ioutil"
     12 	"os"
     13 	"os/exec"
     14 	"path/filepath"
     15 	"runtime"
     16 	"strings"
     17 	"testing"
     18 )
     19 
     20 var (
     21 	pclineTempDir    string
     22 	pclinetestBinary string
     23 )
     24 
     25 func dotest(t *testing.T) {
     26 	testenv.MustHaveGoBuild(t)
     27 	// For now, only works on amd64 platforms.
     28 	if runtime.GOARCH != "amd64" {
     29 		t.Skipf("skipping on non-AMD64 system %s", runtime.GOARCH)
     30 	}
     31 	var err error
     32 	pclineTempDir, err = ioutil.TempDir("", "pclinetest")
     33 	if err != nil {
     34 		t.Fatal(err)
     35 	}
     36 	// This command builds pclinetest from pclinetest.asm;
     37 	// the resulting binary looks like it was built from pclinetest.s,
     38 	// but we have renamed it to keep it away from the go tool.
     39 	pclinetestBinary = filepath.Join(pclineTempDir, "pclinetest")
     40 	cmd := exec.Command(testenv.GoToolPath(t), "tool", "asm", "-o", pclinetestBinary+".o", "pclinetest.asm")
     41 	cmd.Stdout = os.Stdout
     42 	cmd.Stderr = os.Stderr
     43 	if err := cmd.Run(); err != nil {
     44 		t.Fatal(err)
     45 	}
     46 
     47 	// stamp .o file as being 'package main' so that go tool link will accept it
     48 	data, err := ioutil.ReadFile(pclinetestBinary + ".o")
     49 	if err != nil {
     50 		t.Fatal(err)
     51 	}
     52 	i := bytes.IndexByte(data, '\n')
     53 	if i < 0 {
     54 		t.Fatal("bad binary")
     55 	}
     56 	data = append(append(data[:i:i], "\nmain"...), data[i:]...)
     57 	if err := ioutil.WriteFile(pclinetestBinary+".o", data, 0666); err != nil {
     58 		t.Fatal(err)
     59 	}
     60 
     61 	cmd = exec.Command(testenv.GoToolPath(t), "tool", "link", "-H", "linux",
     62 		"-o", pclinetestBinary, pclinetestBinary+".o")
     63 	cmd.Stdout = os.Stdout
     64 	cmd.Stderr = os.Stderr
     65 	if err := cmd.Run(); err != nil {
     66 		t.Fatal(err)
     67 	}
     68 }
     69 
     70 func endtest() {
     71 	if pclineTempDir != "" {
     72 		os.RemoveAll(pclineTempDir)
     73 		pclineTempDir = ""
     74 		pclinetestBinary = ""
     75 	}
     76 }
     77 
     78 // skipIfNotELF skips the test if we are not running on an ELF system.
     79 // These tests open and examine the test binary, and use elf.Open to do so.
     80 func skipIfNotELF(t *testing.T) {
     81 	switch runtime.GOOS {
     82 	case "dragonfly", "freebsd", "linux", "netbsd", "openbsd", "solaris":
     83 		// OK.
     84 	default:
     85 		t.Skipf("skipping on non-ELF system %s", runtime.GOOS)
     86 	}
     87 }
     88 
     89 func getTable(t *testing.T) *Table {
     90 	f, tab := crack(os.Args[0], t)
     91 	f.Close()
     92 	return tab
     93 }
     94 
     95 func crack(file string, t *testing.T) (*elf.File, *Table) {
     96 	// Open self
     97 	f, err := elf.Open(file)
     98 	if err != nil {
     99 		t.Fatal(err)
    100 	}
    101 	return parse(file, f, t)
    102 }
    103 
    104 func parse(file string, f *elf.File, t *testing.T) (*elf.File, *Table) {
    105 	s := f.Section(".gosymtab")
    106 	if s == nil {
    107 		t.Skip("no .gosymtab section")
    108 	}
    109 	symdat, err := s.Data()
    110 	if err != nil {
    111 		f.Close()
    112 		t.Fatalf("reading %s gosymtab: %v", file, err)
    113 	}
    114 	pclndat, err := f.Section(".gopclntab").Data()
    115 	if err != nil {
    116 		f.Close()
    117 		t.Fatalf("reading %s gopclntab: %v", file, err)
    118 	}
    119 
    120 	pcln := NewLineTable(pclndat, f.Section(".text").Addr)
    121 	tab, err := NewTable(symdat, pcln)
    122 	if err != nil {
    123 		f.Close()
    124 		t.Fatalf("parsing %s gosymtab: %v", file, err)
    125 	}
    126 
    127 	return f, tab
    128 }
    129 
    130 func TestLineFromAline(t *testing.T) {
    131 	skipIfNotELF(t)
    132 
    133 	tab := getTable(t)
    134 	if tab.go12line != nil {
    135 		// aline's don't exist in the Go 1.2 table.
    136 		t.Skip("not relevant to Go 1.2 symbol table")
    137 	}
    138 
    139 	// Find the sym package
    140 	pkg := tab.LookupFunc("debug/gosym.TestLineFromAline").Obj
    141 	if pkg == nil {
    142 		t.Fatalf("nil pkg")
    143 	}
    144 
    145 	// Walk every absolute line and ensure that we hit every
    146 	// source line monotonically
    147 	lastline := make(map[string]int)
    148 	final := -1
    149 	for i := 0; i < 10000; i++ {
    150 		path, line := pkg.lineFromAline(i)
    151 		// Check for end of object
    152 		if path == "" {
    153 			if final == -1 {
    154 				final = i - 1
    155 			}
    156 			continue
    157 		} else if final != -1 {
    158 			t.Fatalf("reached end of package at absolute line %d, but absolute line %d mapped to %s:%d", final, i, path, line)
    159 		}
    160 		// It's okay to see files multiple times (e.g., sys.a)
    161 		if line == 1 {
    162 			lastline[path] = 1
    163 			continue
    164 		}
    165 		// Check that the is the next line in path
    166 		ll, ok := lastline[path]
    167 		if !ok {
    168 			t.Errorf("file %s starts on line %d", path, line)
    169 		} else if line != ll+1 {
    170 			t.Fatalf("expected next line of file %s to be %d, got %d", path, ll+1, line)
    171 		}
    172 		lastline[path] = line
    173 	}
    174 	if final == -1 {
    175 		t.Errorf("never reached end of object")
    176 	}
    177 }
    178 
    179 func TestLineAline(t *testing.T) {
    180 	skipIfNotELF(t)
    181 
    182 	tab := getTable(t)
    183 	if tab.go12line != nil {
    184 		// aline's don't exist in the Go 1.2 table.
    185 		t.Skip("not relevant to Go 1.2 symbol table")
    186 	}
    187 
    188 	for _, o := range tab.Files {
    189 		// A source file can appear multiple times in a
    190 		// object.  alineFromLine will always return alines in
    191 		// the first file, so track which lines we've seen.
    192 		found := make(map[string]int)
    193 		for i := 0; i < 1000; i++ {
    194 			path, line := o.lineFromAline(i)
    195 			if path == "" {
    196 				break
    197 			}
    198 
    199 			// cgo files are full of 'Z' symbols, which we don't handle
    200 			if len(path) > 4 && path[len(path)-4:] == ".cgo" {
    201 				continue
    202 			}
    203 
    204 			if minline, ok := found[path]; path != "" && ok {
    205 				if minline >= line {
    206 					// We've already covered this file
    207 					continue
    208 				}
    209 			}
    210 			found[path] = line
    211 
    212 			a, err := o.alineFromLine(path, line)
    213 			if err != nil {
    214 				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)
    215 			} else if a != i {
    216 				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)
    217 			}
    218 		}
    219 	}
    220 }
    221 
    222 func TestPCLine(t *testing.T) {
    223 	dotest(t)
    224 	defer endtest()
    225 
    226 	f, tab := crack(pclinetestBinary, t)
    227 	defer f.Close()
    228 	text := f.Section(".text")
    229 	textdat, err := text.Data()
    230 	if err != nil {
    231 		t.Fatalf("reading .text: %v", err)
    232 	}
    233 
    234 	// Test PCToLine
    235 	sym := tab.LookupFunc("linefrompc")
    236 	wantLine := 0
    237 	for pc := sym.Entry; pc < sym.End; pc++ {
    238 		off := pc - text.Addr // TODO(rsc): should not need off; bug in 8g
    239 		if textdat[off] == 255 {
    240 			break
    241 		}
    242 		wantLine += int(textdat[off])
    243 		t.Logf("off is %d %#x (max %d)", off, textdat[off], sym.End-pc)
    244 		file, line, fn := tab.PCToLine(pc)
    245 		if fn == nil {
    246 			t.Errorf("failed to get line of PC %#x", pc)
    247 		} else if !strings.HasSuffix(file, "pclinetest.asm") || line != wantLine || fn != sym {
    248 			t.Errorf("PCToLine(%#x) = %s:%d (%s), want %s:%d (%s)", pc, file, line, fn.Name, "pclinetest.asm", wantLine, sym.Name)
    249 		}
    250 	}
    251 
    252 	// Test LineToPC
    253 	sym = tab.LookupFunc("pcfromline")
    254 	lookupline := -1
    255 	wantLine = 0
    256 	off := uint64(0) // TODO(rsc): should not need off; bug in 8g
    257 	for pc := sym.Value; pc < sym.End; pc += 2 + uint64(textdat[off]) {
    258 		file, line, fn := tab.PCToLine(pc)
    259 		off = pc - text.Addr
    260 		if textdat[off] == 255 {
    261 			break
    262 		}
    263 		wantLine += int(textdat[off])
    264 		if line != wantLine {
    265 			t.Errorf("expected line %d at PC %#x in pcfromline, got %d", wantLine, pc, line)
    266 			off = pc + 1 - text.Addr
    267 			continue
    268 		}
    269 		if lookupline == -1 {
    270 			lookupline = line
    271 		}
    272 		for ; lookupline <= line; lookupline++ {
    273 			pc2, fn2, err := tab.LineToPC(file, lookupline)
    274 			if lookupline != line {
    275 				// Should be nothing on this line
    276 				if err == nil {
    277 					t.Errorf("expected no PC at line %d, got %#x (%s)", lookupline, pc2, fn2.Name)
    278 				}
    279 			} else if err != nil {
    280 				t.Errorf("failed to get PC of line %d: %s", lookupline, err)
    281 			} else if pc != pc2 {
    282 				t.Errorf("expected PC %#x (%s) at line %d, got PC %#x (%s)", pc, fn.Name, line, pc2, fn2.Name)
    283 			}
    284 		}
    285 		off = pc + 1 - text.Addr
    286 	}
    287 }
    288