dwarf/line: fix some bugs with the state machine
Adds a test that compares the output of our state machine with the output of the debug_line reader in the standard library and checks that they produce the same output for the debug_line section of grafana as compiled on macOS (which is the most interesting case since it uses cgo and therefore goes through dsymutil). A few bugs were uncovered and fixed: 1. is_stmt was reset improperly after a DW_LNS_end_sequence instruction 2. basic_block, prologue_end and epilogue_begin were not reset after a DW_LNS_copy instruction 3. some opcodes were not decoded properly if the debug_line section declares fewer standard opcodes than we know about. Fixes #1282
This commit is contained in:
parent
8f1fc63da8
commit
3f9875e272
BIN
pkg/dwarf/line/_testdata/debug.grafana.debug.gz
Executable file
BIN
pkg/dwarf/line/_testdata/debug.grafana.debug.gz
Executable file
Binary file not shown.
@ -5,6 +5,7 @@ import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/derekparker/delve/pkg/dwarf/util"
|
||||
)
|
||||
@ -34,8 +35,6 @@ type StateMachine struct {
|
||||
// the compilation unit)
|
||||
valid bool
|
||||
|
||||
lastOpcodeKind opcodeKind
|
||||
|
||||
started bool
|
||||
|
||||
buf *bytes.Buffer // remaining instructions
|
||||
@ -272,7 +271,7 @@ func (lineInfo *DebugLineInfo) LineToPC(filename string, lineno int) uint64 {
|
||||
|
||||
for {
|
||||
if err := sm.next(); err != nil {
|
||||
if lineInfo.Logf != nil {
|
||||
if lineInfo.Logf != nil && err != io.EOF {
|
||||
lineInfo.Logf("LineToPC error: %v", err)
|
||||
}
|
||||
break
|
||||
@ -313,38 +312,37 @@ func (sm *StateMachine) next() error {
|
||||
sm.started = true
|
||||
if sm.valid {
|
||||
sm.lastAddress, sm.lastFile, sm.lastLine = sm.address, sm.file, sm.line
|
||||
|
||||
// valid is set by either a special opcode or a DW_LNS_copy, in both cases
|
||||
// we need to reset basic_block, prologue_end and epilogue_begin
|
||||
sm.basicBlock = false
|
||||
sm.prologueEnd = false
|
||||
sm.epilogueBegin = false
|
||||
}
|
||||
if sm.endSeq {
|
||||
sm.endSeq = false
|
||||
sm.file = sm.dbl.FileNames[0].Path
|
||||
sm.line = 1
|
||||
sm.column = 0
|
||||
sm.isStmt = false
|
||||
sm.isStmt = sm.dbl.Prologue.InitialIsStmt == uint8(1)
|
||||
sm.basicBlock = false
|
||||
}
|
||||
if sm.lastOpcodeKind == specialOpcode {
|
||||
sm.basicBlock = false
|
||||
sm.prologueEnd = false
|
||||
sm.epilogueBegin = false
|
||||
}
|
||||
b, err := sm.buf.ReadByte()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if int(b) < len(sm.opcodes) {
|
||||
if b == 0 {
|
||||
sm.lastOpcodeKind = extendedOpcode
|
||||
if b < sm.dbl.Prologue.OpcodeBase {
|
||||
if int(b) < len(sm.opcodes) {
|
||||
sm.valid = false
|
||||
sm.opcodes[b](sm, sm.buf)
|
||||
} else {
|
||||
sm.lastOpcodeKind = standardOpcode
|
||||
}
|
||||
sm.valid = false
|
||||
sm.opcodes[b](sm, sm.buf)
|
||||
} else if b < sm.dbl.Prologue.OpcodeBase {
|
||||
// unimplemented standard opcode, read the number of arguments specified
|
||||
// in the prologue and do nothing with them
|
||||
opnum := sm.dbl.Prologue.StdOpLengths[b-1]
|
||||
for i := 0; i < int(opnum); i++ {
|
||||
util.DecodeSLEB128(sm.buf)
|
||||
// unimplemented standard opcode, read the number of arguments specified
|
||||
// in the prologue and do nothing with them
|
||||
opnum := sm.dbl.Prologue.StdOpLengths[b-1]
|
||||
for i := 0; i < int(opnum); i++ {
|
||||
util.DecodeSLEB128(sm.buf)
|
||||
}
|
||||
fmt.Printf("unknown opcode\n")
|
||||
}
|
||||
} else {
|
||||
execSpecialOpcode(sm, b)
|
||||
@ -361,8 +359,6 @@ func execSpecialOpcode(sm *StateMachine, instr byte) {
|
||||
sm.lastDelta = int(sm.dbl.Prologue.LineBase + int8(decoded%sm.dbl.Prologue.LineRange))
|
||||
sm.line += sm.lastDelta
|
||||
sm.address += uint64(decoded/sm.dbl.Prologue.LineRange) * uint64(sm.dbl.Prologue.MinInstrLength)
|
||||
sm.basicBlock = false
|
||||
sm.lastOpcodeKind = specialOpcode
|
||||
sm.valid = true
|
||||
}
|
||||
|
||||
@ -375,7 +371,6 @@ func execExtendedOpcode(sm *StateMachine, buf *bytes.Buffer) {
|
||||
}
|
||||
|
||||
func copyfn(sm *StateMachine, buf *bytes.Buffer) {
|
||||
sm.basicBlock = false
|
||||
sm.valid = true
|
||||
}
|
||||
|
||||
|
128
pkg/dwarf/line/state_machine_test.go
Normal file
128
pkg/dwarf/line/state_machine_test.go
Normal file
@ -0,0 +1,128 @@
|
||||
package line
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"debug/dwarf"
|
||||
"debug/macho"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func slurpGzip(path string) ([]byte, error) {
|
||||
fh, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer fh.Close()
|
||||
gzin, err := gzip.NewReader(fh)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer gzin.Close()
|
||||
return ioutil.ReadAll(gzin)
|
||||
}
|
||||
|
||||
const (
|
||||
newCompileUnit = "NEW COMPILE UNIT"
|
||||
debugLineEnd = "END"
|
||||
)
|
||||
|
||||
func TestGrafana(t *testing.T) {
|
||||
// Compares a full execution of our state machine on the debug_line section
|
||||
// of grafana to the output generated using debug/dwarf.LineReader on the
|
||||
// same section.
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("filepath.Join ruins this test on windows")
|
||||
}
|
||||
|
||||
debugBytes, err := slurpGzip("_testdata/debug.grafana.debug.gz")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
exe, err := macho.NewFile(bytes.NewReader(debugBytes))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
sec := exe.Section("__debug_line")
|
||||
debugLineBytes, err := sec.Data()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
data, err := exe.DWARF()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
debugLineBuffer := bytes.NewBuffer(debugLineBytes)
|
||||
rdr := data.Reader()
|
||||
for {
|
||||
e, err := rdr.Next()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if e == nil {
|
||||
break
|
||||
}
|
||||
rdr.SkipChildren()
|
||||
if e.Tag != dwarf.TagCompileUnit {
|
||||
continue
|
||||
}
|
||||
cuname, _ := e.Val(dwarf.AttrName).(string)
|
||||
|
||||
lineInfo := Parse(e.Val(dwarf.AttrCompDir).(string), debugLineBuffer, t.Logf)
|
||||
sm := newStateMachine(lineInfo, lineInfo.Instructions)
|
||||
|
||||
lnrdr, err := data.LineReader(e)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
checkCompileUnit(t, cuname, lnrdr, sm)
|
||||
}
|
||||
}
|
||||
|
||||
func checkCompileUnit(t *testing.T, cuname string, lnrdr *dwarf.LineReader, sm *StateMachine) {
|
||||
var lne dwarf.LineEntry
|
||||
for {
|
||||
if err := sm.next(); err != nil {
|
||||
if err != io.EOF {
|
||||
t.Fatalf("state machine next error: %v", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
if !sm.valid {
|
||||
continue
|
||||
}
|
||||
|
||||
err := lnrdr.Next(&lne)
|
||||
if err == io.EOF {
|
||||
t.Fatalf("line reader ended before our state machine for compile unit %s", cuname)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tgt := fmt.Sprintf("%#x %s:%d isstmt:%v prologue_end:%v epilogue_begin:%v", lne.Address, lne.File.Name, lne.Line, lne.IsStmt, lne.PrologueEnd, lne.EpilogueBegin)
|
||||
|
||||
out := fmt.Sprintf("%#x %s:%d isstmt:%v prologue_end:%v epilogue_begin:%v", sm.address, sm.file, sm.line, sm.isStmt, sm.prologueEnd, sm.epilogueBegin)
|
||||
if out != tgt {
|
||||
t.Errorf("mismatch:\n")
|
||||
t.Errorf("got:\t%s\n", out)
|
||||
t.Errorf("expected:\t%s\n", tgt)
|
||||
t.Fatal("previous error")
|
||||
}
|
||||
}
|
||||
|
||||
err := lnrdr.Next(&lne)
|
||||
if err != io.EOF {
|
||||
t.Fatalf("state machine ended before the line reader for compile unit %s", cuname)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user