gitlab.com/Raven-IO/raven-delve@v1.22.4/pkg/dwarf/line/line_parser_test.go (about)

     1  package line
     2  
     3  import (
     4  	"compress/zlib"
     5  	"debug/elf"
     6  	"debug/macho"
     7  	"debug/pe"
     8  	"flag"
     9  	"fmt"
    10  	"io"
    11  	"os"
    12  	"os/exec"
    13  	"path/filepath"
    14  	"runtime"
    15  	"strings"
    16  	"testing"
    17  	"time"
    18  	"unsafe"
    19  
    20  	"gitlab.com/Raven-IO/raven-delve/pkg/dwarf/godwarf"
    21  	"gitlab.com/Raven-IO/raven-delve/pkg/goversion"
    22  )
    23  
    24  var userTestFile string
    25  
    26  func TestMain(m *testing.M) {
    27  	flag.StringVar(&userTestFile, "user", "", "runs line parsing test on one extra file")
    28  	flag.Parse()
    29  	os.Exit(m.Run())
    30  }
    31  
    32  func grabDebugLineSection(p string, t *testing.T) []byte {
    33  	f, err := os.Open(p)
    34  	if err != nil {
    35  		t.Fatal(err)
    36  	}
    37  	defer f.Close()
    38  
    39  	ef, err := elf.NewFile(f)
    40  	if err == nil {
    41  		data, _ := godwarf.GetDebugSectionElf(ef, "line")
    42  		return data
    43  	}
    44  
    45  	pf, err := pe.NewFile(f)
    46  	if err == nil {
    47  		data, _ := godwarf.GetDebugSectionPE(pf, "line")
    48  		return data
    49  	}
    50  
    51  	mf, err := macho.NewFile(f)
    52  	if err == nil {
    53  		data, _ := godwarf.GetDebugSectionMacho(mf, "line")
    54  		return data
    55  	}
    56  
    57  	return nil
    58  }
    59  
    60  const (
    61  	lineBaseGo14    int8   = -1
    62  	lineBaseGo18    int8   = -4
    63  	lineRangeGo14   uint8  = 4
    64  	lineRangeGo18   uint8  = 10
    65  	versionGo14     uint16 = 2
    66  	versionGo111    uint16 = 3
    67  	opcodeBaseGo14  uint8  = 10
    68  	opcodeBaseGo111 uint8  = 11
    69  )
    70  
    71  func ptrSizeByRuntimeArch() int {
    72  	return int(unsafe.Sizeof(uintptr(0)))
    73  }
    74  
    75  func testDebugLinePrologueParser(p string, t *testing.T) {
    76  	data := grabDebugLineSection(p, t)
    77  	debugLines := ParseAll(data, nil, nil, 0, true, ptrSizeByRuntimeArch())
    78  	mainFileFound := false
    79  
    80  	for _, dbl := range debugLines {
    81  		prologue := dbl.Prologue
    82  
    83  		if prologue.Version != versionGo14 && prologue.Version != versionGo111 {
    84  			t.Fatal("Version not parsed correctly", prologue.Version)
    85  		}
    86  
    87  		if prologue.MinInstrLength != uint8(1) {
    88  			t.Fatal("Minimum Instruction Length not parsed correctly", prologue.MinInstrLength)
    89  		}
    90  
    91  		if prologue.InitialIsStmt != uint8(1) {
    92  			t.Fatal("Initial value of 'is_stmt' not parsed correctly", prologue.InitialIsStmt)
    93  		}
    94  
    95  		if prologue.LineBase != lineBaseGo14 && prologue.LineBase != lineBaseGo18 {
    96  			// go < 1.8 uses -1
    97  			// go >= 1.8 uses -4
    98  			t.Fatal("Line base not parsed correctly", prologue.LineBase)
    99  		}
   100  
   101  		if prologue.LineRange != lineRangeGo14 && prologue.LineRange != lineRangeGo18 {
   102  			// go < 1.8 uses 4
   103  			// go >= 1.8 uses 10
   104  			t.Fatal("Line Range not parsed correctly", prologue.LineRange)
   105  		}
   106  
   107  		if prologue.OpcodeBase != opcodeBaseGo14 && prologue.OpcodeBase != opcodeBaseGo111 {
   108  			t.Fatal("Opcode Base not parsed correctly", prologue.OpcodeBase)
   109  		}
   110  
   111  		lengths := []uint8{0, 1, 1, 1, 1, 0, 0, 0, 1, 0}
   112  		for i, l := range prologue.StdOpLengths {
   113  			if l != lengths[i] {
   114  				t.Fatal("Length not parsed correctly", l)
   115  			}
   116  		}
   117  
   118  		if !goversion.VersionAfterOrEqual(runtime.Version(), 1, 16) {
   119  			if len(dbl.IncludeDirs) != 1 {
   120  				t.Fatal("Include dirs not parsed correctly")
   121  			}
   122  		}
   123  
   124  		for _, ln := range dbl.Lookup {
   125  			if ln.Path == "<autogenerated>" || strings.HasPrefix(ln.Path, "<missing>_") || ln.Path == "_gomod_.go" {
   126  				continue
   127  			}
   128  			if _, err := os.Stat(ln.Path); err != nil {
   129  				t.Fatalf("Invalid input path %s: %s\n", ln.Path, err)
   130  			}
   131  		}
   132  
   133  		for _, n := range dbl.FileNames {
   134  			t.Logf("file %s\n", n.Path)
   135  			if strings.Contains(n.Path, "/_fixtures/testnextprog.go") {
   136  				mainFileFound = true
   137  				break
   138  			}
   139  		}
   140  	}
   141  	if !mainFileFound {
   142  		t.Fatal("File names table not parsed correctly")
   143  	}
   144  }
   145  
   146  func TestUserFile(t *testing.T) {
   147  	if userTestFile == "" {
   148  		return
   149  	}
   150  	t.Logf("testing %q", userTestFile)
   151  	testDebugLinePrologueParser(userTestFile, t)
   152  }
   153  
   154  func TestDebugLinePrologueParser(t *testing.T) {
   155  	// Test against known good values, from readelf --debug-dump=rawline _fixtures/testnextprog
   156  	p, err := filepath.Abs("../../../_fixtures/testnextprog")
   157  	if err != nil {
   158  		t.Fatal(err)
   159  	}
   160  
   161  	err = exec.Command("go", "build", "-gcflags=-N -l", "-o", p, p+".go").Run()
   162  	if err != nil {
   163  		t.Fatal("Could not compile test file", p, err)
   164  	}
   165  	defer os.Remove(p)
   166  	testDebugLinePrologueParser(p, t)
   167  }
   168  
   169  func BenchmarkLineParser(b *testing.B) {
   170  	p, err := filepath.Abs("../../../_fixtures/testnextprog")
   171  	if err != nil {
   172  		b.Fatal(err)
   173  	}
   174  	err = exec.Command("go", "build", "-gcflags=-N -l", "-o", p, p+".go").Run()
   175  	if err != nil {
   176  		b.Fatal("Could not compile test file", p, err)
   177  	}
   178  	defer os.Remove(p)
   179  
   180  	data := grabDebugLineSection(p, nil)
   181  
   182  	b.ResetTimer()
   183  	for i := 0; i < b.N; i++ {
   184  		_ = ParseAll(data, nil, nil, 0, true, ptrSizeByRuntimeArch())
   185  	}
   186  }
   187  
   188  func loadBenchmarkData(tb testing.TB) DebugLines {
   189  	p, err := filepath.Abs("../../../_fixtures/debug_line_benchmark_data")
   190  	if err != nil {
   191  		tb.Fatal("Could not find test data", p, err)
   192  	}
   193  
   194  	data, err := os.ReadFile(p)
   195  	if err != nil {
   196  		tb.Fatal("Could not read test data", err)
   197  	}
   198  
   199  	return ParseAll(data, nil, nil, 0, true, ptrSizeByRuntimeArch())
   200  }
   201  
   202  func BenchmarkStateMachine(b *testing.B) {
   203  	lineInfos := loadBenchmarkData(b)
   204  	b.ResetTimer()
   205  
   206  	for i := 0; i < b.N; i++ {
   207  		sm := newStateMachine(lineInfos[0], lineInfos[0].Instructions, ptrSizeByRuntimeArch())
   208  
   209  		for {
   210  			if err := sm.next(); err != nil {
   211  				break
   212  			}
   213  		}
   214  	}
   215  }
   216  
   217  type pctolineEntry struct {
   218  	pc   uint64
   219  	file string
   220  	line int
   221  }
   222  
   223  func (entry *pctolineEntry) match(file string, line int) bool {
   224  	if entry.file == "" {
   225  		return true
   226  	}
   227  	return entry.file == file && entry.line == line
   228  }
   229  
   230  func setupTestPCToLine(t testing.TB, lineInfos DebugLines) ([]pctolineEntry, []uint64) {
   231  	entries := []pctolineEntry{}
   232  	basePCs := []uint64{}
   233  
   234  	sm := newStateMachine(lineInfos[0], lineInfos[0].Instructions, ptrSizeByRuntimeArch())
   235  	for {
   236  		if err := sm.next(); err != nil {
   237  			break
   238  		}
   239  		if sm.valid {
   240  			if len(entries) == 0 || entries[len(entries)-1].pc != sm.address {
   241  				entries = append(entries, pctolineEntry{pc: sm.address, file: sm.file, line: sm.line})
   242  			} else if len(entries) > 0 {
   243  				// having two entries at the same PC address messes up the test
   244  				entries[len(entries)-1].file = ""
   245  			}
   246  			if len(basePCs) == 0 || sm.address-basePCs[len(basePCs)-1] >= 0x1000 {
   247  				basePCs = append(basePCs, sm.address)
   248  			}
   249  		}
   250  	}
   251  
   252  	for i := 1; i < len(entries); i++ {
   253  		if entries[i].pc <= entries[i-1].pc {
   254  			t.Fatalf("not monotonically increasing %d %x", i, entries[i].pc)
   255  		}
   256  	}
   257  
   258  	return entries, basePCs
   259  }
   260  
   261  func runTestPCToLine(t testing.TB, lineInfos DebugLines, entries []pctolineEntry, basePCs []uint64, log bool, testSize uint64) {
   262  	const samples = 1000
   263  	t0 := time.Now()
   264  
   265  	i := 0
   266  	basePCIdx := 0
   267  	for pc := entries[0].pc; pc <= entries[0].pc+testSize; pc++ {
   268  		if basePCIdx+1 < len(basePCs) && pc >= basePCs[basePCIdx+1] {
   269  			basePCIdx++
   270  		}
   271  		basePC := basePCs[basePCIdx]
   272  		file, line := lineInfos[0].PCToLine(basePC, pc)
   273  		if pc == entries[i].pc {
   274  			if i%samples == 0 && log {
   275  				fmt.Printf("match %x / %x (%v)\n", pc, entries[len(entries)-1].pc, time.Since(t0)/samples)
   276  				t0 = time.Now()
   277  			}
   278  
   279  			if !entries[i].match(file, line) {
   280  				t.Fatalf("Mismatch at PC %#x, expected %s:%d got %s:%d", pc, entries[i].file, entries[i].line, file, line)
   281  			}
   282  			i++
   283  		} else if !entries[i-1].match(file, line) {
   284  			t.Fatalf("Mismatch at PC %#x, expected %s:%d (from previous valid entry) got %s:%d", pc, entries[i-1].file, entries[i-1].line, file, line)
   285  		}
   286  	}
   287  }
   288  
   289  func TestPCToLine(t *testing.T) {
   290  	lineInfos := loadBenchmarkData(t)
   291  
   292  	entries, basePCs := setupTestPCToLine(t, lineInfos)
   293  	runTestPCToLine(t, lineInfos, entries, basePCs, true, 0x50000)
   294  	t.Logf("restart form beginning")
   295  	runTestPCToLine(t, lineInfos, entries, basePCs, true, 0x10000)
   296  }
   297  
   298  func BenchmarkPCToLine(b *testing.B) {
   299  	lineInfos := loadBenchmarkData(b)
   300  
   301  	entries, basePCs := setupTestPCToLine(b, lineInfos)
   302  	b.ResetTimer()
   303  	for i := 0; i < b.N; i++ {
   304  		runTestPCToLine(b, lineInfos, entries, basePCs, false, 0x10000)
   305  	}
   306  }
   307  
   308  func TestDebugLineC(t *testing.T) {
   309  	p, err := filepath.Abs("../../../_fixtures/debug_line_c_data")
   310  	if err != nil {
   311  		t.Fatal("Could not find test data", p, err)
   312  	}
   313  
   314  	data, err := os.ReadFile(p)
   315  	if err != nil {
   316  		t.Fatal("Could not read test data", err)
   317  	}
   318  
   319  	parsed := ParseAll(data, nil, nil, 0, true, ptrSizeByRuntimeArch())
   320  
   321  	if len(parsed) == 0 {
   322  		t.Fatal("Parser result is empty")
   323  	}
   324  
   325  	file := []string{"main.c", "/mnt/c/develop/delve/_fixtures/main.c", "/usr/lib/gcc/x86_64-linux-gnu/7/include/stddef.h",
   326  		"/usr/include/x86_64-linux-gnu/bits/types.h", "/usr/include/x86_64-linux-gnu/bits/libio.h", "/usr/include/stdio.h",
   327  		"/usr/include/x86_64-linux-gnu/bits/sys_errlist.h"}
   328  
   329  	for _, ln := range parsed {
   330  		if len(ln.FileNames) == 0 {
   331  			t.Fatal("Parser could not parse Filenames")
   332  		}
   333  		for _, fn := range ln.FileNames {
   334  			found := false
   335  			for _, cmp := range file {
   336  				if filepath.ToSlash(fn.Path) == cmp {
   337  					found = true
   338  					break
   339  				}
   340  			}
   341  			if !found {
   342  				t.Fatalf("Found %s does not appear in the filelist\n", fn.Path)
   343  			}
   344  		}
   345  	}
   346  }
   347  
   348  func TestDebugLineDwarf4(t *testing.T) {
   349  	p, err := filepath.Abs("../../../_fixtures/zdebug_line_dwarf4")
   350  	if err != nil {
   351  		t.Fatal("Could not find test data", p, err)
   352  	}
   353  	fh, err := os.Open(p)
   354  	if err != nil {
   355  		t.Fatal("Could not open test data", err)
   356  	}
   357  	defer fh.Close()
   358  	fh.Seek(12, 0) // skip "ZLIB" magic signature and length
   359  	r, err := zlib.NewReader(fh)
   360  	if err != nil {
   361  		t.Fatal("Could not open test data (zlib)", err)
   362  	}
   363  	data, err := io.ReadAll(r)
   364  	if err != nil {
   365  		t.Fatal("Could not read test data", err)
   366  	}
   367  
   368  	debugLines := ParseAll(data, nil, nil, 0, true, 8)
   369  
   370  	for _, dbl := range debugLines {
   371  		if dbl.Prologue.Version == 4 {
   372  			if dbl.Prologue.LineBase != -5 {
   373  				t.Errorf("Wrong LineBase %d\n", dbl.Prologue.LineBase)
   374  			}
   375  			if dbl.Prologue.LineRange != 14 {
   376  				t.Errorf("Wrong LineRange %d\n", dbl.Prologue.LineRange)
   377  			}
   378  		}
   379  	}
   380  
   381  }