delve/pkg/dwarf/line/state_machine.go
Alessandro Arzilli c3a4d726e2
Miscellaneous debug_line improvements (#1999)
* dwarf/line: implement DW_LNE_set_discriminator

We don't use the discriminator field in any way but we need to at least
parse it to support debub_line programs that use it.

* dwarf/line: support parsing DWARF4 debug_line sections

There is an extra field maximum_operations_per_instruction that is used
for VLIW CPUs. We don't support this feature but we have to at least
parse the field to not crash.
2020-04-09 13:57:44 -07:00

574 lines
14 KiB
Go

package line
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"github.com/go-delve/delve/pkg/dwarf/util"
)
type Location struct {
File string
Line int
Address uint64
Delta int
}
type StateMachine struct {
dbl *DebugLineInfo
file string
line int
address uint64
column uint
isStmt bool
isa uint64 // instruction set architecture register (DWARFv4)
basicBlock bool
endSeq bool
lastDelta int
prologueEnd bool
epilogueBegin bool
// valid is true if the current value of the state machine is the address of
// an instruction (using the terminology used by DWARF spec the current
// value of the state machine should be appended to the matrix representing
// the compilation unit)
valid bool
started bool
buf *bytes.Buffer // remaining instructions
opcodes []opcodefn
definedFiles []*FileEntry // files defined with DW_LINE_define_file
lastAddress uint64
lastFile string
lastLine int
ptrSize int
}
type opcodeKind uint8
const (
specialOpcode opcodeKind = iota
standardOpcode
extendedOpcode
)
type opcodefn func(*StateMachine, *bytes.Buffer)
// Special opcodes
const (
DW_LNS_copy = 1
DW_LNS_advance_pc = 2
DW_LNS_advance_line = 3
DW_LNS_set_file = 4
DW_LNS_set_column = 5
DW_LNS_negate_stmt = 6
DW_LNS_set_basic_block = 7
DW_LNS_const_add_pc = 8
DW_LNS_fixed_advance_pc = 9
DW_LNS_prologue_end = 10
DW_LNS_epilogue_begin = 11
DW_LNS_set_isa = 12
)
// Extended opcodes
const (
DW_LINE_end_sequence = 1
DW_LINE_set_address = 2
DW_LINE_define_file = 3
DW_LINE_set_discriminator = 4
)
var standardopcodes = map[byte]opcodefn{
DW_LNS_copy: copyfn,
DW_LNS_advance_pc: advancepc,
DW_LNS_advance_line: advanceline,
DW_LNS_set_file: setfile,
DW_LNS_set_column: setcolumn,
DW_LNS_negate_stmt: negatestmt,
DW_LNS_set_basic_block: setbasicblock,
DW_LNS_const_add_pc: constaddpc,
DW_LNS_fixed_advance_pc: fixedadvancepc,
DW_LNS_prologue_end: prologueend,
DW_LNS_epilogue_begin: epiloguebegin,
DW_LNS_set_isa: setisa,
}
var extendedopcodes = map[byte]opcodefn{
DW_LINE_end_sequence: endsequence,
DW_LINE_set_address: setaddress,
DW_LINE_define_file: definefile,
DW_LINE_set_discriminator: setdiscriminator,
}
func newStateMachine(dbl *DebugLineInfo, instructions []byte, ptrSize int) *StateMachine {
opcodes := make([]opcodefn, len(standardopcodes)+1)
opcodes[0] = execExtendedOpcode
for op := range standardopcodes {
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,
lastAddress: ^uint64(0),
ptrSize: ptrSize,
}
return sm
}
// AllPCsForFileLines Adds all PCs for a given file and set (domain of map) of lines
// to the map value corresponding to each line.
func (lineInfo *DebugLineInfo) AllPCsForFileLines(f string, m map[int][]uint64) {
if lineInfo == nil {
return
}
var (
lastAddr uint64
sm = newStateMachine(lineInfo, lineInfo.Instructions, lineInfo.ptrSize)
)
for {
if err := sm.next(); err != nil {
if lineInfo.Logf != nil {
lineInfo.Logf("AllPCsForFileLine error: %v", err)
}
break
}
if sm.address != lastAddr && sm.isStmt && sm.valid && sm.file == f {
if pcs, ok := m[sm.line]; ok {
pcs = append(pcs, sm.address)
m[sm.line] = pcs
lastAddr = sm.address
}
}
}
return
}
var NoSourceError = errors.New("no source available")
// AllPCsBetween returns all PC addresses between begin and end (including both begin and end) that have the is_stmt flag set and do not belong to excludeFile:excludeLine
func (lineInfo *DebugLineInfo) AllPCsBetween(begin, end uint64, excludeFile string, excludeLine int) ([]uint64, error) {
if lineInfo == nil {
return nil, NoSourceError
}
var (
pcs []uint64
lastaddr uint64
sm = newStateMachine(lineInfo, lineInfo.Instructions, lineInfo.ptrSize)
)
for {
if err := sm.next(); err != nil {
if lineInfo.Logf != nil {
lineInfo.Logf("AllPCsBetween error: %v", err)
}
break
}
if !sm.valid {
continue
}
if (sm.address > end) && (end >= sm.lastAddress) {
break
}
if sm.address >= begin && sm.address <= end && sm.address > lastaddr && sm.isStmt && ((sm.file != excludeFile) || (sm.line != excludeLine)) {
lastaddr = sm.address
pcs = append(pcs, sm.address)
}
}
return pcs, nil
}
// copy returns a copy of this state machine, running the returned state
// machine will not affect sm.
func (sm *StateMachine) copy() *StateMachine {
var r StateMachine
r = *sm
r.buf = bytes.NewBuffer(sm.buf.Bytes())
return &r
}
func (lineInfo *DebugLineInfo) stateMachineForEntry(basePC uint64) (sm *StateMachine) {
sm = lineInfo.stateMachineCache[basePC]
if sm == nil {
sm = newStateMachine(lineInfo, lineInfo.Instructions, lineInfo.ptrSize)
sm.PCToLine(basePC)
lineInfo.stateMachineCache[basePC] = sm
}
sm = sm.copy()
return
}
// PCToLine returns the filename and line number associated with pc.
// If pc isn't found inside lineInfo's table it will return the filename and
// line number associated with the closest PC address preceding pc.
// basePC will be used for caching, it's normally the entry point for the
// function containing pc.
func (lineInfo *DebugLineInfo) PCToLine(basePC, pc uint64) (string, int) {
if lineInfo == nil {
return "", 0
}
if basePC > pc {
panic(fmt.Errorf("basePC after pc %#x %#x", basePC, pc))
}
sm := lineInfo.stateMachineFor(basePC, pc)
file, line, _ := sm.PCToLine(pc)
return file, line
}
func (lineInfo *DebugLineInfo) stateMachineFor(basePC, pc uint64) *StateMachine {
var sm *StateMachine
if basePC == 0 {
sm = newStateMachine(lineInfo, lineInfo.Instructions, lineInfo.ptrSize)
} else {
// Try to use the last state machine that we used for this function, if
// there isn't one or it's already past pc try to clone the cached state
// machine stopped at the entry point of the function.
// As a last resort start from the start of the debug_line section.
sm = lineInfo.lastMachineCache[basePC]
if sm == nil || sm.lastAddress >= pc {
sm = lineInfo.stateMachineForEntry(basePC)
lineInfo.lastMachineCache[basePC] = sm
}
}
return sm
}
func (sm *StateMachine) PCToLine(pc uint64) (string, int, bool) {
if !sm.started {
if err := sm.next(); err != nil {
if sm.dbl.Logf != nil {
sm.dbl.Logf("PCToLine error: %v", err)
}
return "", 0, false
}
}
if sm.lastAddress > pc && sm.lastAddress != ^uint64(0) {
return "", 0, false
}
for {
if sm.valid {
if (sm.address > pc) && (pc >= sm.lastAddress) {
return sm.lastFile, sm.lastLine, true
}
if sm.address == pc {
return sm.file, sm.line, true
}
}
if err := sm.next(); err != nil {
if sm.dbl.Logf != nil {
sm.dbl.Logf("PCToLine error: %v", err)
}
break
}
}
if sm.valid {
return sm.file, sm.line, true
}
return "", 0, false
}
// LineToPC returns the first PC address associated with filename:lineno.
func (lineInfo *DebugLineInfo) LineToPC(filename string, lineno int) uint64 {
if lineInfo == nil {
return 0
}
sm := newStateMachine(lineInfo, lineInfo.Instructions, lineInfo.ptrSize)
// if no instruction marked is_stmt is found fallback to the first
// instruction assigned to the filename:line.
var fallbackPC uint64
for {
if err := sm.next(); err != nil {
if lineInfo.Logf != nil && err != io.EOF {
lineInfo.Logf("LineToPC error: %v", err)
}
break
}
if sm.line == lineno && sm.file == filename && sm.valid {
if sm.isStmt {
return sm.address
} else if fallbackPC == 0 {
fallbackPC = sm.address
}
}
}
return fallbackPC
}
// LineToPCIn returns the first PC for filename:lineno in the interval [startPC, endPC).
// This function is used to find the instruction corresponding to
// filename:lineno for a function that has been inlined.
// basePC will be used for caching, it's normally the entry point for the
// function containing pc.
func (lineInfo *DebugLineInfo) LineToPCIn(filename string, lineno int, basePC, startPC, endPC uint64) uint64 {
if lineInfo == nil {
return 0
}
if basePC > startPC {
panic(fmt.Errorf("basePC after startPC %#x %#x", basePC, startPC))
}
sm := lineInfo.stateMachineFor(basePC, startPC)
var fallbackPC uint64
for {
if sm.valid && sm.started {
if sm.address >= endPC {
break
}
if sm.line == lineno && sm.file == filename && sm.address >= startPC {
if sm.isStmt {
return sm.address
} else {
fallbackPC = sm.address
}
}
}
if err := sm.next(); err != nil {
if lineInfo.Logf != nil && err != io.EOF {
lineInfo.Logf("LineToPC error: %v", err)
}
break
}
}
return fallbackPC
}
// PrologueEndPC returns the first PC address marked as prologue_end in the half open interval [start, end)
func (lineInfo *DebugLineInfo) PrologueEndPC(start, end uint64) (pc uint64, file string, line int, ok bool) {
if lineInfo == nil {
return 0, "", 0, false
}
sm := lineInfo.stateMachineForEntry(start)
for {
if sm.valid {
if sm.address >= end {
return 0, "", 0, false
}
if sm.prologueEnd {
return sm.address, sm.file, sm.line, true
}
}
if err := sm.next(); err != nil {
if lineInfo.Logf != nil {
lineInfo.Logf("PrologueEnd error: %v", err)
}
return 0, "", 0, false
}
}
}
// FirstStmtForLine looks in the half open interval [start, end) for the
// first PC address marked as stmt for the line at address 'start'.
func (lineInfo *DebugLineInfo) FirstStmtForLine(start, end uint64) (pc uint64, file string, line int, ok bool) {
first := true
sm := lineInfo.stateMachineForEntry(start)
for {
if sm.valid {
if sm.address >= end {
return 0, "", 0, false
}
if first {
first = false
file, line = sm.file, sm.line
}
if sm.isStmt && sm.file == file && sm.line == line {
return sm.address, sm.file, sm.line, true
}
}
if err := sm.next(); err != nil {
if lineInfo.Logf != nil {
lineInfo.Logf("FirstStmtForLine error: %v", err)
}
return 0, "", 0, false
}
}
}
func (lineInfo *DebugLineInfo) FirstFile() string {
sm := newStateMachine(lineInfo, lineInfo.Instructions, lineInfo.ptrSize)
for {
if sm.valid {
return sm.file
}
if err := sm.next(); err != nil {
if lineInfo.Logf != nil {
lineInfo.Logf("FirstFile error: %v", err)
}
return ""
}
}
}
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.isa = 0
sm.isStmt = sm.dbl.Prologue.InitialIsStmt == uint8(1)
sm.basicBlock = false
sm.lastAddress = ^uint64(0)
}
b, err := sm.buf.ReadByte()
if err != nil {
return err
}
if b < sm.dbl.Prologue.OpcodeBase {
if int(b) < len(sm.opcodes) {
sm.valid = false
sm.opcodes[b](sm, sm.buf)
} else {
// 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 %d(0x%x), %d arguments, file %s, line %d, address 0x%x\n", b, b, opnum, sm.file, sm.line, sm.address)
}
} else {
execSpecialOpcode(sm, b)
}
return nil
}
func execSpecialOpcode(sm *StateMachine, instr byte) {
var (
opcode = uint8(instr)
decoded = opcode - sm.dbl.Prologue.OpcodeBase
)
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.valid = true
}
func execExtendedOpcode(sm *StateMachine, buf *bytes.Buffer) {
_, _ = util.DecodeULEB128(buf)
b, _ := buf.ReadByte()
if fn, ok := extendedopcodes[b]; ok {
fn(sm, buf)
}
}
func copyfn(sm *StateMachine, buf *bytes.Buffer) {
sm.valid = true
}
func advancepc(sm *StateMachine, buf *bytes.Buffer) {
addr, _ := util.DecodeULEB128(buf)
sm.address += addr * uint64(sm.dbl.Prologue.MinInstrLength)
}
func advanceline(sm *StateMachine, buf *bytes.Buffer) {
line, _ := util.DecodeSLEB128(buf)
sm.line += int(line)
sm.lastDelta = int(line)
}
func setfile(sm *StateMachine, buf *bytes.Buffer) {
i, _ := util.DecodeULEB128(buf)
if i-1 < uint64(len(sm.dbl.FileNames)) {
sm.file = sm.dbl.FileNames[i-1].Path
} else {
j := (i - 1) - uint64(len(sm.dbl.FileNames))
if j < uint64(len(sm.definedFiles)) {
sm.file = sm.definedFiles[j].Path
} else {
sm.file = ""
}
}
}
func setcolumn(sm *StateMachine, buf *bytes.Buffer) {
c, _ := util.DecodeULEB128(buf)
sm.column = uint(c)
}
func negatestmt(sm *StateMachine, buf *bytes.Buffer) {
sm.isStmt = !sm.isStmt
}
func setbasicblock(sm *StateMachine, buf *bytes.Buffer) {
sm.basicBlock = true
}
func constaddpc(sm *StateMachine, buf *bytes.Buffer) {
sm.address += uint64((255-sm.dbl.Prologue.OpcodeBase)/sm.dbl.Prologue.LineRange) * uint64(sm.dbl.Prologue.MinInstrLength)
}
func fixedadvancepc(sm *StateMachine, buf *bytes.Buffer) {
var operand uint16
binary.Read(buf, binary.LittleEndian, &operand)
sm.address += uint64(operand)
}
func endsequence(sm *StateMachine, buf *bytes.Buffer) {
sm.endSeq = true
sm.valid = true
}
func setaddress(sm *StateMachine, buf *bytes.Buffer) {
addr, err := util.ReadUintRaw(buf, binary.LittleEndian, sm.ptrSize)
if err != nil {
panic(err)
}
sm.address = addr + sm.dbl.staticBase
}
func setdiscriminator(sm *StateMachine, buf *bytes.Buffer) {
_, _ = util.DecodeULEB128(buf)
}
func definefile(sm *StateMachine, buf *bytes.Buffer) {
entry := readFileEntry(sm.dbl, sm.buf, false)
sm.definedFiles = append(sm.definedFiles, entry)
}
func prologueend(sm *StateMachine, buf *bytes.Buffer) {
sm.prologueEnd = true
}
func epiloguebegin(sm *StateMachine, buf *bytes.Buffer) {
sm.epilogueBegin = true
}
func setisa(sm *StateMachine, buf *bytes.Buffer) {
c, _ := util.DecodeULEB128(buf)
sm.isa = c
}