dwarf/line: fix state machine behavior with multi-sequence units (#1681)

A compile unit can produce a debug_line program consisting of multiple
sequences according to the DWARF standard. The standard guarantees that
addresses monotonically increment within a single sequence but
different sequences may not follow this rule.

This commit changes dwarf/line (in particular PCToLine and
AllPCsBetween) to support debug_line sections containing units with
multiple sequences.

TestPCToLine needs to be changed so that it picks valid addresses (i.e.
addresses covered by a sequence) as values for basePC, instead of just
rounding.

Fixes #1694
This commit is contained in:
Alessandro Arzilli 2019-10-07 18:54:32 +02:00 committed by Derek Parker
parent ca6c6301cd
commit fb3941324b
3 changed files with 167 additions and 14 deletions

@ -210,8 +210,9 @@ func (entry *pctolineEntry) match(file string, line int) bool {
return entry.file == file && entry.line == line return entry.file == file && entry.line == line
} }
func setupTestPCToLine(t testing.TB, lineInfos DebugLines) []pctolineEntry { func setupTestPCToLine(t testing.TB, lineInfos DebugLines) ([]pctolineEntry, []uint64) {
entries := []pctolineEntry{} entries := []pctolineEntry{}
basePCs := []uint64{}
sm := newStateMachine(lineInfos[0], lineInfos[0].Instructions) sm := newStateMachine(lineInfos[0], lineInfos[0].Instructions)
for { for {
@ -225,6 +226,9 @@ func setupTestPCToLine(t testing.TB, lineInfos DebugLines) []pctolineEntry {
// having two entries at the same PC address messes up the test // having two entries at the same PC address messes up the test
entries[len(entries)-1].file = "" entries[len(entries)-1].file = ""
} }
if len(basePCs) == 0 || sm.address-basePCs[len(basePCs)-1] >= 0x1000 {
basePCs = append(basePCs, sm.address)
}
} }
} }
@ -234,16 +238,21 @@ func setupTestPCToLine(t testing.TB, lineInfos DebugLines) []pctolineEntry {
} }
} }
return entries return entries, basePCs
} }
func runTestPCToLine(t testing.TB, lineInfos DebugLines, entries []pctolineEntry, log bool, testSize uint64) { func runTestPCToLine(t testing.TB, lineInfos DebugLines, entries []pctolineEntry, basePCs []uint64, log bool, testSize uint64) {
const samples = 1000 const samples = 1000
t0 := time.Now() t0 := time.Now()
i := 0 i := 0
basePCIdx := 0
for pc := entries[0].pc; pc <= entries[0].pc+testSize; pc++ { for pc := entries[0].pc; pc <= entries[0].pc+testSize; pc++ {
file, line := lineInfos[0].PCToLine(pc/0x1000*0x1000, pc) if basePCIdx+1 < len(basePCs) && pc >= basePCs[basePCIdx+1] {
basePCIdx++
}
basePC := basePCs[basePCIdx]
file, line := lineInfos[0].PCToLine(basePC, pc)
if pc == entries[i].pc { if pc == entries[i].pc {
if i%samples == 0 && log { if i%samples == 0 && log {
fmt.Printf("match %x / %x (%v)\n", pc, entries[len(entries)-1].pc, time.Since(t0)/samples) fmt.Printf("match %x / %x (%v)\n", pc, entries[len(entries)-1].pc, time.Since(t0)/samples)
@ -265,18 +274,18 @@ func runTestPCToLine(t testing.TB, lineInfos DebugLines, entries []pctolineEntry
func TestPCToLine(t *testing.T) { func TestPCToLine(t *testing.T) {
lineInfos := loadBenchmarkData(t) lineInfos := loadBenchmarkData(t)
entries := setupTestPCToLine(t, lineInfos) entries, basePCs := setupTestPCToLine(t, lineInfos)
runTestPCToLine(t, lineInfos, entries, true, 0x50000) runTestPCToLine(t, lineInfos, entries, basePCs, true, 0x50000)
t.Logf("restart form beginning") t.Logf("restart form beginning")
runTestPCToLine(t, lineInfos, entries, true, 0x10000) runTestPCToLine(t, lineInfos, entries, basePCs, true, 0x10000)
} }
func BenchmarkPCToLine(b *testing.B) { func BenchmarkPCToLine(b *testing.B) {
lineInfos := loadBenchmarkData(b) lineInfos := loadBenchmarkData(b)
entries := setupTestPCToLine(b, lineInfos) entries, basePCs := setupTestPCToLine(b, lineInfos)
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
runTestPCToLine(b, lineInfos, entries, false, 0x10000) runTestPCToLine(b, lineInfos, entries, basePCs, false, 0x10000)
} }
} }

@ -108,7 +108,7 @@ func newStateMachine(dbl *DebugLineInfo, instructions []byte) *StateMachine {
for op := range standardopcodes { for op := range standardopcodes {
opcodes[op] = standardopcodes[op] opcodes[op] = standardopcodes[op]
} }
sm := &StateMachine{dbl: dbl, file: dbl.FileNames[0].Path, line: 1, buf: bytes.NewBuffer(instructions), opcodes: opcodes, isStmt: dbl.Prologue.InitialIsStmt == uint8(1), address: dbl.staticBase} sm := &StateMachine{dbl: dbl, file: dbl.FileNames[0].Path, line: 1, buf: bytes.NewBuffer(instructions), opcodes: opcodes, isStmt: dbl.Prologue.InitialIsStmt == uint8(1), address: dbl.staticBase, lastAddress: ^uint64(0)}
return sm return sm
} }
@ -193,10 +193,10 @@ func (lineInfo *DebugLineInfo) AllPCsBetween(begin, end uint64, excludeFile stri
if !sm.valid { if !sm.valid {
continue continue
} }
if sm.address > end { if (sm.address > end) && (end >= sm.lastAddress) {
break break
} }
if (sm.address >= begin && sm.address > lastaddr) && sm.isStmt && ((sm.file != excludeFile) || (sm.line != excludeLine)) { if sm.address >= begin && sm.address <= end && sm.address > lastaddr && sm.isStmt && ((sm.file != excludeFile) || (sm.line != excludeLine)) {
lastaddr = sm.address lastaddr = sm.address
pcs = append(pcs, sm.address) pcs = append(pcs, sm.address)
} }
@ -265,12 +265,12 @@ func (sm *StateMachine) PCToLine(pc uint64) (string, int, bool) {
return "", 0, false return "", 0, false
} }
} }
if sm.lastAddress > pc { if sm.lastAddress > pc && sm.lastAddress != ^uint64(0) {
return "", 0, false return "", 0, false
} }
for { for {
if sm.valid { if sm.valid {
if sm.address > pc { if (sm.address > pc) && (pc >= sm.lastAddress) {
return sm.lastFile, sm.lastLine, true return sm.lastFile, sm.lastLine, true
} }
if sm.address == pc { if sm.address == pc {
@ -360,6 +360,7 @@ func (sm *StateMachine) next() error {
sm.isa = 0 sm.isa = 0
sm.isStmt = sm.dbl.Prologue.InitialIsStmt == uint8(1) sm.isStmt = sm.dbl.Prologue.InitialIsStmt == uint8(1)
sm.basicBlock = false sm.basicBlock = false
sm.lastAddress = ^uint64(0)
} }
b, err := sm.buf.ReadByte() b, err := sm.buf.ReadByte()
if err != nil { if err != nil {
@ -463,6 +464,7 @@ func endsequence(sm *StateMachine, buf *bytes.Buffer) {
} }
func setaddress(sm *StateMachine, buf *bytes.Buffer) { func setaddress(sm *StateMachine, buf *bytes.Buffer) {
//TODO: this needs to be changed to support 32bit architectures (addr must be target arch pointer sized) -- also target endianness
var addr uint64 var addr uint64
binary.Read(buf, binary.LittleEndian, &addr) binary.Read(buf, binary.LittleEndian, &addr)

@ -5,12 +5,15 @@ import (
"compress/gzip" "compress/gzip"
"debug/dwarf" "debug/dwarf"
"debug/macho" "debug/macho"
"encoding/binary"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"os" "os"
"runtime" "runtime"
"testing" "testing"
"github.com/go-delve/delve/pkg/dwarf/util"
) )
func slurpGzip(path string) ([]byte, error) { func slurpGzip(path string) ([]byte, error) {
@ -126,3 +129,142 @@ func checkCompileUnit(t *testing.T, cuname string, lnrdr *dwarf.LineReader, sm *
t.Fatalf("state machine ended before the line reader for compile unit %s", cuname) t.Fatalf("state machine ended before the line reader for compile unit %s", cuname)
} }
} }
func TestMultipleSequences(t *testing.T) {
// Check that our state machine (specifically PCToLine and AllPCsBetween)
// are correct when dealing with units containing more than one sequence.
const thefile = "thefile.go"
instr := bytes.NewBuffer(nil)
write_DW_LNE_set_address := func(addr uint64) {
instr.WriteByte(0)
util.EncodeULEB128(instr, 9) // 1 + ptr_size
instr.WriteByte(DW_LINE_set_address)
binary.Write(instr, binary.LittleEndian, addr)
}
write_DW_LNS_copy := func() {
instr.WriteByte(DW_LNS_copy)
}
write_DW_LNS_advance_pc := func(off uint64) {
instr.WriteByte(DW_LNS_advance_pc)
util.EncodeULEB128(instr, off)
}
write_DW_LNS_advance_line := func(off int64) {
instr.WriteByte(DW_LNS_advance_line)
util.EncodeSLEB128(instr, off)
}
write_DW_LNE_end_sequence := func() {
instr.WriteByte(0)
util.EncodeULEB128(instr, 1)
instr.WriteByte(DW_LINE_end_sequence)
}
write_DW_LNE_set_address(0x400000)
write_DW_LNS_copy() // thefile.go:1 0x400000
write_DW_LNS_advance_pc(0x2)
write_DW_LNS_advance_line(1)
write_DW_LNS_copy() // thefile.go:2 0x400002
write_DW_LNS_advance_pc(0x2)
write_DW_LNS_advance_line(1)
write_DW_LNS_copy() // thefile.go:3 0x400004
write_DW_LNS_advance_pc(0x2)
write_DW_LNE_end_sequence() // thefile.go:3 ends the byte before 0x400006
write_DW_LNE_set_address(0x600000)
write_DW_LNS_advance_line(10)
write_DW_LNS_copy() // thefile.go:11 0x600000
write_DW_LNS_advance_pc(0x2)
write_DW_LNS_advance_line(1)
write_DW_LNS_copy() // thefile.go:12 0x600002
write_DW_LNS_advance_pc(0x2)
write_DW_LNS_advance_line(1)
write_DW_LNS_copy() // thefile.go:13 0x600004
write_DW_LNS_advance_pc(0x2)
write_DW_LNE_end_sequence() // thefile.go:13 ends the byte before 0x600006
write_DW_LNE_set_address(0x500000)
write_DW_LNS_advance_line(20)
write_DW_LNS_copy() // thefile.go:21 0x500000
write_DW_LNS_advance_pc(0x2)
write_DW_LNS_advance_line(1)
write_DW_LNS_copy() // thefile.go:22 0x500002
write_DW_LNS_advance_pc(0x2)
write_DW_LNS_advance_line(1)
write_DW_LNS_copy() // thefile.go:23 0x500004
write_DW_LNS_advance_pc(0x2)
write_DW_LNE_end_sequence() // thefile.go:23 ends the byte before 0x500006
lines := &DebugLineInfo{
Prologue: &DebugLinePrologue{
UnitLength: 1,
Version: 2,
MinInstrLength: 1,
InitialIsStmt: 1,
LineBase: -3,
LineRange: 12,
OpcodeBase: 13,
StdOpLengths: []uint8{0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1},
},
IncludeDirs: []string{},
FileNames: []*FileEntry{&FileEntry{Path: thefile}},
Instructions: instr.Bytes(),
}
// Test that PCToLine is correct for all three sequences
for _, testCase := range []struct {
pc uint64
line int
}{
{0x400000, 1},
{0x400002, 2},
{0x400004, 3},
{0x500000, 21},
{0x500002, 22},
{0x500004, 23},
{0x600000, 11},
{0x600002, 12},
{0x600004, 13},
} {
sm := newStateMachine(lines, lines.Instructions)
file, curline, ok := sm.PCToLine(testCase.pc)
if !ok {
t.Fatalf("Could not find %#x", testCase.pc)
}
if file != thefile {
t.Fatalf("Wrong file returned for %#x %q", testCase.pc, file)
}
if curline != testCase.line {
t.Errorf("Wrong line returned for %#x: got %d expected %d", testCase.pc, curline, testCase.line)
}
}
// Test that AllPCsBetween is correct for all three sequences
for _, testCase := range []struct {
start, end uint64
tgt []uint64
}{
{0x400000, 0x400005, []uint64{0x400000, 0x400002, 0x400004}},
{0x500000, 0x500005, []uint64{0x500000, 0x500002, 0x500004}},
{0x600000, 0x600005, []uint64{0x600000, 0x600002, 0x600004}},
} {
out, err := lines.AllPCsBetween(testCase.start, testCase.end, "", -1)
if err != nil {
t.Fatalf("AllPCsBetween(%#x, %#x): %v", testCase.start, testCase.end, err)
}
if len(out) != len(testCase.tgt) {
t.Errorf("AllPCsBetween(%#x, %#x): expected: %#x got: %#x", testCase.start, testCase.end, testCase.tgt, out)
}
}
}