diff --git a/pkg/dwarf/line/line_parser.go b/pkg/dwarf/line/line_parser.go index 0d3677f1..6506c3d3 100644 --- a/pkg/dwarf/line/line_parser.go +++ b/pkg/dwarf/line/line_parser.go @@ -3,6 +3,7 @@ package line import ( "bytes" "encoding/binary" + "path/filepath" "github.com/derekparker/delve/pkg/dwarf/util" ) @@ -28,7 +29,7 @@ type DebugLineInfo struct { } type FileEntry struct { - Name string + Path string DirIdx uint64 LastModTime uint64 Length uint64 @@ -36,17 +37,8 @@ type FileEntry struct { type DebugLines []*DebugLineInfo -func (d *DebugLines) GetLineInfo(name string) *DebugLineInfo { - // Find in which table file exists and return it. - for _, l := range *d { - if _, ok := l.Lookup[name]; ok { - return l - } - } - return nil -} - -func Parse(data []byte) DebugLines { +// ParseAll parses all debug_line segments found in data +func ParseAll(data []byte) DebugLines { var ( lines = make(DebugLines, 0) buf = bytes.NewBuffer(data) @@ -54,25 +46,34 @@ func Parse(data []byte) DebugLines { // We have to parse multiple file name tables here. for buf.Len() > 0 { - dbl := new(DebugLineInfo) - dbl.Lookup = make(map[string]*FileEntry) - - parseDebugLinePrologue(dbl, buf) - parseIncludeDirs(dbl, buf) - parseFileEntries(dbl, buf) - - // Instructions size calculation breakdown: - // - dbl.Prologue.UnitLength is the length of the entire unit, not including the 4 bytes to represent that length. - // - dbl.Prologue.Length is the length of the prologue not including unit length, version or prologue length itself. - // - So you have UnitLength - PrologueLength - (version_length_bytes(2) + prologue_length_bytes(4)). - dbl.Instructions = buf.Next(int(dbl.Prologue.UnitLength - dbl.Prologue.Length - 6)) - - lines = append(lines, dbl) + lines = append(lines, Parse("", buf)) } return lines } +// Parse parses a single debug_line segment from buf. Compdir is the +// DW_AT_comp_dir attribute of the associated compile unit. +func Parse(compdir string, buf *bytes.Buffer) *DebugLineInfo { + dbl := new(DebugLineInfo) + dbl.Lookup = make(map[string]*FileEntry) + if compdir != "" { + dbl.IncludeDirs = append(dbl.IncludeDirs, compdir) + } + + parseDebugLinePrologue(dbl, buf) + parseIncludeDirs(dbl, buf) + parseFileEntries(dbl, buf) + + // Instructions size calculation breakdown: + // - dbl.Prologue.UnitLength is the length of the entire unit, not including the 4 bytes to represent that length. + // - dbl.Prologue.Length is the length of the prologue not including unit length, version or prologue length itself. + // - So you have UnitLength - PrologueLength - (version_length_bytes(2) + prologue_length_bytes(4)). + dbl.Instructions = buf.Next(int(dbl.Prologue.UnitLength - dbl.Prologue.Length - 6)) + + return dbl +} + func parseDebugLinePrologue(dbl *DebugLineInfo, buf *bytes.Buffer) { p := new(DebugLinePrologue) @@ -106,17 +107,21 @@ func parseFileEntries(info *DebugLineInfo, buf *bytes.Buffer) { for { entry := new(FileEntry) - name, _ := util.ParseString(buf) - if name == "" { + entry.Path, _ = util.ParseString(buf) + if entry.Path == "" { break } - entry.Name = name entry.DirIdx, _ = util.DecodeULEB128(buf) entry.LastModTime, _ = util.DecodeULEB128(buf) entry.Length, _ = util.DecodeULEB128(buf) + if !filepath.IsAbs(entry.Path) { + if entry.DirIdx >= 0 && entry.DirIdx < uint64(len(info.IncludeDirs)) { + entry.Path = filepath.Join(info.IncludeDirs[entry.DirIdx], entry.Path) + } + } info.FileNames = append(info.FileNames, entry) - info.Lookup[name] = entry + info.Lookup[entry.Path] = entry } } diff --git a/pkg/dwarf/line/line_parser_test.go b/pkg/dwarf/line/line_parser_test.go index ac43c303..3bfd3e95 100644 --- a/pkg/dwarf/line/line_parser_test.go +++ b/pkg/dwarf/line/line_parser_test.go @@ -60,7 +60,7 @@ const ( func testDebugLinePrologueParser(p string, t *testing.T) { data := grabDebugLineSection(p, t) - debugLines := Parse(data) + debugLines := ParseAll(data) dbl := debugLines[0] prologue := dbl.Prologue @@ -105,7 +105,7 @@ func testDebugLinePrologueParser(p string, t *testing.T) { ok := false for _, n := range dbl.FileNames { - if strings.Contains(n.Name, "/delve/_fixtures/testnextprog.go") { + if strings.Contains(n.Path, "/delve/_fixtures/testnextprog.go") { ok = true break } @@ -154,6 +154,6 @@ func BenchmarkLineParser(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = Parse(data) + _ = ParseAll(data) } } diff --git a/pkg/dwarf/line/state_machine.go b/pkg/dwarf/line/state_machine.go index feedd4ed..6ca35408 100644 --- a/pkg/dwarf/line/state_machine.go +++ b/pkg/dwarf/line/state_machine.go @@ -47,6 +47,11 @@ const ( DW_LNS_set_basic_block = 7 DW_LNS_const_add_pc = 8 DW_LNS_fixed_advance_pc = 9 + + // DWARF v4 + DW_LNS_set_prologue_end = 10 + DW_LNS_set_epilouge_begin = 11 + DW_LNS_set_isa = 12 ) // Extended opcodes @@ -66,6 +71,11 @@ var standardopcodes = map[byte]opcodefn{ DW_LNS_set_basic_block: setbasicblock, DW_LNS_const_add_pc: constaddpc, DW_LNS_fixed_advance_pc: fixedadvancepc, + + // DWARF v4 + DW_LNS_set_prologue_end: donothing0, + DW_LNS_set_epilouge_begin: donothing0, + DW_LNS_set_isa: donothing1, } var extendedopcodes = map[byte]opcodefn{ @@ -75,16 +85,19 @@ var extendedopcodes = map[byte]opcodefn{ } func newStateMachine(dbl *DebugLineInfo) *StateMachine { - return &StateMachine{dbl: dbl, file: dbl.FileNames[0].Name, line: 1} + return &StateMachine{dbl: dbl, file: dbl.FileNames[0].Path, line: 1} } // Returns all PCs for a given file/line. Useful for loops where the 'for' line // could be split amongst 2 PCs. -func (dbl *DebugLines) AllPCsForFileLine(f string, l int) (pcs []uint64) { +func (lineInfo *DebugLineInfo) AllPCsForFileLine(f string, l int) (pcs []uint64) { + if lineInfo == nil { + return nil + } + var ( foundFile bool lastAddr uint64 - lineInfo = dbl.GetLineInfo(f) sm = newStateMachine(lineInfo) buf = bytes.NewBuffer(lineInfo.Instructions) ) @@ -116,11 +129,11 @@ func (dbl *DebugLines) AllPCsForFileLine(f string, l int) (pcs []uint64) { var NoSourceError = errors.New("no source available") -func (dbl *DebugLines) AllPCsBetween(begin, end uint64, filename string) ([]uint64, error) { - lineInfo := dbl.GetLineInfo(filename) +func (lineInfo *DebugLineInfo) AllPCsBetween(begin, end uint64) ([]uint64, error) { if lineInfo == nil { - return nil, NoSourceError + return nil, nil } + var ( pcs []uint64 lastaddr uint64 @@ -144,6 +157,63 @@ func (dbl *DebugLines) AllPCsBetween(begin, end uint64, filename string) ([]uint return pcs, nil } +// 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. +func (lineInfo *DebugLineInfo) PCToLine(pc uint64) (string, int) { + if lineInfo == nil { + return "", 0 + } + + var ( + buf = bytes.NewBuffer(lineInfo.Instructions) + sm = newStateMachine(lineInfo) + lastFilename string + lastLineno int + ) + for b, err := buf.ReadByte(); err == nil; b, err = buf.ReadByte() { + findAndExecOpcode(sm, buf, b) + if !sm.valid { + continue + } + if sm.address > pc { + return lastFilename, lastLineno + } + if sm.address == pc { + return sm.file, sm.line + } + lastFilename, lastLineno = sm.file, sm.line + } + return "", 0 +} + +// LineToPC returns the first PC address associated with filename:lineno. +func (lineInfo *DebugLineInfo) LineToPC(filename string, lineno int) uint64 { + if lineInfo == nil { + return 0 + } + + var ( + foundFile bool + sm = newStateMachine(lineInfo) + buf = bytes.NewBuffer(lineInfo.Instructions) + ) + + for b, err := buf.ReadByte(); err == nil; b, err = buf.ReadByte() { + findAndExecOpcode(sm, buf, b) + if foundFile && sm.file != filename { + break + } + if sm.line == lineno && sm.file == filename { + foundFile = true + if sm.valid { + return sm.address + } + } + } + return 0 +} + func findAndExecOpcode(sm *StateMachine, buf *bytes.Buffer, b byte) { switch { case b == 0: @@ -215,7 +285,7 @@ func advanceline(sm *StateMachine, buf *bytes.Buffer) { func setfile(sm *StateMachine, buf *bytes.Buffer) { i, _ := util.DecodeULEB128(buf) - sm.file = sm.dbl.FileNames[i-1].Name + sm.file = sm.dbl.FileNames[i-1].Path } func setcolumn(sm *StateMachine, buf *bytes.Buffer) { @@ -242,6 +312,15 @@ func fixedadvancepc(sm *StateMachine, buf *bytes.Buffer) { sm.address += uint64(operand) } +func donothing0(sm *StateMachine, buf *bytes.Buffer) { + // does nothing, no operands +} + +func donothing1(sm *StateMachine, buf *bytes.Buffer) { + // does nothing, consumes one operand + util.DecodeSLEB128(buf) +} + func endsequence(sm *StateMachine, buf *bytes.Buffer) { sm.endSeq = true sm.valid = true diff --git a/pkg/proc/bininfo.go b/pkg/proc/bininfo.go index 319a85a0..810405df 100644 --- a/pkg/proc/bininfo.go +++ b/pkg/proc/bininfo.go @@ -3,13 +3,14 @@ package proc import ( "debug/dwarf" "debug/elf" - "debug/gosym" "debug/macho" "debug/pe" "errors" "fmt" "io" "os" + "sort" + "strings" "sync" "time" @@ -31,13 +32,18 @@ type BinaryInfo struct { Arch Arch dwarf *dwarf.Data frameEntries frame.FrameDescriptionEntries - lineInfo line.DebugLines - goSymTable *gosym.Table + compileUnits []*compileUnit types map[string]dwarf.Offset packageVars map[string]dwarf.Offset - functions []functionDebugInfo gStructOffset uint64 + // Functions is a list of all DW_TAG_subprogram entries in debug_info. + Functions []Function + // Sources is a list of all source files found in debug_line. + Sources []string + // LookupFunc maps function names to a description of the function. + LookupFunc map[string]*Function + typeCache map[dwarf.Offset]godwarf.Type loadModuleDataOnce sync.Once @@ -52,6 +58,64 @@ var UnsupportedLinuxArchErr = errors.New("unsupported architecture - only linux/ var UnsupportedWindowsArchErr = errors.New("unsupported architecture of windows/386 - only windows/amd64 is supported") var UnsupportedDarwinArchErr = errors.New("unsupported architecture - only darwin/amd64 is supported") +const dwarfGoLanguage = 22 // DW_LANG_Go (from DWARF v5, section 7.12, page 231) + +type compileUnit struct { + entry *dwarf.Entry // debug_info entry describing this compile unit + isgo bool // true if this is the go compile unit + Name string // univocal name for non-go compile units + lineInfo *line.DebugLineInfo // debug_line segment associated with this compile unit + LowPC, HighPC uint64 +} + +// Function describes a function in the target program. +type Function struct { + Name string + Entry, End uint64 // same as DW_AT_lowpc and DW_AT_highpc + offset dwarf.Offset + cu *compileUnit +} + +// PackageName returns the package part of the symbol name, +// or the empty string if there is none. +// Borrowed from $GOROOT/debug/gosym/symtab.go +func (fn *Function) PackageName() string { + pathend := strings.LastIndex(fn.Name, "/") + if pathend < 0 { + pathend = 0 + } + + if i := strings.Index(fn.Name[pathend:], "."); i != -1 { + return fn.Name[:pathend+i] + } + return "" +} + +// ReceiverName returns the receiver type name of this symbol, +// or the empty string if there is none. +// Borrowed from $GOROOT/debug/gosym/symtab.go +func (fn *Function) ReceiverName() string { + pathend := strings.LastIndex(fn.Name, "/") + if pathend < 0 { + pathend = 0 + } + l := strings.Index(fn.Name[pathend:], ".") + r := strings.LastIndex(fn.Name[pathend:], ".") + if l == -1 || r == -1 || l == r { + return "" + } + return fn.Name[pathend+l+1 : pathend+r] +} + +// BaseName returns the symbol name without the package or receiver name. +// Borrowed from $GOROOT/debug/gosym/symtab.go +func (fn *Function) BaseName() string { + if i := strings.LastIndex(fn.Name, "."); i != -1 { + return fn.Name[i+1:] + } + return fn.Name +} + func NewBinaryInfo(goos, goarch string) BinaryInfo { r := BinaryInfo{GOOS: goos, nameOfRuntimeType: make(map[uintptr]nameOfRuntimeTypeEntry), typeCache: make(map[dwarf.Offset]godwarf.Type)} @@ -96,16 +160,6 @@ func (bi *BinaryInfo) DwarfReader() *reader.Reader { return reader.New(bi.dwarf) } -// Sources returns list of source files that comprise the debugged binary. -func (bi *BinaryInfo) Sources() map[string]*gosym.Obj { - return bi.goSymTable.Files -} - -// Funcs returns list of functions present in the debugged program. -func (bi *BinaryInfo) Funcs() []gosym.Func { - return bi.goSymTable.Funcs -} - // Types returns list of types present in the debugged program. func (bi *BinaryInfo) Types() ([]string, error) { types := make([]string, 0, len(bi.types)) @@ -116,18 +170,44 @@ func (bi *BinaryInfo) Types() ([]string, error) { } // PCToLine converts an instruction address to a file/line/function. -func (bi *BinaryInfo) PCToLine(pc uint64) (string, int, *gosym.Func) { - return bi.goSymTable.PCToLine(pc) +func (bi *BinaryInfo) PCToLine(pc uint64) (string, int, *Function) { + fn := bi.PCToFunc(pc) + if fn == nil { + return "", 0, nil + } + f, ln := fn.cu.lineInfo.PCToLine(pc) + return f, ln, fn } // LineToPC converts a file:line into a memory address. -func (bi *BinaryInfo) LineToPC(filename string, lineno int) (pc uint64, fn *gosym.Func, err error) { - return bi.goSymTable.LineToPC(filename, lineno) +func (bi *BinaryInfo) LineToPC(filename string, lineno int) (pc uint64, fn *Function, err error) { + for _, cu := range bi.compileUnits { + if cu.lineInfo.Lookup[filename] != nil { + pc = cu.lineInfo.LineToPC(filename, lineno) + fn = bi.PCToFunc(pc) + if fn == nil { + err = fmt.Errorf("no code at %s:%d", filename, lineno) + } + return + } + } + err = fmt.Errorf("could not find %s:%d", filename, lineno) + return } // PCToFunc returns the function containing the given PC address -func (bi *BinaryInfo) PCToFunc(pc uint64) *gosym.Func { - return bi.goSymTable.PCToFunc(pc) +func (bi *BinaryInfo) PCToFunc(pc uint64) *Function { + i := sort.Search(len(bi.Functions), func(i int) bool { + fn := bi.Functions[i] + return pc <= fn.Entry || (fn.Entry <= pc && pc < fn.End) + }) + if i != len(bi.Functions) { + fn := &bi.Functions[i] + if fn.Entry <= pc && pc < fn.End { + return fn + } + } + return nil } func (bi *BinaryInfo) Close() error { @@ -164,11 +244,14 @@ func (bi *BinaryInfo) LoadBinaryInfoElf(path string, wg *sync.WaitGroup) error { return err } - wg.Add(5) + debugLineBytes, err := getDebugLineInfoElf(elfFile) + if err != nil { + return err + } + + wg.Add(3) go bi.parseDebugFrameElf(elfFile, wg) - go bi.obtainGoSymbolsElf(elfFile, wg) - go bi.parseDebugLineInfoElf(elfFile, wg) - go bi.loadDebugInfoMaps(wg) + go bi.loadDebugInfoMaps(debugLineBytes, wg) go bi.setGStructOffsetElf(elfFile, wg) return nil } @@ -197,55 +280,15 @@ func (bi *BinaryInfo) parseDebugFrameElf(exe *elf.File, wg *sync.WaitGroup) { } } -func (bi *BinaryInfo) obtainGoSymbolsElf(exe *elf.File, wg *sync.WaitGroup) { - defer wg.Done() - - var ( - symdat []byte - pclndat []byte - err error - ) - - if sec := exe.Section(".gosymtab"); sec != nil { - symdat, err = sec.Data() - if err != nil { - bi.setLoadError("could not get .gosymtab section: %v", err) - return - } - } - - if sec := exe.Section(".gopclntab"); sec != nil { - pclndat, err = sec.Data() - if err != nil { - bi.setLoadError("could not get .gopclntab section: %v", err) - return - } - } - - pcln := gosym.NewLineTable(pclndat, exe.Section(".text").Addr) - tab, err := gosym.NewTable(symdat, pcln) - if err != nil { - bi.setLoadError("could not get initialize line table: %v", err) - return - } - - bi.goSymTable = tab -} - -func (bi *BinaryInfo) parseDebugLineInfoElf(exe *elf.File, wg *sync.WaitGroup) { - defer wg.Done() - +func getDebugLineInfoElf(exe *elf.File) ([]byte, error) { if sec := exe.Section(".debug_line"); sec != nil { debugLine, err := exe.Section(".debug_line").Data() if err != nil { - bi.setLoadError("could not get .debug_line section: %v", err) - return + return nil, fmt.Errorf("could not get .debug_line section: %v", err) } - bi.lineInfo = line.Parse(debugLine) - } else { - bi.setLoadError("could not find .debug_line section in binary") - return + return debugLine, nil } + return nil, errors.New("could not find .debug_line section in binary") } func (bi *BinaryInfo) setGStructOffsetElf(exe *elf.File, wg *sync.WaitGroup) { @@ -302,11 +345,14 @@ func (bi *BinaryInfo) LoadBinaryInfoPE(path string, wg *sync.WaitGroup) error { return err } - wg.Add(4) + debugLineBytes, err := getDebugLineInfoPE(peFile) + if err != nil { + return err + } + + wg.Add(2) go bi.parseDebugFramePE(peFile, wg) - go bi.obtainGoSymbolsPE(peFile, wg) - go bi.parseDebugLineInfoPE(peFile, wg) - go bi.loadDebugInfoMaps(wg) + go bi.loadDebugInfoMaps(debugLineBytes, wg) // Use ArbitraryUserPointer (0x28) as pointer to pointer // to G struct per: @@ -356,25 +402,6 @@ func (bi *BinaryInfo) parseDebugFramePE(exe *pe.File, wg *sync.WaitGroup) { } } -func (bi *BinaryInfo) obtainGoSymbolsPE(exe *pe.File, wg *sync.WaitGroup) { - defer wg.Done() - - _, symdat, pclndat, err := pclnPE(exe) - if err != nil { - bi.setLoadError("could not get Go symbols: %v", err) - return - } - - pcln := gosym.NewLineTable(pclndat, uint64(exe.Section(".text").Offset)) - tab, err := gosym.NewTable(symdat, pcln) - if err != nil { - bi.setLoadError("could not get initialize line table: %v", err) - return - } - - bi.goSymTable = tab -} - // Borrowed from https://golang.org/src/cmd/internal/objfile/pe.go func findPESymbol(f *pe.File, name string) (*pe.Symbol, error) { for _, s := range f.Symbols { @@ -445,23 +472,18 @@ func pclnPE(exe *pe.File) (textStart uint64, symtab, pclntab []byte, err error) return textStart, symtab, pclntab, nil } -func (bi *BinaryInfo) parseDebugLineInfoPE(exe *pe.File, wg *sync.WaitGroup) { - defer wg.Done() - +func getDebugLineInfoPE(exe *pe.File) ([]byte, error) { if sec := exe.Section(".debug_line"); sec != nil { debugLine, err := sec.Data() if err != nil && uint32(len(debugLine)) < sec.Size { - bi.setLoadError("could not get .debug_line section: %v", err) - return + return nil, fmt.Errorf("could not get .debug_line section: %v", err) } if 0 < sec.VirtualSize && sec.VirtualSize < sec.Size { debugLine = debugLine[:sec.VirtualSize] } - bi.lineInfo = line.Parse(debugLine) - } else { - bi.setLoadError("could not find .debug_line section in binary") - return + return debugLine, nil } + return nil, errors.New("could not find .debug_line section in binary") } // MACH-O //////////////////////////////////////////////////////////// @@ -480,11 +502,14 @@ func (bi *BinaryInfo) LoadBinaryInfoMacho(path string, wg *sync.WaitGroup) error return err } - wg.Add(4) + debugLineBytes, err := getDebugLineInfoMacho(exe) + if err != nil { + return err + } + + wg.Add(2) go bi.parseDebugFrameMacho(exe, wg) - go bi.obtainGoSymbolsMacho(exe, wg) - go bi.parseDebugLineInfoMacho(exe, wg) - go bi.loadDebugInfoMaps(wg) + go bi.loadDebugInfoMaps(debugLineBytes, wg) bi.gStructOffset = 0x8a0 return nil } @@ -513,53 +538,13 @@ func (bi *BinaryInfo) parseDebugFrameMacho(exe *macho.File, wg *sync.WaitGroup) } } -func (bi *BinaryInfo) obtainGoSymbolsMacho(exe *macho.File, wg *sync.WaitGroup) { - defer wg.Done() - - var ( - symdat []byte - pclndat []byte - err error - ) - - if sec := exe.Section("__gosymtab"); sec != nil { - symdat, err = sec.Data() - if err != nil { - bi.setLoadError("could not get .gosymtab section: %v", err) - return - } - } - - if sec := exe.Section("__gopclntab"); sec != nil { - pclndat, err = sec.Data() - if err != nil { - bi.setLoadError("could not get .gopclntab section: %v", err) - return - } - } - - pcln := gosym.NewLineTable(pclndat, exe.Section("__text").Addr) - tab, err := gosym.NewTable(symdat, pcln) - if err != nil { - bi.setLoadError("could not get initialize line table: %v", err) - return - } - - bi.goSymTable = tab -} - -func (bi *BinaryInfo) parseDebugLineInfoMacho(exe *macho.File, wg *sync.WaitGroup) { - defer wg.Done() - +func getDebugLineInfoMacho(exe *macho.File) ([]byte, error) { if sec := exe.Section("__debug_line"); sec != nil { debugLine, err := exe.Section("__debug_line").Data() if err != nil { - bi.setLoadError("could not get __debug_line section: %v", err) - return + return nil, fmt.Errorf("could not get __debug_line section: %v", err) } - bi.lineInfo = line.Parse(debugLine) - } else { - bi.setLoadError("could not find __debug_line section in binary") - return + return debugLine, nil } + return nil, errors.New("could not find __debug_line section in binary") } diff --git a/pkg/proc/disasm_amd64.go b/pkg/proc/disasm_amd64.go index cafe33a9..943c3a7d 100644 --- a/pkg/proc/disasm_amd64.go +++ b/pkg/proc/disasm_amd64.go @@ -1,7 +1,6 @@ package proc import ( - "debug/gosym" "encoding/binary" "golang.org/x/arch/x86/x86asm" @@ -141,7 +140,7 @@ func init() { // FirstPCAfterPrologue returns the address of the first instruction after the prologue for function fn // If sameline is set FirstPCAfterPrologue will always return an address associated with the same line as fn.Entry -func FirstPCAfterPrologue(p Process, fn *gosym.Func, sameline bool) (uint64, error) { +func FirstPCAfterPrologue(p Process, fn *Function, sameline bool) (uint64, error) { var mem MemoryReadWriter = p.CurrentThread() breakpoints := p.Breakpoints() bi := p.BinInfo() diff --git a/pkg/proc/proc.go b/pkg/proc/proc.go index b1897635..e840d569 100644 --- a/pkg/proc/proc.go +++ b/pkg/proc/proc.go @@ -1,7 +1,6 @@ package proc import ( - "debug/dwarf" "encoding/binary" "errors" "fmt" @@ -11,11 +10,6 @@ import ( "strconv" ) -type functionDebugInfo struct { - lowpc, highpc uint64 - offset dwarf.Offset -} - var NotExecutableErr = errors.New("not an executable file") var NotRecordedErr = errors.New("not a recording") @@ -35,7 +29,7 @@ func (pe ProcessExitedError) Error() string { // FindFileLocation returns the PC for a given file:line. // Assumes that `file` is normailzed to lower case and '/' on Windows. func FindFileLocation(p Process, fileName string, lineno int) (uint64, error) { - pc, fn, err := p.BinInfo().goSymTable.LineToPC(fileName, lineno) + pc, fn, err := p.BinInfo().LineToPC(fileName, lineno) if err != nil { return 0, err } @@ -53,7 +47,7 @@ func FindFileLocation(p Process, fileName string, lineno int) (uint64, error) { // https://github.com/derekparker/delve/issues/170 func FindFunctionLocation(p Process, funcName string, firstLine bool, lineOffset int) (uint64, error) { bi := p.BinInfo() - origfn := bi.goSymTable.LookupFunc(funcName) + origfn := bi.LookupFunc[funcName] if origfn == nil { return 0, fmt.Errorf("Could not find function %s\n", funcName) } @@ -61,8 +55,8 @@ func FindFunctionLocation(p Process, funcName string, firstLine bool, lineOffset if firstLine { return FirstPCAfterPrologue(p, origfn, false) } else if lineOffset > 0 { - filename, lineno, _ := bi.goSymTable.PCToLine(origfn.Entry) - breakAddr, _, err := bi.goSymTable.LineToPC(filename, lineno+lineOffset) + filename, lineno := origfn.cu.lineInfo.PCToLine(origfn.Entry) + breakAddr, _, err := bi.LineToPC(filename, lineno+lineOffset) return breakAddr, err } diff --git a/pkg/proc/stack.go b/pkg/proc/stack.go index f527b913..544cb11d 100644 --- a/pkg/proc/stack.go +++ b/pkg/proc/stack.go @@ -32,8 +32,6 @@ type Stackframe struct { CFA int64 // High address of the stack. StackHi uint64 - // Description of the stack frame. - FDE *frame.FrameDescriptionEntry // Return address for this stack frame (as read from the stack frame itself). Ret uint64 // Address to the memory location containing the return address @@ -108,11 +106,11 @@ type savedLR struct { } func newStackIterator(bi *BinaryInfo, mem MemoryReadWriter, pc, sp, bp, stackhi uint64, stkbar []savedLR, stkbarPos int) *stackIterator { - stackBarrierFunc := bi.goSymTable.LookupFunc(runtimeStackBarrier) // stack barriers were removed in Go 1.9 + stackBarrierFunc := bi.LookupFunc[runtimeStackBarrier] // stack barriers were removed in Go 1.9 var stackBarrierPC uint64 if stackBarrierFunc != nil && stkbar != nil { stackBarrierPC = stackBarrierFunc.Entry - fn := bi.goSymTable.PCToFunc(pc) + fn := bi.PCToFunc(pc) if fn != nil && fn.Name == runtimeStackBarrier { // We caught the goroutine as it's executing the stack barrier, we must // determine whether or not g.stackPos has already been incremented or not. @@ -211,7 +209,7 @@ func (it *stackIterator) newStackframe(pc uint64, cfa int64, retaddr uintptr, fd if err != nil { it.err = err } - r := Stackframe{Current: Location{PC: pc, File: f, Line: l, Fn: fn}, CFA: cfa, FDE: fde, Ret: ret, addrret: uint64(retaddr), StackHi: it.stackhi} + r := Stackframe{Current: Location{PC: pc, File: f, Line: l, Fn: fn}, CFA: cfa, Ret: ret, addrret: uint64(retaddr), StackHi: it.stackhi} if !top { r.Call.File, r.Call.Line, r.Call.Fn = it.bi.PCToLine(pc - 1) r.Call.PC = r.Current.PC diff --git a/pkg/proc/threads.go b/pkg/proc/threads.go index 1545b60f..3e56d97a 100644 --- a/pkg/proc/threads.go +++ b/pkg/proc/threads.go @@ -1,9 +1,9 @@ package proc import ( - "debug/gosym" "encoding/binary" "errors" + "fmt" "go/ast" "go/token" "path/filepath" @@ -42,7 +42,7 @@ type Location struct { PC uint64 File string Line int - Fn *gosym.Func + Fn *Function } // ThreadBlockedError is returned when the thread @@ -104,6 +104,10 @@ func next(dbp Process, stepInto bool) error { return err } + if topframe.Current.Fn == nil { + return fmt.Errorf("no source for pc %#x", topframe.Current.PC) + } + success := false defer func() { if !success { @@ -123,7 +127,7 @@ func next(dbp Process, stepInto bool) error { } } - text, err := disassemble(thread, regs, dbp.Breakpoints(), dbp.BinInfo(), topframe.FDE.Begin(), topframe.FDE.End()) + text, err := disassemble(thread, regs, dbp.Breakpoints(), dbp.BinInfo(), topframe.Current.Fn.Entry, topframe.Current.Fn.End) if err != nil && stepInto { return err } @@ -203,7 +207,7 @@ func next(dbp Process, stepInto bool) error { } // Add breakpoints on all the lines in the current function - pcs, err := dbp.BinInfo().lineInfo.AllPCsBetween(topframe.FDE.Begin(), topframe.FDE.End()-1, topframe.Current.File) + pcs, err := topframe.Current.Fn.cu.lineInfo.AllPCsBetween(topframe.Current.Fn.Entry, topframe.Current.Fn.End-1) if err != nil { return err } @@ -211,14 +215,14 @@ func next(dbp Process, stepInto bool) error { if !csource { var covered bool for i := range pcs { - if topframe.FDE.Cover(pcs[i]) { + if topframe.Current.Fn.Entry <= pcs[i] && pcs[i] < topframe.Current.Fn.End { covered = true break } } if !covered { - fn := dbp.BinInfo().goSymTable.PCToFunc(topframe.Ret) + fn := dbp.BinInfo().PCToFunc(topframe.Ret) if selg != nil && fn != nil && fn.Name == "runtime.goexit" { return nil } diff --git a/pkg/proc/types.go b/pkg/proc/types.go index c1b0bf61..4c824736 100644 --- a/pkg/proc/types.go +++ b/pkg/proc/types.go @@ -8,6 +8,7 @@ import ( "go/ast" "go/constant" "go/token" + "path/filepath" "reflect" "sort" "strconv" @@ -16,6 +17,7 @@ import ( "unsafe" "github.com/derekparker/delve/pkg/dwarf/godwarf" + "github.com/derekparker/delve/pkg/dwarf/line" "github.com/derekparker/delve/pkg/dwarf/reader" ) @@ -155,62 +157,128 @@ func (bi *BinaryInfo) loadPackageMap() error { return nil } -type sortFunctionsDebugInfoByLowpc []functionDebugInfo +type functionsDebugInfoByEntry []Function -func (v sortFunctionsDebugInfoByLowpc) Len() int { return len(v) } -func (v sortFunctionsDebugInfoByLowpc) Less(i, j int) bool { return v[i].lowpc < v[j].lowpc } -func (v sortFunctionsDebugInfoByLowpc) Swap(i, j int) { - temp := v[i] - v[i] = v[j] - v[j] = temp -} +func (v functionsDebugInfoByEntry) Len() int { return len(v) } +func (v functionsDebugInfoByEntry) Less(i, j int) bool { return v[i].Entry < v[j].Entry } +func (v functionsDebugInfoByEntry) Swap(i, j int) { v[i], v[j] = v[j], v[i] } -func (bi *BinaryInfo) loadDebugInfoMaps(wg *sync.WaitGroup) { +type compileUnitsByLowpc []*compileUnit + +func (v compileUnitsByLowpc) Len() int { return len(v) } +func (v compileUnitsByLowpc) Less(i int, j int) bool { return v[i].LowPC < v[j].LowPC } +func (v compileUnitsByLowpc) Swap(i int, j int) { v[i], v[j] = v[j], v[i] } + +func (bi *BinaryInfo) loadDebugInfoMaps(debugLineBytes []byte, wg *sync.WaitGroup) { defer wg.Done() bi.types = make(map[string]dwarf.Offset) bi.packageVars = make(map[string]dwarf.Offset) - bi.functions = []functionDebugInfo{} + bi.Functions = []Function{} + bi.compileUnits = []*compileUnit{} reader := bi.DwarfReader() + var cu *compileUnit = nil for entry, err := reader.Next(); entry != nil; entry, err = reader.Next() { if err != nil { break } switch entry.Tag { + case dwarf.TagCompileUnit: + cu = &compileUnit{} + cu.entry = entry + if lang, _ := entry.Val(dwarf.AttrLanguage).(int64); lang == dwarfGoLanguage { + cu.isgo = true + } + cu.Name, _ = entry.Val(dwarf.AttrName).(string) + compdir, _ := entry.Val(dwarf.AttrCompDir).(string) + if compdir != "" { + cu.Name = filepath.Join(compdir, cu.Name) + } + if ranges, _ := bi.dwarf.Ranges(entry); len(ranges) == 1 { + cu.LowPC = ranges[0][0] + cu.HighPC = ranges[0][1] + } + lineInfoOffset, _ := entry.Val(dwarf.AttrStmtList).(int64) + if lineInfoOffset >= 0 && lineInfoOffset < int64(len(debugLineBytes)) { + cu.lineInfo = line.Parse(compdir, bytes.NewBuffer(debugLineBytes[lineInfoOffset:])) + } + bi.compileUnits = append(bi.compileUnits, cu) + case dwarf.TagArrayType, dwarf.TagBaseType, dwarf.TagClassType, dwarf.TagStructType, dwarf.TagUnionType, dwarf.TagConstType, dwarf.TagVolatileType, dwarf.TagRestrictType, dwarf.TagEnumerationType, dwarf.TagPointerType, dwarf.TagSubroutineType, dwarf.TagTypedef, dwarf.TagUnspecifiedType: if name, ok := entry.Val(dwarf.AttrName).(string); ok { + if !cu.isgo { + name = "C." + name + } if _, exists := bi.types[name]; !exists { bi.types[name] = entry.Offset } } reader.SkipChildren() + case dwarf.TagVariable: if n, ok := entry.Val(dwarf.AttrName).(string); ok { + if !cu.isgo { + n = "C." + n + } bi.packageVars[n] = entry.Offset } + case dwarf.TagSubprogram: - lowpc, ok1 := entry.Val(dwarf.AttrLowpc).(uint64) - highpc, ok2 := entry.Val(dwarf.AttrHighpc).(uint64) + ok1 := false + var lowpc, highpc uint64 + if ranges, _ := bi.dwarf.Ranges(entry); len(ranges) == 1 { + ok1 = true + lowpc = ranges[0][0] + highpc = ranges[0][1] + } + name, ok2 := entry.Val(dwarf.AttrName).(string) if ok1 && ok2 { - bi.functions = append(bi.functions, functionDebugInfo{lowpc, highpc, entry.Offset}) + if !cu.isgo { + name = "C." + name + } + bi.Functions = append(bi.Functions, Function{ + Name: name, + Entry: lowpc, End: highpc, + offset: entry.Offset, + cu: cu, + }) } reader.SkipChildren() + } } - sort.Sort(sortFunctionsDebugInfoByLowpc(bi.functions)) + sort.Sort(compileUnitsByLowpc(bi.compileUnits)) + sort.Sort(functionsDebugInfoByEntry(bi.Functions)) + + bi.LookupFunc = make(map[string]*Function) + for i := range bi.Functions { + bi.LookupFunc[bi.Functions[i].Name] = &bi.Functions[i] + } + + bi.Sources = []string{} + for _, cu := range bi.compileUnits { + if cu.lineInfo != nil { + for _, fileEntry := range cu.lineInfo.FileNames { + bi.Sources = append(bi.Sources, fileEntry.Path) + } + } + } + sort.Strings(bi.Sources) + bi.Sources = uniq(bi.Sources) } -func (bi *BinaryInfo) findFunctionDebugInfo(pc uint64) (dwarf.Offset, error) { - i := sort.Search(len(bi.functions), func(i int) bool { - fn := bi.functions[i] - return pc <= fn.lowpc || (fn.lowpc <= pc && pc < fn.highpc) - }) - if i != len(bi.functions) { - fn := bi.functions[i] - if fn.lowpc <= pc && pc < fn.highpc { - return fn.offset, nil - } +func uniq(s []string) []string { + if len(s) <= 0 { + return s } - return 0, errors.New("unable to find function context") + src, dst := 1, 1 + for src < len(s) { + if s[src] != s[dst-1] { + s[dst] = s[src] + dst++ + } + src++ + } + return s[:dst] } func (bi *BinaryInfo) expandPackagesInType(expr ast.Expr) { diff --git a/pkg/proc/variables.go b/pkg/proc/variables.go index 1a9bc7ba..ec479bc1 100644 --- a/pkg/proc/variables.go +++ b/pkg/proc/variables.go @@ -511,7 +511,7 @@ func (g *G) UserCurrent() Location { // Go returns the location of the 'go' statement // that spawned this goroutine. func (g *G) Go() Location { - f, l, fn := g.variable.bi.goSymTable.PCToLine(g.GoPC) + f, l, fn := g.variable.bi.PCToLine(g.GoPC) return Location{PC: g.GoPC, File: f, Line: l, Fn: fn} } @@ -1203,7 +1203,7 @@ func (v *Variable) readFunctionPtr() { } v.Base = uintptr(binary.LittleEndian.Uint64(val)) - fn := v.bi.goSymTable.PCToFunc(uint64(v.Base)) + fn := v.bi.PCToFunc(uint64(v.Base)) if fn == nil { v.Unreadable = fmt.Errorf("could not find function for %#v", v.Base) return @@ -1660,14 +1660,14 @@ func (v *variablesByDepth) Swap(i int, j int) { // Fetches all variables of a specific type in the current function scope func (scope *EvalScope) variablesByTag(tag dwarf.Tag, cfg *LoadConfig) ([]*Variable, error) { - off, err := scope.BinInfo.findFunctionDebugInfo(scope.PC) - if err != nil { - return nil, err + fn := scope.BinInfo.PCToFunc(scope.PC) + if fn == nil { + return nil, errors.New("unable to find function context") } var vars []*Variable var depths []int - varReader := reader.Variables(scope.BinInfo.dwarf, off, scope.PC, tag == dwarf.TagVariable) + varReader := reader.Variables(scope.BinInfo.dwarf, fn.offset, scope.PC, tag == dwarf.TagVariable) hasScopes := false for varReader.Next() { entry := varReader.Entry() diff --git a/service/api/conversions.go b/service/api/conversions.go index b5c215fc..3d7a7bbf 100644 --- a/service/api/conversions.go +++ b/service/api/conversions.go @@ -2,7 +2,6 @@ package api import ( "bytes" - "debug/gosym" "go/constant" "go/printer" "go/token" @@ -187,16 +186,20 @@ func ConvertVar(v *proc.Variable) *Variable { // ConvertFunction converts from gosym.Func to // api.Function. -func ConvertFunction(fn *gosym.Func) *Function { +func ConvertFunction(fn *proc.Function) *Function { if fn == nil { return nil } + // fn here used to be a *gosym.Func, the fields Type and GoType below + // corresponded to the omonymous field of gosym.Func. Since the contents of + // those fields is not documented their value was replaced with 0 when + // gosym.Func was replaced by debug_info entries. return &Function{ Name: fn.Name, - Type: fn.Type, - Value: fn.Value, - GoType: fn.GoType, + Type: 0, + Value: fn.Entry, + GoType: 0, } } diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go index c2c30206..f788c0e3 100644 --- a/service/debugger/debugger.go +++ b/service/debugger/debugger.go @@ -1,7 +1,6 @@ package debugger import ( - "debug/gosym" "errors" "fmt" "go/parser" @@ -305,7 +304,7 @@ func (d *Debugger) CreateBreakpoint(requestedBp *api.Breakpoint) (*api.Breakpoin if runtime.GOOS == "windows" { // Accept fileName which is case-insensitive and slash-insensitive match fileNameNormalized := strings.ToLower(filepath.ToSlash(fileName)) - for symFile := range d.target.BinInfo().Sources() { + for _, symFile := range d.target.BinInfo().Sources { if fileNameNormalized == strings.ToLower(filepath.ToSlash(symFile)) { fileName = symFile break @@ -637,7 +636,7 @@ func (d *Debugger) Sources(filter string) ([]string, error) { } files := []string{} - for f := range d.target.BinInfo().Sources() { + for _, f := range d.target.BinInfo().Sources { if regex.Match([]byte(f)) { files = append(files, f) } @@ -650,7 +649,7 @@ func (d *Debugger) Functions(filter string) ([]string, error) { d.processMutex.Lock() defer d.processMutex.Unlock() - return regexFilterFuncs(filter, d.target.BinInfo().Funcs()) + return regexFilterFuncs(filter, d.target.BinInfo().Functions) } func (d *Debugger) Types(filter string) ([]string, error) { @@ -677,7 +676,7 @@ func (d *Debugger) Types(filter string) ([]string, error) { return r, nil } -func regexFilterFuncs(filter string, allFuncs []gosym.Func) ([]string, error) { +func regexFilterFuncs(filter string, allFuncs []proc.Function) ([]string, error) { regex, err := regexp.Compile(filter) if err != nil { return nil, fmt.Errorf("invalid filter argument: %s", err.Error()) @@ -685,7 +684,7 @@ func regexFilterFuncs(filter string, allFuncs []gosym.Func) ([]string, error) { funcs := []string{} for _, f := range allFuncs { - if f.Sym != nil && regex.Match([]byte(f.Name)) { + if regex.Match([]byte(f.Name)) { funcs = append(funcs, f.Name) } } diff --git a/service/debugger/locations.go b/service/debugger/locations.go index db7da3de..5adcd272 100644 --- a/service/debugger/locations.go +++ b/service/debugger/locations.go @@ -1,7 +1,6 @@ package debugger import ( - "debug/gosym" "fmt" "go/constant" "path/filepath" @@ -216,7 +215,7 @@ func stripReceiverDecoration(in string) string { return in[2 : len(in)-1] } -func (spec *FuncLocationSpec) Match(sym *gosym.Sym) bool { +func (spec *FuncLocationSpec) Match(sym proc.Function) bool { if spec.BaseName != sym.BaseName() { return false } @@ -243,7 +242,7 @@ func (spec *FuncLocationSpec) Match(sym *gosym.Sym) bool { } func (loc *RegexLocationSpec) Find(d *Debugger, scope *proc.EvalScope, locStr string) ([]api.Location, error) { - funcs := d.target.BinInfo().Funcs() + funcs := d.target.BinInfo().Functions matches, err := regexFilterFuncs(loc.FuncRegex, funcs) if err != nil { return nil, err @@ -329,7 +328,7 @@ func (ale AmbiguousLocationError) Error() string { func (loc *NormalLocationSpec) Find(d *Debugger, scope *proc.EvalScope, locStr string) ([]api.Location, error) { limit := maxFindLocationCandidates var candidateFiles []string - for file := range d.target.BinInfo().Sources() { + for _, file := range d.target.BinInfo().Sources { if loc.FileMatch(file) { candidateFiles = append(candidateFiles, file) if len(candidateFiles) >= limit { @@ -342,11 +341,8 @@ func (loc *NormalLocationSpec) Find(d *Debugger, scope *proc.EvalScope, locStr s var candidateFuncs []string if loc.FuncBase != nil { - for _, f := range d.target.BinInfo().Funcs() { - if f.Sym == nil { - continue - } - if !loc.FuncBase.Match(f.Sym) { + for _, f := range d.target.BinInfo().Functions { + if !loc.FuncBase.Match(f) { continue } if loc.Base == f.Name {