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 }