proc,debugger: implement logical breakpoints (#1717)

Modifies FindFileLocation, FindFunctionLocation and LineToPC as well as
service/debugger to support inlining and introduces the concept of
logical breakpoints.

For inlined functions FindFileLocation, FindFunctionLocation and
LineToPC will now return one PC address for each inlining and one PC
for the concrete implementation of the function (if present).

A proc.Breakpoint will continue to represent a physical breakpoint, at
a single memory location.

Breakpoints returned by service/debugger, however, will represent
logical breakpoints and may be associated with multiple memory
locations and, therefore, multiple proc.Breakpoints.

The necessary logic is introduced in service/debugger so that a change
to a logical breakpoint will be mirrored to all its physical
breakpoints and physical breakpoints are aggregated into a single
logical breakpoint when returned.
This commit is contained in:
Alessandro Arzilli 2019-11-01 20:41:06 +01:00 committed by Derek Parker
parent 1c5e8abf34
commit 222deeec36
12 changed files with 465 additions and 217 deletions

@ -112,33 +112,6 @@ func newStateMachine(dbl *DebugLineInfo, instructions []byte) *StateMachine {
return sm return sm
} }
// Returns all PCs for a given file/line. Useful for loops where the 'for' line
// could be split amongst 2 PCs.
func (lineInfo *DebugLineInfo) AllPCsForFileLine(f string, l int) (pcs []uint64) {
if lineInfo == nil {
return nil
}
var (
lastAddr uint64
sm = newStateMachine(lineInfo, lineInfo.Instructions)
)
for {
if err := sm.next(); err != nil {
if lineInfo.Logf != nil {
lineInfo.Logf("AllPCsForFileLine error: %v", err)
}
break
}
if sm.line == l && sm.file == f && sm.address != lastAddr && sm.isStmt && sm.valid {
pcs = append(pcs, sm.address)
lastAddr = sm.address
}
}
return
}
// AllPCsForFileLines Adds all PCs for a given file and set (domain of map) of lines // AllPCsForFileLines Adds all PCs for a given file and set (domain of map) of lines
// to the map value corresponding to each line. // to the map value corresponding to each line.
func (lineInfo *DebugLineInfo) AllPCsForFileLines(f string, m map[int][]uint64) { func (lineInfo *DebugLineInfo) AllPCsForFileLines(f string, m map[int][]uint64) {
@ -237,6 +210,13 @@ func (lineInfo *DebugLineInfo) PCToLine(basePC, pc uint64) (string, int) {
panic(fmt.Errorf("basePC after pc %#x %#x", 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 var sm *StateMachine
if basePC == 0 { if basePC == 0 {
sm = newStateMachine(lineInfo, lineInfo.Instructions) sm = newStateMachine(lineInfo, lineInfo.Instructions)
@ -246,14 +226,12 @@ func (lineInfo *DebugLineInfo) PCToLine(basePC, pc uint64) (string, int) {
// machine stopped at the entry point of the function. // machine stopped at the entry point of the function.
// As a last resort start from the start of the debug_line section. // As a last resort start from the start of the debug_line section.
sm = lineInfo.lastMachineCache[basePC] sm = lineInfo.lastMachineCache[basePC]
if sm == nil || sm.lastAddress > pc { if sm == nil || sm.lastAddress >= pc {
sm = lineInfo.stateMachineForEntry(basePC) sm = lineInfo.stateMachineForEntry(basePC)
lineInfo.lastMachineCache[basePC] = sm lineInfo.lastMachineCache[basePC] = sm
} }
} }
return sm
file, line, _ := sm.PCToLine(pc)
return file, line
} }
func (sm *StateMachine) PCToLine(pc uint64) (string, int, bool) { func (sm *StateMachine) PCToLine(pc uint64) (string, int, bool) {
@ -320,6 +298,42 @@ func (lineInfo *DebugLineInfo) LineToPC(filename string, lineno int) uint64 {
return fallbackPC 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)
for {
if sm.valid && sm.started {
if sm.address >= endPC {
return 0
}
if sm.line == lineno && sm.file == filename && sm.address >= startPC && sm.isStmt {
return sm.address
}
}
if err := sm.next(); err != nil {
if lineInfo.Logf != nil && err != io.EOF {
lineInfo.Logf("LineToPC error: %v", err)
}
break
}
}
return 0
}
// PrologueEndPC returns the first PC address marked as prologue_end in the half open interval [start, end) // 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) { func (lineInfo *DebugLineInfo) PrologueEndPC(start, end uint64) (pc uint64, file string, line int, ok bool) {
if lineInfo == nil { if lineInfo == nil {
@ -345,6 +359,33 @@ func (lineInfo *DebugLineInfo) PrologueEndPC(start, end uint64) (pc uint64, file
} }
} }
// 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("StmtAfter error: %v", err)
}
return 0, "", 0, false
}
}
}
func (sm *StateMachine) next() error { func (sm *StateMachine) next() error {
sm.started = true sm.started = true
if sm.valid { if sm.valid {

@ -80,6 +80,11 @@ type BinaryInfo struct {
// consts[off] lists all the constants with the type defined at offset off. // consts[off] lists all the constants with the type defined at offset off.
consts constantsMap consts constantsMap
// inlinedCallLines maps a file:line pair, corresponding to the header line
// of a function to a list of PC addresses where an inlined call to that
// function starts.
inlinedCallLines map[fileLine][]uint64
logger *logrus.Entry logger *logrus.Entry
} }
@ -107,32 +112,32 @@ type compileUnit struct {
lowPC uint64 lowPC uint64
ranges [][2]uint64 ranges [][2]uint64
entry *dwarf.Entry // debug_info entry describing this compile unit entry *dwarf.Entry // debug_info entry describing this compile unit
isgo bool // true if this is the go compile unit isgo bool // true if this is the go compile unit
lineInfo *line.DebugLineInfo // debug_line segment associated with this compile unit lineInfo *line.DebugLineInfo // debug_line segment associated with this compile unit
concreteInlinedFns []inlinedFn // list of concrete inlined functions within this compile unit optimized bool // this compile unit is optimized
optimized bool // this compile unit is optimized producer string // producer attribute
producer string // producer attribute
offset dwarf.Offset // offset of the entry describing the compile unit offset dwarf.Offset // offset of the entry describing the compile unit
image *Image // parent image of this compilation unit. image *Image // parent image of this compilation unit.
} }
type fileLine struct {
file string
line int
}
// dwarfRef is a reference to a Debug Info Entry inside a shared object. // dwarfRef is a reference to a Debug Info Entry inside a shared object.
type dwarfRef struct { type dwarfRef struct {
imageIndex int imageIndex int
offset dwarf.Offset offset dwarf.Offset
} }
// inlinedFn represents a concrete inlined function, e.g. // InlinedCall represents a concrete inlined call to a function.
// an entry for the generated code of an inlined function. type InlinedCall struct {
type inlinedFn struct { cu *compileUnit
Name string // Name of the function that was inlined LowPC, HighPC uint64 // Address range of the generated inlined instructions
LowPC, HighPC uint64 // Address range of the generated inlined instructions
CallFile string // File of the call site of the inlined function
CallLine int64 // Line of the call site of the inlined function
Parent *Function // The function that contains this inlined function
} }
// Function describes a function in the target program. // Function describes a function in the target program.
@ -141,6 +146,9 @@ type Function struct {
Entry, End uint64 // same as DW_AT_lowpc and DW_AT_highpc Entry, End uint64 // same as DW_AT_lowpc and DW_AT_highpc
offset dwarf.Offset offset dwarf.Offset
cu *compileUnit cu *compileUnit
// InlinedCalls lists all inlined calls to this function
InlinedCalls []InlinedCall
} }
// PackageName returns the package part of the symbol name, // PackageName returns the package part of the symbol name,
@ -389,39 +397,64 @@ func (err *ErrCouldNotFindLine) Error() string {
return fmt.Sprintf("could not find file %s", err.filename) return fmt.Sprintf("could not find file %s", err.filename)
} }
// LineToPC converts a file:line into a memory address. // LineToPC converts a file:line into a list of matching memory addresses,
func (bi *BinaryInfo) LineToPC(filename string, lineno int) (pc uint64, fn *Function, err error) { // corresponding to the first instruction matching the specified file:line
// in the containing function and all its inlined calls.
func (bi *BinaryInfo) LineToPC(filename string, lineno int) (pcs []uint64, err error) {
fileFound := false fileFound := false
var pc uint64
for _, cu := range bi.compileUnits { for _, cu := range bi.compileUnits {
if cu.lineInfo.Lookup[filename] != nil { if cu.lineInfo.Lookup[filename] == nil {
fileFound = true continue
pc := cu.lineInfo.LineToPC(filename, lineno) }
if pc == 0 { fileFound = true
// Check to see if this file:line belongs to the call site pc = cu.lineInfo.LineToPC(filename, lineno)
// of an inlined function. if pc != 0 {
for _, ifn := range cu.concreteInlinedFns { break
if strings.Contains(ifn.CallFile, filename) && ifn.CallLine == int64(lineno) {
return ifn.LowPC, ifn.Parent, nil
}
}
}
if fn := bi.PCToFunc(pc); fn != nil {
return pc, fn, nil
}
} }
} }
return 0, nil, &ErrCouldNotFindLine{fileFound, filename, lineno}
if pc == 0 {
// Check if the line contained a call to a function that was inlined, in
// that case it's possible for the line itself to not appear in debug_line
// at all, but it will still be in debug_info as the call site for an
// inlined subroutine entry.
if pcs := bi.inlinedCallLines[fileLine{filename, lineno}]; len(pcs) != 0 {
return pcs, nil
}
return nil, &ErrCouldNotFindLine{fileFound, filename, lineno}
}
// The code above will find the first occurence of an instruction
// corresponding to filename:line. If the function corresponding to that
// instruction has been inlined we don't just want to return the first
// occurence (which could be either the concrete version of the function or
// one of the inlinings) but instead:
// - the first instruction corresponding to filename:line in the concrete
// version of the function
// - the first instruction corresponding to filename:line in each inlined
// instance of the function.
fn := bi.PCToInlineFunc(pc)
if fn == nil {
return []uint64{pc}, nil
}
pcs = make([]uint64, 0, len(fn.InlinedCalls)+1)
pcs = appendLineToPCIn(pcs, filename, lineno, fn.cu, fn, fn.Entry, fn.End)
for _, call := range fn.InlinedCalls {
pcs = appendLineToPCIn(pcs, filename, lineno, call.cu, bi.PCToFunc(call.LowPC), call.LowPC, call.HighPC)
}
return pcs, nil
} }
// AllPCsForFileLine returns all PC addresses for the given filename:lineno. func appendLineToPCIn(pcs []uint64, filename string, lineno int, cu *compileUnit, containingFn *Function, lowPC, highPC uint64) []uint64 {
func (bi *BinaryInfo) AllPCsForFileLine(filename string, lineno int) []uint64 { var entry uint64
r := make([]uint64, 0, 1) if containingFn != nil {
for _, cu := range bi.compileUnits { entry = containingFn.Entry
if cu.lineInfo.Lookup[filename] != nil {
r = append(r, cu.lineInfo.AllPCsForFileLine(filename, lineno)...)
}
} }
return r pc := cu.lineInfo.LineToPCIn(filename, lineno, entry, lowPC, highPC)
if pc != 0 {
return append(pcs, pc)
}
return pcs
} }
// AllPCsForFileLines returns a map providing all PC addresses for filename and each line in linenos // AllPCsForFileLines returns a map providing all PC addresses for filename and each line in linenos
@ -438,7 +471,8 @@ func (bi *BinaryInfo) AllPCsForFileLines(filename string, linenos []int) map[int
return r return r
} }
// PCToFunc returns the function containing the given PC address // PCToFunc returns the concrete function containing the given PC address.
// If the PC address belongs to an inlined call it will return the containing function.
func (bi *BinaryInfo) PCToFunc(pc uint64) *Function { func (bi *BinaryInfo) PCToFunc(pc uint64) *Function {
i := sort.Search(len(bi.Functions), func(i int) bool { i := sort.Search(len(bi.Functions), func(i int) bool {
fn := bi.Functions[i] fn := bi.Functions[i]
@ -453,6 +487,29 @@ func (bi *BinaryInfo) PCToFunc(pc uint64) *Function {
return nil return nil
} }
// PCToInlineFunc returns the function containing the given PC address.
// If the PC address belongs to an inlined call it will return the inlined function.
func (bi *BinaryInfo) PCToInlineFunc(pc uint64) *Function {
fn := bi.PCToFunc(pc)
irdr := reader.InlineStack(fn.cu.image.dwarf, fn.offset, reader.ToRelAddr(pc, fn.cu.image.StaticBase))
var inlineFnEntry *dwarf.Entry
for irdr.Next() {
inlineFnEntry = irdr.Entry()
}
if inlineFnEntry == nil {
return fn
}
e, _ := reader.LoadAbstractOrigin(inlineFnEntry, fn.cu.image.dwarfReader)
fnname, okname := e.Val(dwarf.AttrName).(string)
if !okname {
return fn
}
return bi.LookupFunc[fnname]
}
// PCToImage returns the image containing the given PC address. // PCToImage returns the image containing the given PC address.
func (bi *BinaryInfo) PCToImage(pc uint64) *Image { func (bi *BinaryInfo) PCToImage(pc uint64) *Image {
fn := bi.PCToFunc(pc) fn := bi.PCToFunc(pc)
@ -1281,6 +1338,10 @@ func (bi *BinaryInfo) loadDebugInfoMaps(image *Image, debugLineBytes []byte, wg
if bi.packageMap == nil { if bi.packageMap == nil {
bi.packageMap = make(map[string]string) bi.packageMap = make(map[string]string)
} }
if bi.inlinedCallLines == nil {
bi.inlinedCallLines = make(map[fileLine][]uint64)
}
image.runtimeTypeToDIE = make(map[uint64]runtimeTypeDIE) image.runtimeTypeToDIE = make(map[uint64]runtimeTypeDIE)
ctxt := newLoadDebugInfoMapsContext(bi, image) ctxt := newLoadDebugInfoMapsContext(bi, image)
@ -1440,7 +1501,6 @@ func (bi *BinaryInfo) loadDebugInfoMapsCompileUnit(ctxt *loadDebugInfoMapsContex
case dwarf.TagSubprogram: case dwarf.TagSubprogram:
inlined := false inlined := false
if inval, ok := entry.Val(dwarf.AttrInline).(int64); ok { if inval, ok := entry.Val(dwarf.AttrInline).(int64); ok {
inlined = inval == 1 inlined = inval == 1
} }
@ -1496,11 +1556,11 @@ func (bi *BinaryInfo) addAbstractSubprogram(entry *dwarf.Entry, ctxt *loadDebugI
} }
if entry.Children { if entry.Children {
bi.loadDebugInfoMapsInlinedCalls(ctxt, reader, cu, &fn) bi.loadDebugInfoMapsInlinedCalls(ctxt, reader, cu)
} }
bi.Functions = append(bi.Functions, fn) bi.Functions = append(bi.Functions, fn)
ctxt.abstractOriginNameTable[entry.Offset] = name ctxt.abstractOriginTable[entry.Offset] = len(bi.Functions) - 1
} }
// addConcreteInlinedSubprogram adds the concrete entry of a subprogram that was also inlined. // addConcreteInlinedSubprogram adds the concrete entry of a subprogram that was also inlined.
@ -1514,7 +1574,7 @@ func (bi *BinaryInfo) addConcreteInlinedSubprogram(entry *dwarf.Entry, originOff
return return
} }
name, ok := ctxt.abstractOriginNameTable[originOffset] originIdx, ok := ctxt.abstractOriginTable[originOffset]
if !ok { if !ok {
bi.logger.Errorf("Error reading debug_info: could not find abstract origin of concrete inlined subprogram at %#x (origin offset %#x)", entry.Offset, originOffset) bi.logger.Errorf("Error reading debug_info: could not find abstract origin of concrete inlined subprogram at %#x (origin offset %#x)", entry.Offset, originOffset)
if entry.Children { if entry.Children {
@ -1523,21 +1583,18 @@ func (bi *BinaryInfo) addConcreteInlinedSubprogram(entry *dwarf.Entry, originOff
return return
} }
fn := Function{ fn := &bi.Functions[originIdx]
Name: name, fn.offset = entry.Offset
Entry: lowpc, End: highpc, fn.Entry = lowpc
offset: entry.Offset, fn.End = highpc
cu: cu,
}
bi.Functions = append(bi.Functions, fn)
if entry.Children { if entry.Children {
bi.loadDebugInfoMapsInlinedCalls(ctxt, reader, cu, &fn) bi.loadDebugInfoMapsInlinedCalls(ctxt, reader, cu)
} }
} }
// addConcreteSubprogram adds a concrete subprogram (a normal subprogram // addConcreteSubprogram adds a concrete subprogram (a normal subprogram
// that doesn't have abstract or inlined entries). // that doesn't have abstract or inlined entries)
func (bi *BinaryInfo) addConcreteSubprogram(entry *dwarf.Entry, ctxt *loadDebugInfoMapsContext, reader *reader.Reader, cu *compileUnit) { func (bi *BinaryInfo) addConcreteSubprogram(entry *dwarf.Entry, ctxt *loadDebugInfoMapsContext, reader *reader.Reader, cu *compileUnit) {
lowpc, highpc, ok := subprogramEntryRange(entry, cu.image) lowpc, highpc, ok := subprogramEntryRange(entry, cu.image)
if !ok { if !ok {
@ -1567,7 +1624,7 @@ func (bi *BinaryInfo) addConcreteSubprogram(entry *dwarf.Entry, ctxt *loadDebugI
bi.Functions = append(bi.Functions, fn) bi.Functions = append(bi.Functions, fn)
if entry.Children { if entry.Children {
bi.loadDebugInfoMapsInlinedCalls(ctxt, reader, cu, &fn) bi.loadDebugInfoMapsInlinedCalls(ctxt, reader, cu)
} }
} }
@ -1592,7 +1649,7 @@ func subprogramEntryRange(entry *dwarf.Entry, image *Image) (lowpc, highpc uint6
return lowpc, highpc, ok return lowpc, highpc, ok
} }
func (bi *BinaryInfo) loadDebugInfoMapsInlinedCalls(ctxt *loadDebugInfoMapsContext, reader *reader.Reader, cu *compileUnit, parentFn *Function) { func (bi *BinaryInfo) loadDebugInfoMapsInlinedCalls(ctxt *loadDebugInfoMapsContext, reader *reader.Reader, cu *compileUnit) {
for { for {
entry, err := reader.Next() entry, err := reader.Next()
if err != nil { if err != nil {
@ -1610,12 +1667,13 @@ func (bi *BinaryInfo) loadDebugInfoMapsInlinedCalls(ctxt *loadDebugInfoMapsConte
continue continue
} }
name, ok := ctxt.abstractOriginNameTable[originOffset] originIdx, ok := ctxt.abstractOriginTable[originOffset]
if !ok { if !ok {
bi.logger.Errorf("Error reading debug_info: could not find abstract origin (%#x) of inlined call at %#x", originOffset, entry.Offset) bi.logger.Errorf("Error reading debug_info: could not find abstract origin (%#x) of inlined call at %#x", originOffset, entry.Offset)
reader.SkipChildren() reader.SkipChildren()
continue continue
} }
fn := &bi.Functions[originIdx]
lowpc, highpc, ok := subprogramEntryRange(entry, cu.image) lowpc, highpc, ok := subprogramEntryRange(entry, cu.image)
if !ok { if !ok {
@ -1643,14 +1701,14 @@ func (bi *BinaryInfo) loadDebugInfoMapsInlinedCalls(ctxt *loadDebugInfoMapsConte
} }
callfile := cu.lineInfo.FileNames[callfileidx-1].Path callfile := cu.lineInfo.FileNames[callfileidx-1].Path
cu.concreteInlinedFns = append(cu.concreteInlinedFns, inlinedFn{ fn.InlinedCalls = append(fn.InlinedCalls, InlinedCall{
Name: name, cu: cu,
LowPC: lowpc, LowPC: lowpc,
HighPC: highpc, HighPC: highpc,
CallFile: callfile,
CallLine: callline,
Parent: parentFn,
}) })
fl := fileLine{callfile, int(callline)}
bi.inlinedCallLines[fl] = append(bi.inlinedCallLines[fl], lowpc)
} }
reader.SkipChildren() reader.SkipChildren()
} }

@ -8,7 +8,7 @@ import (
"reflect" "reflect"
) )
// Breakpoint represents a breakpoint. Stores information on the break // Breakpoint represents a physical breakpoint. Stores information on the break
// point including the byte of data that originally was stored at that // point including the byte of data that originally was stored at that
// address. // address.
type Breakpoint struct { type Breakpoint struct {
@ -20,7 +20,7 @@ type Breakpoint struct {
Addr uint64 // Address breakpoint is set for. Addr uint64 // Address breakpoint is set for.
OriginalData []byte // If software breakpoint, the data we replace with breakpoint instruction. OriginalData []byte // If software breakpoint, the data we replace with breakpoint instruction.
Name string // User defined name of the breakpoint Name string // User defined name of the breakpoint
ID int // Monotonically increasing ID. LogicalID int // ID of the logical breakpoint that owns this physical breakpoint
// Kind describes whether this is an internal breakpoint (for next'ing or // Kind describes whether this is an internal breakpoint (for next'ing or
// stepping). // stepping).
@ -82,7 +82,7 @@ const (
) )
func (bp *Breakpoint) String() string { func (bp *Breakpoint) String() string {
return fmt.Sprintf("Breakpoint %d at %#v %s:%d (%d)", bp.ID, bp.Addr, bp.File, bp.Line, bp.TotalHitCount) return fmt.Sprintf("Breakpoint %d at %#v %s:%d (%d)", bp.LogicalID, bp.Addr, bp.File, bp.Line, bp.TotalHitCount)
} }
// BreakpointExistsError is returned when trying to set a breakpoint at // BreakpointExistsError is returned when trying to set a breakpoint at
@ -269,11 +269,11 @@ func (bpmap *BreakpointMap) Set(addr uint64, kind BreakpointKind, cond ast.Expr,
if kind != UserBreakpoint { if kind != UserBreakpoint {
bpmap.internalBreakpointIDCounter++ bpmap.internalBreakpointIDCounter++
newBreakpoint.ID = bpmap.internalBreakpointIDCounter newBreakpoint.LogicalID = bpmap.internalBreakpointIDCounter
newBreakpoint.internalCond = cond newBreakpoint.internalCond = cond
} else { } else {
bpmap.breakpointIDCounter++ bpmap.breakpointIDCounter++
newBreakpoint.ID = bpmap.breakpointIDCounter newBreakpoint.LogicalID = bpmap.breakpointIDCounter
newBreakpoint.Cond = cond newBreakpoint.Cond = cond
} }
@ -282,11 +282,11 @@ func (bpmap *BreakpointMap) Set(addr uint64, kind BreakpointKind, cond ast.Expr,
return newBreakpoint, nil return newBreakpoint, nil
} }
// SetWithID creates a breakpoint at addr, with the specified ID. // SetWithID creates a breakpoint at addr, with the specified logical ID.
func (bpmap *BreakpointMap) SetWithID(id int, addr uint64, writeBreakpoint WriteBreakpointFn) (*Breakpoint, error) { func (bpmap *BreakpointMap) SetWithID(id int, addr uint64, writeBreakpoint WriteBreakpointFn) (*Breakpoint, error) {
bp, err := bpmap.Set(addr, UserBreakpoint, nil, writeBreakpoint) bp, err := bpmap.Set(addr, UserBreakpoint, nil, writeBreakpoint)
if err == nil { if err == nil {
bp.ID = id bp.LogicalID = id
bpmap.breakpointIDCounter-- bpmap.breakpointIDCounter--
} }
return bp, err return bp, err

@ -52,10 +52,20 @@ func assertNoError(err error, t testing.TB, s string) {
} }
func setFunctionBreakpoint(p proc.Process, t *testing.T, fname string) *proc.Breakpoint { func setFunctionBreakpoint(p proc.Process, t *testing.T, fname string) *proc.Breakpoint {
addr, err := proc.FindFunctionLocation(p, fname, 0) _, f, l, _ := runtime.Caller(1)
assertNoError(err, t, fmt.Sprintf("FindFunctionLocation(%s)", fname)) f = filepath.Base(f)
bp, err := p.SetBreakpoint(addr, proc.UserBreakpoint, nil)
assertNoError(err, t, fmt.Sprintf("SetBreakpoint(%#x) function %s", addr, fname)) addrs, err := proc.FindFunctionLocation(p, fname, 0)
if err != nil {
t.Fatalf("%s:%d: FindFunctionLocation(%s): %v", f, l, fname, err)
}
if len(addrs) != 1 {
t.Fatalf("%s:%d: setFunctionBreakpoint(%s): too many results %v", f, l, fname, addrs)
}
bp, err := p.SetBreakpoint(addrs[0], proc.UserBreakpoint, nil)
if err != nil {
t.Fatalf("%s:%d: FindFunctionLocation(%s): %v", f, l, fname, err)
}
return bp return bp
} }
@ -109,18 +119,28 @@ func TestRestartDuringStop(t *testing.T) {
}) })
} }
func setFileBreakpoint(p proc.Process, t *testing.T, file string, line int) *proc.Breakpoint { func setFileBreakpoint(p proc.Process, t *testing.T, fixture protest.Fixture, lineno int) *proc.Breakpoint {
addr, _, err := p.BinInfo().LineToPC(file, line) _, f, l, _ := runtime.Caller(1)
assertNoError(err, t, "LineToPC") f = filepath.Base(f)
bp, err := p.SetBreakpoint(addr, proc.UserBreakpoint, nil)
assertNoError(err, t, fmt.Sprintf("SetBreakpoint(%#x) - %s:%d", addr, file, line)) addrs, err := proc.FindFileLocation(p, fixture.Source, lineno)
if err != nil {
t.Fatalf("%s:%d: FindFileLocation(%s, %d): %v", f, l, fixture.Source, lineno, err)
}
if len(addrs) != 1 {
t.Fatalf("%s:%d: setFileLineBreakpoint(%s, %d): too many results %v", f, l, fixture.Source, lineno, addrs)
}
bp, err := p.SetBreakpoint(addrs[0], proc.UserBreakpoint, nil)
if err != nil {
t.Fatalf("%s:%d: SetBreakpoint: %v", f, l, err)
}
return bp return bp
} }
func TestReverseBreakpointCounts(t *testing.T) { func TestReverseBreakpointCounts(t *testing.T) {
protest.AllowRecording(t) protest.AllowRecording(t)
withTestRecording("bpcountstest", t, func(p *gdbserial.Process, fixture protest.Fixture) { withTestRecording("bpcountstest", t, func(p *gdbserial.Process, fixture protest.Fixture) {
endbp := setFileBreakpoint(p, t, fixture.Source, 28) endbp := setFileBreakpoint(p, t, fixture, 28)
assertNoError(proc.Continue(p), t, "Continue()") assertNoError(proc.Continue(p), t, "Continue()")
loc, _ := p.CurrentThread().Location() loc, _ := p.CurrentThread().Location()
if loc.PC != endbp.Addr { if loc.PC != endbp.Addr {
@ -129,8 +149,8 @@ func TestReverseBreakpointCounts(t *testing.T) {
p.ClearBreakpoint(endbp.Addr) p.ClearBreakpoint(endbp.Addr)
assertNoError(p.Direction(proc.Backward), t, "Switching to backward direction") assertNoError(p.Direction(proc.Backward), t, "Switching to backward direction")
bp := setFileBreakpoint(p, t, fixture.Source, 12) bp := setFileBreakpoint(p, t, fixture, 12)
startbp := setFileBreakpoint(p, t, fixture.Source, 20) startbp := setFileBreakpoint(p, t, fixture, 20)
countLoop: countLoop:
for { for {

@ -77,15 +77,21 @@ func PostInitializationSetup(p Process, path string, debugInfoDirs []string, wri
// FindFileLocation returns the PC for a given file:line. // FindFileLocation returns the PC for a given file:line.
// Assumes that `file` is normalized to lower case and '/' on Windows. // Assumes that `file` is normalized to lower case and '/' on Windows.
func FindFileLocation(p Process, fileName string, lineno int) (uint64, error) { func FindFileLocation(p Process, fileName string, lineno int) ([]uint64, error) {
pc, fn, err := p.BinInfo().LineToPC(fileName, lineno) pcs, err := p.BinInfo().LineToPC(fileName, lineno)
if err != nil { if err != nil {
return 0, err return nil, err
} }
if fn.Entry == pc { var fn *Function
pc, _ = FirstPCAfterPrologue(p, fn, true) for i := range pcs {
if fn == nil || pcs[i] < fn.Entry || pcs[i] >= fn.End {
fn = p.BinInfo().PCToFunc(pcs[i])
}
if fn != nil && fn.Entry == pcs[i] {
pcs[i], _ = FirstPCAfterPrologue(p, fn, true)
}
} }
return pc, nil return pcs, nil
} }
// ErrFunctionNotFound is returned when failing to find the // ErrFunctionNotFound is returned when failing to find the
@ -100,19 +106,34 @@ func (err *ErrFunctionNotFound) Error() string {
// FindFunctionLocation finds address of a function's line // FindFunctionLocation finds address of a function's line
// If lineOffset is passed FindFunctionLocation will return the address of that line // If lineOffset is passed FindFunctionLocation will return the address of that line
func FindFunctionLocation(p Process, funcName string, lineOffset int) (uint64, error) { func FindFunctionLocation(p Process, funcName string, lineOffset int) ([]uint64, error) {
bi := p.BinInfo() bi := p.BinInfo()
origfn := bi.LookupFunc[funcName] origfn := bi.LookupFunc[funcName]
if origfn == nil { if origfn == nil {
return 0, &ErrFunctionNotFound{funcName} return nil, &ErrFunctionNotFound{funcName}
} }
if lineOffset <= 0 { if lineOffset <= 0 {
return FirstPCAfterPrologue(p, origfn, false) r := make([]uint64, 0, len(origfn.InlinedCalls)+1)
if origfn.Entry > 0 {
// add concrete implementation of the function
pc, err := FirstPCAfterPrologue(p, origfn, false)
if err != nil {
return nil, err
}
r = append(r, pc)
}
// add inlined calls to the function
for _, call := range origfn.InlinedCalls {
r = append(r, call.LowPC)
}
if len(r) == 0 {
return nil, &ErrFunctionNotFound{funcName}
}
return r, nil
} }
filename, lineno := origfn.cu.lineInfo.PCToLine(origfn.Entry, origfn.Entry) filename, lineno := origfn.cu.lineInfo.PCToLine(origfn.Entry, origfn.Entry)
breakAddr, _, err := bi.LineToPC(filename, lineno+lineOffset) return bi.LineToPC(filename, lineno+lineOffset)
return breakAddr, err
} }
// Next continues execution until the next source line. // Next continues execution until the next source line.
@ -704,12 +725,12 @@ func FrameToScope(bi *BinaryInfo, thread MemoryReadWriter, g *G, frames ...Stack
// createUnrecoveredPanicBreakpoint creates the unrecoverable-panic breakpoint. // createUnrecoveredPanicBreakpoint creates the unrecoverable-panic breakpoint.
// This function is meant to be called by implementations of the Process interface. // This function is meant to be called by implementations of the Process interface.
func createUnrecoveredPanicBreakpoint(p Process, writeBreakpoint WriteBreakpointFn) { func createUnrecoveredPanicBreakpoint(p Process, writeBreakpoint WriteBreakpointFn) {
panicpc, err := FindFunctionLocation(p, "runtime.startpanic", 0) panicpcs, err := FindFunctionLocation(p, "runtime.startpanic", 0)
if _, isFnNotFound := err.(*ErrFunctionNotFound); isFnNotFound { if _, isFnNotFound := err.(*ErrFunctionNotFound); isFnNotFound {
panicpc, err = FindFunctionLocation(p, "runtime.fatalpanic", 0) panicpcs, err = FindFunctionLocation(p, "runtime.fatalpanic", 0)
} }
if err == nil { if err == nil {
bp, err := p.Breakpoints().SetWithID(unrecoveredPanicID, panicpc, writeBreakpoint) bp, err := p.Breakpoints().SetWithID(unrecoveredPanicID, panicpcs[0], writeBreakpoint)
if err == nil { if err == nil {
bp.Name = UnrecoveredPanic bp.Name = UnrecoveredPanic
bp.Variables = []string{"runtime.curg._panic.arg"} bp.Variables = []string{"runtime.curg._panic.arg"}
@ -718,9 +739,9 @@ func createUnrecoveredPanicBreakpoint(p Process, writeBreakpoint WriteBreakpoint
} }
func createFatalThrowBreakpoint(p Process, writeBreakpoint WriteBreakpointFn) { func createFatalThrowBreakpoint(p Process, writeBreakpoint WriteBreakpointFn) {
fatalpc, err := FindFunctionLocation(p, "runtime.fatalthrow", 0) fatalpcs, err := FindFunctionLocation(p, "runtime.fatalthrow", 0)
if err == nil { if err == nil {
bp, err := p.Breakpoints().SetWithID(fatalThrowID, fatalpc, writeBreakpoint) bp, err := p.Breakpoints().SetWithID(fatalThrowID, fatalpcs[0], writeBreakpoint)
if err == nil { if err == nil {
bp.Name = FatalThrow bp.Name = FatalThrow
} }
@ -752,9 +773,8 @@ func FirstPCAfterPrologue(p Process, fn *Function, sameline bool) (uint64, error
// Look for the first instruction with the stmt flag set, so that setting a // Look for the first instruction with the stmt flag set, so that setting a
// breakpoint with file:line and with the function name always result on // breakpoint with file:line and with the function name always result on
// the same instruction being selected. // the same instruction being selected.
entryFile, entryLine := fn.cu.lineInfo.PCToLine(fn.Entry, fn.Entry) if pc2, _, _, ok := fn.cu.lineInfo.FirstStmtForLine(fn.Entry, fn.End); ok {
if pc, _, err := p.BinInfo().LineToPC(entryFile, entryLine); err == nil && pc >= fn.Entry && pc < fn.End { return pc2, nil
return pc, nil
} }
} }

@ -176,11 +176,14 @@ func setFunctionBreakpoint(p proc.Process, t testing.TB, fname string) *proc.Bre
_, f, l, _ := runtime.Caller(1) _, f, l, _ := runtime.Caller(1)
f = filepath.Base(f) f = filepath.Base(f)
addr, err := proc.FindFunctionLocation(p, fname, 0) addrs, err := proc.FindFunctionLocation(p, fname, 0)
if err != nil { if err != nil {
t.Fatalf("%s:%d: FindFunctionLocation(%s): %v", f, l, fname, err) t.Fatalf("%s:%d: FindFunctionLocation(%s): %v", f, l, fname, err)
} }
bp, err := p.SetBreakpoint(addr, proc.UserBreakpoint, nil) if len(addrs) != 1 {
t.Fatalf("%s:%d: setFunctionBreakpoint(%s): too many results %v", f, l, fname, addrs)
}
bp, err := p.SetBreakpoint(addrs[0], proc.UserBreakpoint, nil)
if err != nil { if err != nil {
t.Fatalf("%s:%d: FindFunctionLocation(%s): %v", f, l, fname, err) t.Fatalf("%s:%d: FindFunctionLocation(%s): %v", f, l, fname, err)
} }
@ -191,11 +194,14 @@ func setFileBreakpoint(p proc.Process, t *testing.T, path string, lineno int) *p
_, f, l, _ := runtime.Caller(1) _, f, l, _ := runtime.Caller(1)
f = filepath.Base(f) f = filepath.Base(f)
addr, err := proc.FindFileLocation(p, path, lineno) addrs, err := proc.FindFileLocation(p, path, lineno)
if err != nil { if err != nil {
t.Fatalf("%s:%d: FindFileLocation(%s, %d): %v", f, l, path, lineno, err) t.Fatalf("%s:%d: FindFileLocation(%s, %d): %v", f, l, path, lineno, err)
} }
bp, err := p.SetBreakpoint(addr, proc.UserBreakpoint, nil) if len(addrs) != 1 {
t.Fatalf("%s:%d: setFileLineBreakpoint(%s, %d): too many results %v", f, l, path, lineno, addrs)
}
bp, err := p.SetBreakpoint(addrs[0], proc.UserBreakpoint, nil)
if err != nil { if err != nil {
t.Fatalf("%s:%d: SetBreakpoint: %v", f, l, err) t.Fatalf("%s:%d: SetBreakpoint: %v", f, l, err)
} }
@ -203,23 +209,29 @@ func setFileBreakpoint(p proc.Process, t *testing.T, path string, lineno int) *p
} }
func findFunctionLocation(p proc.Process, t *testing.T, fnname string) uint64 { func findFunctionLocation(p proc.Process, t *testing.T, fnname string) uint64 {
addr, err := proc.FindFunctionLocation(p, fnname, 0) _, f, l, _ := runtime.Caller(1)
f = filepath.Base(f)
addrs, err := proc.FindFunctionLocation(p, fnname, 0)
if err != nil { if err != nil {
_, f, l, _ := runtime.Caller(1)
f = filepath.Base(f)
t.Fatalf("%s:%d: FindFunctionLocation(%s): %v", f, l, fnname, err) t.Fatalf("%s:%d: FindFunctionLocation(%s): %v", f, l, fnname, err)
} }
return addr if len(addrs) != 1 {
t.Fatalf("%s:%d: FindFunctionLocation(%s): too many results %v", f, l, fnname, addrs)
}
return addrs[0]
} }
func findFileLocation(p proc.Process, t *testing.T, file string, lineno int) uint64 { func findFileLocation(p proc.Process, t *testing.T, file string, lineno int) uint64 {
addr, err := proc.FindFileLocation(p, file, lineno) _, f, l, _ := runtime.Caller(1)
f = filepath.Base(f)
addrs, err := proc.FindFileLocation(p, file, lineno)
if err != nil { if err != nil {
_, f, l, _ := runtime.Caller(1)
f = filepath.Base(f)
t.Fatalf("%s:%d: FindFileLocation(%s, %d): %v", f, l, file, lineno, err) t.Fatalf("%s:%d: FindFileLocation(%s, %d): %v", f, l, file, lineno, err)
} }
return addr if len(addrs) != 1 {
t.Fatalf("%s:%d: FindFileLocation(%s, %d): too many results %v", f, l, file, lineno, addrs)
}
return addrs[0]
} }
func TestHalt(t *testing.T) { func TestHalt(t *testing.T) {
@ -359,7 +371,7 @@ type nextTest struct {
func countBreakpoints(p proc.Process) int { func countBreakpoints(p proc.Process) int {
bpcount := 0 bpcount := 0
for _, bp := range p.Breakpoints().M { for _, bp := range p.Breakpoints().M {
if bp.ID >= 0 { if bp.LogicalID >= 0 {
bpcount++ bpcount++
} }
} }
@ -1031,11 +1043,11 @@ func TestContinueMulti(t *testing.T) {
} }
assertNoError(err, t, "Continue()") assertNoError(err, t, "Continue()")
if bp := p.CurrentThread().Breakpoint(); bp.ID == bp1.ID { if bp := p.CurrentThread().Breakpoint(); bp.LogicalID == bp1.LogicalID {
mainCount++ mainCount++
} }
if bp := p.CurrentThread().Breakpoint(); bp.ID == bp2.ID { if bp := p.CurrentThread().Breakpoint(); bp.LogicalID == bp2.LogicalID {
sayhiCount++ sayhiCount++
} }
} }
@ -3626,7 +3638,8 @@ func TestInlinedStacktraceAndVariables(t *testing.T) {
} }
withTestProcessArgs("testinline", t, ".", []string{}, protest.EnableInlining, func(p proc.Process, fixture protest.Fixture) { withTestProcessArgs("testinline", t, ".", []string{}, protest.EnableInlining, func(p proc.Process, fixture protest.Fixture) {
pcs := p.BinInfo().AllPCsForFileLine(fixture.Source, 7) pcs, err := p.BinInfo().LineToPC(fixture.Source, 7)
assertNoError(err, t, "LineToPC")
if len(pcs) < 2 { if len(pcs) < 2 {
t.Fatalf("expected at least two locations for %s:%d (got %d: %#x)", fixture.Source, 7, len(pcs), pcs) t.Fatalf("expected at least two locations for %s:%d (got %d: %#x)", fixture.Source, 7, len(pcs), pcs)
} }
@ -3774,15 +3787,17 @@ func TestInlineBreakpoint(t *testing.T) {
t.Skip("inlining not supported") t.Skip("inlining not supported")
} }
withTestProcessArgs("testinline", t, ".", []string{}, protest.EnableInlining|protest.EnableOptimization, func(p proc.Process, fixture protest.Fixture) { withTestProcessArgs("testinline", t, ".", []string{}, protest.EnableInlining|protest.EnableOptimization, func(p proc.Process, fixture protest.Fixture) {
pc, fn, err := p.BinInfo().LineToPC(fixture.Source, 17) pcs, err := p.BinInfo().LineToPC(fixture.Source, 17)
if pc == 0 { t.Logf("%#v\n", pcs)
t.Fatal("unable to get PC for inlined function call") if len(pcs) != 1 {
t.Fatalf("unable to get PC for inlined function call: %v", pcs)
} }
fn := p.BinInfo().PCToFunc(pcs[0])
expectedFn := "main.main" expectedFn := "main.main"
if fn.Name != expectedFn { if fn.Name != expectedFn {
t.Fatalf("incorrect function returned, expected %s, got %s", expectedFn, fn.Name) t.Fatalf("incorrect function returned, expected %s, got %s", expectedFn, fn.Name)
} }
_, err = p.SetBreakpoint(pc, proc.UserBreakpoint, nil) _, err = p.SetBreakpoint(pcs[0], proc.UserBreakpoint, nil)
if err != nil { if err != nil {
t.Fatalf("unable to set breakpoint: %v", err) t.Fatalf("unable to set breakpoint: %v", err)
} }
@ -4290,7 +4305,7 @@ func TestCallConcurrent(t *testing.T) {
returned := testCallConcurrentCheckReturns(p, t, gid1, -1) returned := testCallConcurrentCheckReturns(p, t, gid1, -1)
curthread := p.CurrentThread() curthread := p.CurrentThread()
if curbp := curthread.Breakpoint(); curbp.Breakpoint == nil || curbp.ID != bp.ID || returned > 0 { if curbp := curthread.Breakpoint(); curbp.Breakpoint == nil || curbp.LogicalID != bp.LogicalID || returned > 0 {
t.Logf("skipping test, the call injection terminated before we hit a breakpoint in a different thread") t.Logf("skipping test, the call injection terminated before we hit a breakpoint in a different thread")
return return
} }

@ -73,16 +73,16 @@ func (v packageVarsByAddr) Less(i int, j int) bool { return v[i].addr < v[j].add
func (v packageVarsByAddr) Swap(i int, j int) { v[i], v[j] = v[j], v[i] } func (v packageVarsByAddr) Swap(i int, j int) { v[i], v[j] = v[j], v[i] }
type loadDebugInfoMapsContext struct { type loadDebugInfoMapsContext struct {
ardr *reader.Reader ardr *reader.Reader
abstractOriginNameTable map[dwarf.Offset]string abstractOriginTable map[dwarf.Offset]int
knownPackageVars map[string]struct{} knownPackageVars map[string]struct{}
} }
func newLoadDebugInfoMapsContext(bi *BinaryInfo, image *Image) *loadDebugInfoMapsContext { func newLoadDebugInfoMapsContext(bi *BinaryInfo, image *Image) *loadDebugInfoMapsContext {
ctxt := &loadDebugInfoMapsContext{} ctxt := &loadDebugInfoMapsContext{}
ctxt.ardr = image.DwarfReader() ctxt.ardr = image.DwarfReader()
ctxt.abstractOriginNameTable = make(map[dwarf.Offset]string) ctxt.abstractOriginTable = make(map[dwarf.Offset]int)
ctxt.knownPackageVars = map[string]struct{}{} ctxt.knownPackageVars = map[string]struct{}{}
for _, v := range bi.packageVars { for _, v := range bi.packageVars {

@ -18,7 +18,7 @@ import (
func ConvertBreakpoint(bp *proc.Breakpoint) *Breakpoint { func ConvertBreakpoint(bp *proc.Breakpoint) *Breakpoint {
b := &Breakpoint{ b := &Breakpoint{
Name: bp.Name, Name: bp.Name,
ID: bp.ID, ID: bp.LogicalID,
FunctionName: bp.FunctionName, FunctionName: bp.FunctionName,
File: bp.File, File: bp.File,
Line: bp.Line, Line: bp.Line,
@ -31,6 +31,7 @@ func ConvertBreakpoint(bp *proc.Breakpoint) *Breakpoint {
LoadArgs: LoadConfigFromProc(bp.LoadArgs), LoadArgs: LoadConfigFromProc(bp.LoadArgs),
LoadLocals: LoadConfigFromProc(bp.LoadLocals), LoadLocals: LoadConfigFromProc(bp.LoadLocals),
TotalHitCount: bp.TotalHitCount, TotalHitCount: bp.TotalHitCount,
Addrs: []uint64{bp.Addr},
} }
b.HitCount = map[string]uint64{} b.HitCount = map[string]uint64{}
@ -45,6 +46,28 @@ func ConvertBreakpoint(bp *proc.Breakpoint) *Breakpoint {
return b return b
} }
// ConvertBreakpoints converts a slice of physical breakpoints into a slice
// of logical breakpoints.
// The input must be sorted by increasing LogicalID
func ConvertBreakpoints(bps []*proc.Breakpoint) []*Breakpoint {
if len(bps) <= 0 {
return nil
}
r := make([]*Breakpoint, 0, len(bps))
for _, bp := range bps {
if len(r) > 0 {
if r[len(r)-1].ID == bp.LogicalID {
r[len(r)-1].Addrs = append(r[len(r)-1].Addrs, bp.Addr)
continue
} else if r[len(r)-1].ID > bp.LogicalID {
panic("input not sorted")
}
}
r = append(r, ConvertBreakpoint(bp))
}
return r
}
// ConvertThread converts a proc.Thread into an // ConvertThread converts a proc.Thread into an
// api thread. // api thread.
func ConvertThread(th proc.Thread) *Thread { func ConvertThread(th proc.Thread) *Thread {

@ -39,15 +39,17 @@ type DebuggerState struct {
Err error `json:"-"` Err error `json:"-"`
} }
// Breakpoint addresses a location at which process execution may be // Breakpoint addresses a set of locations at which process execution may be
// suspended. // suspended.
type Breakpoint struct { type Breakpoint struct {
// ID is a unique identifier for the breakpoint. // ID is a unique identifier for the breakpoint.
ID int `json:"id"` ID int `json:"id"`
// User defined name of the breakpoint // User defined name of the breakpoint.
Name string `json:"name"` Name string `json:"name"`
// Addr is the address of the breakpoint. // Addr is deprecated, use Addrs.
Addr uint64 `json:"addr"` Addr uint64 `json:"addr"`
// Addrs is the list of addresses for this breakpoint.
Addrs []uint64 `json:"addrs"`
// File is the source file for the breakpoint. // File is the source file for the breakpoint.
File string `json:"file"` File string `json:"file"`
// Line is a line in File for the breakpoint. // Line is a line in File for the breakpoint.

@ -8,6 +8,7 @@ import (
"path/filepath" "path/filepath"
"regexp" "regexp"
"runtime" "runtime"
"sort"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -326,24 +327,25 @@ func (d *Debugger) Restart(rerecord bool, pos string, resetArgs bool, newArgs []
return nil, fmt.Errorf("could not launch process: %s", err) return nil, fmt.Errorf("could not launch process: %s", err)
} }
discarded := []api.DiscardedBreakpoint{} discarded := []api.DiscardedBreakpoint{}
for _, oldBp := range d.breakpoints() { for _, oldBp := range api.ConvertBreakpoints(d.breakpoints()) {
if oldBp.ID < 0 { if oldBp.ID < 0 {
continue continue
} }
if len(oldBp.File) > 0 { if len(oldBp.File) > 0 {
var err error addrs, err := proc.FindFileLocation(p, oldBp.File, oldBp.Line)
oldBp.Addr, err = proc.FindFileLocation(p, oldBp.File, oldBp.Line)
if err != nil { if err != nil {
discarded = append(discarded, api.DiscardedBreakpoint{Breakpoint: oldBp, Reason: err.Error()}) discarded = append(discarded, api.DiscardedBreakpoint{Breakpoint: oldBp, Reason: err.Error()})
continue continue
} }
} createLogicalBreakpoint(p, addrs, oldBp)
newBp, err := p.SetBreakpoint(oldBp.Addr, proc.UserBreakpoint, nil) } else {
if err != nil { newBp, err := p.SetBreakpoint(oldBp.Addr, proc.UserBreakpoint, nil)
return nil, err if err != nil {
} return nil, err
if err := copyBreakpointInfo(newBp, oldBp); err != nil { }
return nil, err if err := copyBreakpointInfo(newBp, oldBp); err != nil {
return nil, err
}
} }
} }
d.target = p d.target = p
@ -413,9 +415,8 @@ func (d *Debugger) CreateBreakpoint(requestedBp *api.Breakpoint) (*api.Breakpoin
defer d.processMutex.Unlock() defer d.processMutex.Unlock()
var ( var (
createdBp *api.Breakpoint addrs []uint64
addr uint64 err error
err error
) )
if requestedBp.Name != "" { if requestedBp.Name != "" {
@ -429,7 +430,7 @@ func (d *Debugger) CreateBreakpoint(requestedBp *api.Breakpoint) (*api.Breakpoin
switch { switch {
case requestedBp.TraceReturn: case requestedBp.TraceReturn:
addr = requestedBp.Addr addrs = []uint64{requestedBp.Addr}
case len(requestedBp.File) > 0: case len(requestedBp.File) > 0:
fileName := requestedBp.File fileName := requestedBp.File
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
@ -442,30 +443,57 @@ func (d *Debugger) CreateBreakpoint(requestedBp *api.Breakpoint) (*api.Breakpoin
} }
} }
} }
addr, err = proc.FindFileLocation(d.target, fileName, requestedBp.Line) addrs, err = proc.FindFileLocation(d.target, fileName, requestedBp.Line)
case len(requestedBp.FunctionName) > 0: case len(requestedBp.FunctionName) > 0:
addr, err = proc.FindFunctionLocation(d.target, requestedBp.FunctionName, requestedBp.Line) addrs, err = proc.FindFunctionLocation(d.target, requestedBp.FunctionName, requestedBp.Line)
default: default:
addr = requestedBp.Addr addrs = []uint64{requestedBp.Addr}
} }
if err != nil { if err != nil {
return nil, err return nil, err
} }
bp, err := d.target.SetBreakpoint(addr, proc.UserBreakpoint, nil) createdBp, err := createLogicalBreakpoint(d.target, addrs, requestedBp)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err := copyBreakpointInfo(bp, requestedBp); err != nil { d.log.Infof("created breakpoint: %#v", createdBp)
if _, err1 := d.target.ClearBreakpoint(bp.Addr); err1 != nil { return createdBp, nil
err = fmt.Errorf("error while creating breakpoint: %v, additionally the breakpoint could not be properly rolled back: %v", err, err1) }
// createLogicalBreakpoint creates one physical breakpoint for each address
// in addrs and associates all of them with the same logical breakpoint.
func createLogicalBreakpoint(p proc.Process, addrs []uint64, requestedBp *api.Breakpoint) (*api.Breakpoint, error) {
bps := make([]*proc.Breakpoint, len(addrs))
var err error
for i := range addrs {
bps[i], err = p.SetBreakpoint(addrs[i], proc.UserBreakpoint, nil)
if err != nil {
break
}
if i > 0 {
bps[i].LogicalID = bps[0].LogicalID
}
err = copyBreakpointInfo(bps[i], requestedBp)
if err != nil {
break
}
}
if err != nil {
for _, bp := range bps {
if bp == nil {
continue
}
if _, err1 := p.ClearBreakpoint(bp.Addr); err1 != nil {
err = fmt.Errorf("error while creating breakpoint: %v, additionally the breakpoint could not be properly rolled back: %v", err, err1)
return nil, err
}
} }
return nil, err return nil, err
} }
createdBp = api.ConvertBreakpoint(bp) createdBp := api.ConvertBreakpoints(bps)
d.log.Infof("created breakpoint: %#v", createdBp) return createdBp[0], nil // we created a single logical breakpoint, the slice here will always have len == 1
return createdBp, nil
} }
// AmendBreakpoint will update the breakpoint with the matching ID. // AmendBreakpoint will update the breakpoint with the matching ID.
@ -473,14 +501,19 @@ func (d *Debugger) AmendBreakpoint(amend *api.Breakpoint) error {
d.processMutex.Lock() d.processMutex.Lock()
defer d.processMutex.Unlock() defer d.processMutex.Unlock()
original := d.findBreakpoint(amend.ID) originals := d.findBreakpoint(amend.ID)
if original == nil { if originals == nil {
return fmt.Errorf("no breakpoint with ID %d", amend.ID) return fmt.Errorf("no breakpoint with ID %d", amend.ID)
} }
if err := api.ValidBreakpointName(amend.Name); err != nil { if err := api.ValidBreakpointName(amend.Name); err != nil {
return err return err
} }
return copyBreakpointInfo(original, amend) for _, original := range originals {
if err := copyBreakpointInfo(original, amend); err != nil {
return err
}
}
return nil
} }
// CancelNext will clear internal breakpoints, thus cancelling the 'next', // CancelNext will clear internal breakpoints, thus cancelling the 'next',
@ -524,16 +557,17 @@ func (d *Debugger) ClearBreakpoint(requestedBp *api.Breakpoint) (*api.Breakpoint
func (d *Debugger) Breakpoints() []*api.Breakpoint { func (d *Debugger) Breakpoints() []*api.Breakpoint {
d.processMutex.Lock() d.processMutex.Lock()
defer d.processMutex.Unlock() defer d.processMutex.Unlock()
return d.breakpoints() return api.ConvertBreakpoints(d.breakpoints())
} }
func (d *Debugger) breakpoints() []*api.Breakpoint { func (d *Debugger) breakpoints() []*proc.Breakpoint {
bps := []*api.Breakpoint{} bps := []*proc.Breakpoint{}
for _, bp := range d.target.Breakpoints().M { for _, bp := range d.target.Breakpoints().M {
if bp.IsUser() { if bp.IsUser() {
bps = append(bps, api.ConvertBreakpoint(bp)) bps = append(bps, bp)
} }
} }
sort.Sort(breakpointsByLogicalID(bps))
return bps return bps
} }
@ -541,21 +575,21 @@ func (d *Debugger) breakpoints() []*api.Breakpoint {
func (d *Debugger) FindBreakpoint(id int) *api.Breakpoint { func (d *Debugger) FindBreakpoint(id int) *api.Breakpoint {
d.processMutex.Lock() d.processMutex.Lock()
defer d.processMutex.Unlock() defer d.processMutex.Unlock()
bps := api.ConvertBreakpoints(d.findBreakpoint(id))
bp := d.findBreakpoint(id) if len(bps) <= 0 {
if bp == nil {
return nil return nil
} }
return api.ConvertBreakpoint(bp) return bps[0]
} }
func (d *Debugger) findBreakpoint(id int) *proc.Breakpoint { func (d *Debugger) findBreakpoint(id int) []*proc.Breakpoint {
var bps []*proc.Breakpoint
for _, bp := range d.target.Breakpoints().M { for _, bp := range d.target.Breakpoints().M {
if bp.ID == id { if bp.LogicalID == id {
return bp bps = append(bps, bp)
} }
} }
return nil return bps
} }
// FindBreakpointByName returns the breakpoint specified by 'name' // FindBreakpointByName returns the breakpoint specified by 'name'
@ -566,12 +600,18 @@ func (d *Debugger) FindBreakpointByName(name string) *api.Breakpoint {
} }
func (d *Debugger) findBreakpointByName(name string) *api.Breakpoint { func (d *Debugger) findBreakpointByName(name string) *api.Breakpoint {
var bps []*proc.Breakpoint
for _, bp := range d.breakpoints() { for _, bp := range d.breakpoints() {
if bp.Name == name { if bp.Name == name {
return bp bps = append(bps, bp)
} }
} }
return nil if len(bps) == 0 {
return nil
}
sort.Sort(breakpointsByLogicalID(bps))
r := api.ConvertBreakpoints(bps)
return r[0] // there can only be one logical breakpoint with the same name
} }
// Threads returns the threads of the target process. // Threads returns the threads of the target process.
@ -1304,3 +1344,15 @@ func go11DecodeErrorCheck(err error) error {
return fmt.Errorf("executables built by Go 1.11 or later need Delve built by Go 1.11 or later") return fmt.Errorf("executables built by Go 1.11 or later need Delve built by Go 1.11 or later")
} }
type breakpointsByLogicalID []*proc.Breakpoint
func (v breakpointsByLogicalID) Len() int { return len(v) }
func (v breakpointsByLogicalID) Swap(i, j int) { v[i], v[j] = v[j], v[i] }
func (v breakpointsByLogicalID) Less(i, j int) bool {
if v[i].LogicalID == v[j].LogicalID {
return v[i].Addr < v[j].Addr
}
return v[i].LogicalID < v[j].LogicalID
}

@ -250,8 +250,8 @@ func (loc *RegexLocationSpec) Find(d *Debugger, scope *proc.EvalScope, locStr st
} }
r := make([]api.Location, 0, len(matches)) r := make([]api.Location, 0, len(matches))
for i := range matches { for i := range matches {
addr, err := proc.FindFunctionLocation(d.target, matches[i], 0) addrs, _ := proc.FindFunctionLocation(d.target, matches[i], 0)
if err == nil { for _, addr := range addrs {
r = append(r, api.Location{PC: addr}) r = append(r, api.Location{PC: addr})
} }
} }
@ -377,26 +377,37 @@ func (loc *NormalLocationSpec) Find(d *Debugger, scope *proc.EvalScope, locStr s
} }
// len(candidateFiles) + len(candidateFuncs) == 1 // len(candidateFiles) + len(candidateFuncs) == 1
var addr uint64 var addrs []uint64
var err error var err error
if len(candidateFiles) == 1 { if len(candidateFiles) == 1 {
if loc.LineOffset < 0 { if loc.LineOffset < 0 {
return nil, fmt.Errorf("Malformed breakpoint location, no line offset specified") return nil, fmt.Errorf("Malformed breakpoint location, no line offset specified")
} }
addr, err = proc.FindFileLocation(d.target, candidateFiles[0], loc.LineOffset) addrs, err = proc.FindFileLocation(d.target, candidateFiles[0], loc.LineOffset)
if includeNonExecutableLines { if includeNonExecutableLines {
if _, isCouldNotFindLine := err.(*proc.ErrCouldNotFindLine); isCouldNotFindLine { if _, isCouldNotFindLine := err.(*proc.ErrCouldNotFindLine); isCouldNotFindLine {
return []api.Location{{File: candidateFiles[0], Line: loc.LineOffset}}, nil return []api.Location{{File: candidateFiles[0], Line: loc.LineOffset}}, nil
} }
} }
} else { // len(candidateFUncs) == 1 } else { // len(candidateFUncs) == 1
addr, err = proc.FindFunctionLocation(d.target, candidateFuncs[0], loc.LineOffset) addrs, err = proc.FindFunctionLocation(d.target, candidateFuncs[0], loc.LineOffset)
} }
if err != nil { if err != nil {
return nil, err return nil, err
} }
return []api.Location{{PC: addr}}, nil return addressesToLocations(addrs), nil
}
func addressesToLocations(addrs []uint64) []api.Location {
if addrs == nil {
return nil
}
r := make([]api.Location, len(addrs))
for i := range addrs {
r[i] = api.Location{PC: addrs[i]}
}
return r
} }
func (loc *OffsetLocationSpec) Find(d *Debugger, scope *proc.EvalScope, locStr string, includeNonExecutableLines bool) ([]api.Location, error) { func (loc *OffsetLocationSpec) Find(d *Debugger, scope *proc.EvalScope, locStr string, includeNonExecutableLines bool) ([]api.Location, error) {
@ -410,13 +421,13 @@ func (loc *OffsetLocationSpec) Find(d *Debugger, scope *proc.EvalScope, locStr s
if fn == nil { if fn == nil {
return nil, fmt.Errorf("could not determine current location") return nil, fmt.Errorf("could not determine current location")
} }
addr, err := proc.FindFileLocation(d.target, file, line+loc.Offset) addrs, err := proc.FindFileLocation(d.target, file, line+loc.Offset)
if includeNonExecutableLines { if includeNonExecutableLines {
if _, isCouldNotFindLine := err.(*proc.ErrCouldNotFindLine); isCouldNotFindLine { if _, isCouldNotFindLine := err.(*proc.ErrCouldNotFindLine); isCouldNotFindLine {
return []api.Location{{File: file, Line: line + loc.Offset}}, nil return []api.Location{{File: file, Line: line + loc.Offset}}, nil
} }
} }
return []api.Location{{PC: addr}}, err return addressesToLocations(addrs), err
} }
func (loc *LineLocationSpec) Find(d *Debugger, scope *proc.EvalScope, locStr string, includeNonExecutableLines bool) ([]api.Location, error) { func (loc *LineLocationSpec) Find(d *Debugger, scope *proc.EvalScope, locStr string, includeNonExecutableLines bool) ([]api.Location, error) {
@ -427,11 +438,11 @@ func (loc *LineLocationSpec) Find(d *Debugger, scope *proc.EvalScope, locStr str
if fn == nil { if fn == nil {
return nil, fmt.Errorf("could not determine current location") return nil, fmt.Errorf("could not determine current location")
} }
addr, err := proc.FindFileLocation(d.target, file, loc.Line) addrs, err := proc.FindFileLocation(d.target, file, loc.Line)
if includeNonExecutableLines { if includeNonExecutableLines {
if _, isCouldNotFindLine := err.(*proc.ErrCouldNotFindLine); isCouldNotFindLine { if _, isCouldNotFindLine := err.(*proc.ErrCouldNotFindLine); isCouldNotFindLine {
return []api.Location{{File: file, Line: loc.Line}}, nil return []api.Location{{File: file, Line: loc.Line}}, nil
} }
} }
return []api.Location{{PC: addr}}, err return addressesToLocations(addrs), err
} }

@ -1070,11 +1070,14 @@ func setFunctionBreakpoint(p proc.Process, t testing.TB, fname string) *proc.Bre
_, f, l, _ := runtime.Caller(1) _, f, l, _ := runtime.Caller(1)
f = filepath.Base(f) f = filepath.Base(f)
addr, err := proc.FindFunctionLocation(p, fname, 0) addrs, err := proc.FindFunctionLocation(p, fname, 0)
if err != nil { if err != nil {
t.Fatalf("%s:%d: FindFunctionLocation(%s): %v", f, l, fname, err) t.Fatalf("%s:%d: FindFunctionLocation(%s): %v", f, l, fname, err)
} }
bp, err := p.SetBreakpoint(addr, proc.UserBreakpoint, nil) if len(addrs) != 1 {
t.Fatalf("%s:%d: setFunctionBreakpoint(%s): too many results %v", f, l, fname, addrs)
}
bp, err := p.SetBreakpoint(addrs[0], proc.UserBreakpoint, nil)
if err != nil { if err != nil {
t.Fatalf("%s:%d: FindFunctionLocation(%s): %v", f, l, fname, err) t.Fatalf("%s:%d: FindFunctionLocation(%s): %v", f, l, fname, err)
} }
@ -1352,11 +1355,14 @@ func setFileBreakpoint(p proc.Process, t *testing.T, fixture protest.Fixture, li
_, f, l, _ := runtime.Caller(1) _, f, l, _ := runtime.Caller(1)
f = filepath.Base(f) f = filepath.Base(f)
addr, err := proc.FindFileLocation(p, fixture.Source, lineno) addrs, err := proc.FindFileLocation(p, fixture.Source, lineno)
if err != nil { if err != nil {
t.Fatalf("%s:%d: FindFileLocation(%s, %d): %v", f, l, fixture.Source, lineno, err) t.Fatalf("%s:%d: FindFileLocation(%s, %d): %v", f, l, fixture.Source, lineno, err)
} }
bp, err := p.SetBreakpoint(addr, proc.UserBreakpoint, nil) if len(addrs) != 1 {
t.Fatalf("%s:%d: setFileLineBreakpoint(%s, %d): too many results %v", f, l, fixture.Source, lineno, addrs)
}
bp, err := p.SetBreakpoint(addrs[0], proc.UserBreakpoint, nil)
if err != nil { if err != nil {
t.Fatalf("%s:%d: SetBreakpoint: %v", f, l, err) t.Fatalf("%s:%d: SetBreakpoint: %v", f, l, err)
} }