diff --git a/_fixtures/genericbp.go b/_fixtures/genericbp.go new file mode 100644 index 00000000..b3f43d8d --- /dev/null +++ b/_fixtures/genericbp.go @@ -0,0 +1,12 @@ +package main + +import "fmt" + +func testfn[T any](arg T) { + fmt.Println(arg) +} + +func main() { + testfn[uint16](1) + testfn[float64](2.1) +} diff --git a/pkg/dwarf/line/state_machine.go b/pkg/dwarf/line/state_machine.go index bf3029a6..d2e3afa7 100644 --- a/pkg/dwarf/line/state_machine.go +++ b/pkg/dwarf/line/state_machine.go @@ -277,76 +277,35 @@ func (sm *StateMachine) PCToLine(pc uint64) (string, int, bool) { return "", 0, false } -// LineToPC returns the first PC address associated with filename:lineno. -func (lineInfo *DebugLineInfo) LineToPC(filename string, lineno int) uint64 { +// PCStmt is a PC address with its is_stmt flag +type PCStmt struct { + PC uint64 + Stmt bool +} + +// LineToPCs returns all PCs associated with filename:lineno +func (lineInfo *DebugLineInfo) LineToPCs(filename string, lineno int) []PCStmt { if lineInfo == nil { - return 0 + return nil } sm := newStateMachine(lineInfo, lineInfo.Instructions, lineInfo.ptrSize) - // if no instruction marked is_stmt is found fallback to the first - // instruction assigned to the filename:line. - var fallbackPC uint64 + pcstmts := []PCStmt{} for { if err := sm.next(); err != nil { if lineInfo.Logf != nil && err != io.EOF { - lineInfo.Logf("LineToPC error: %v", err) + lineInfo.Logf("LineToPCs error: %v", err) } break } if sm.line == lineno && sm.file == filename && sm.valid { - if sm.isStmt { - return sm.address - } else if fallbackPC == 0 { - fallbackPC = sm.address - } + pcstmts = append(pcstmts, PCStmt{sm.address, sm.isStmt}) } } - return fallbackPC -} -// LineToPCIn returns the first PC for filename:lineno in the interval [startPC, endPC). -// This function is used to find the instruction corresponding to -// filename:lineno for a function that has been inlined. -// basePC will be used for caching, it's normally the entry point for the -// function containing pc. -func (lineInfo *DebugLineInfo) LineToPCIn(filename string, lineno int, basePC, startPC, endPC uint64) uint64 { - if lineInfo == nil { - return 0 - } - if basePC > startPC { - panic(fmt.Errorf("basePC after startPC %#x %#x", basePC, startPC)) - } - - sm := lineInfo.stateMachineFor(basePC, startPC) - - var fallbackPC uint64 - - for { - if sm.valid && sm.started { - if sm.address >= endPC { - break - } - if sm.line == lineno && sm.file == filename && sm.address >= startPC { - if sm.isStmt { - return sm.address - } else { - fallbackPC = sm.address - } - } - } - if err := sm.next(); err != nil { - if lineInfo.Logf != nil && err != io.EOF { - lineInfo.Logf("LineToPC error: %v", err) - } - break - } - - } - - return fallbackPC + return pcstmts } // PrologueEndPC returns the first PC address marked as prologue_end in the half open interval [start, end) diff --git a/pkg/proc/bininfo.go b/pkg/proc/bininfo.go index 89dad17e..6e40601a 100644 --- a/pkg/proc/bininfo.go +++ b/pkg/proc/bininfo.go @@ -149,21 +149,158 @@ func (err *ErrFunctionNotFound) Error() string { // FindFileLocation returns the PC for a given file:line. // Assumes that `file` is normalized to lower case and '/' on Windows. -func FindFileLocation(p Process, fileName string, lineno int) ([]uint64, error) { - pcs, err := p.BinInfo().LineToPC(fileName, lineno) - if err != nil { - return nil, err +func FindFileLocation(p Process, filename string, lineno int) ([]uint64, error) { + // A single file:line can appear in multiple concrete functions, because of + // generics instantiation as well as multiple inlined calls into other + // concrete functions. + + // 1. Find all instructions assigned in debug_line to filename:lineno. + + bi := p.BinInfo() + + fileFound := false + pcs := []line.PCStmt{} + for _, image := range bi.Images { + for _, cu := range image.compileUnits { + if cu.lineInfo == nil || cu.lineInfo.Lookup[filename] == nil { + continue + } + + fileFound = true + pcs = append(pcs, cu.lineInfo.LineToPCs(filename, lineno)...) + } } + + if len(pcs) == 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. + for _, pc := range bi.inlinedCallLines[fileLine{filename, lineno}] { + pcs = append(pcs, line.PCStmt{PC: pc, Stmt: true}) + } + } + + if len(pcs) == 0 { + return nil, &ErrCouldNotFindLine{fileFound, filename, lineno} + } + + // 2. assign all occurences of filename:lineno to their containing function + + pcByFunc := map[*Function][]line.PCStmt{} + sort.Slice(pcs, func(i, j int) bool { return pcs[i].PC < pcs[j].PC }) var fn *Function - for i := range pcs { - if fn == nil || pcs[i] < fn.Entry || pcs[i] >= fn.End { - fn = p.BinInfo().PCToFunc(pcs[i]) + for _, pcstmt := range pcs { + if fn == nil || (pcstmt.PC < fn.Entry) || (pcstmt.PC >= fn.End) { + fn = p.BinInfo().PCToFunc(pcstmt.PC) } - if fn != nil && fn.Entry == pcs[i] { - pcs[i], _ = FirstPCAfterPrologue(p, fn, true) + if fn != nil { + pcByFunc[fn] = append(pcByFunc[fn], pcstmt) } } - return pcs, nil + + selectedPCs := []uint64{} + + for fn, pcs := range pcByFunc { + + // 3. for each concrete function split instruction between the inlined functions it contains + + if strings.Contains(fn.Name, "·dwrap·") || fn.trampoline { + // skip autogenerated functions + continue + } + + dwtree, err := fn.cu.image.getDwarfTree(fn.offset) + if err != nil { + return nil, fmt.Errorf("loading DWARF for %s@%#x: %v", fn.Name, fn.offset, err) + } + inlrngs := allInlineCallRanges(dwtree) + + // findInlRng returns the DWARF offset of the inlined call containing pc. + // If multiple nested inlined calls contain pc the deepest one is returned + // (since allInlineCallRanges returns inlined call by decreasing depth + // this is the first matching entry of the slice). + findInlRng := func(pc uint64) dwarf.Offset { + for _, inlrng := range inlrngs { + if inlrng.rng[0] <= pc && pc < inlrng.rng[1] { + return inlrng.off + } + } + return fn.offset + } + + pcsByOff := map[dwarf.Offset][]line.PCStmt{} + + for _, pc := range pcs { + off := findInlRng(pc.PC) + pcsByOff[off] = append(pcsByOff[off], pc) + } + + // 4. pick the first instruction with stmt set for each inlined call as + // well as the main body of the concrete function. If nothing has + // is_stmt set pick the first instruction instead. + + for off, pcs := range pcsByOff { + sort.Slice(pcs, func(i, j int) bool { return pcs[i].PC < pcs[j].PC }) + + var selectedPC uint64 + for _, pc := range pcs { + if pc.Stmt { + selectedPC = pc.PC + break + } + } + + if selectedPC == 0 && len(pcs) > 0 { + selectedPC = pcs[0].PC + } + + if selectedPC == 0 { + continue + } + + // 5. if we picked the entry point of the function, skip it + + if off == fn.offset && fn.Entry == selectedPC { + selectedPC, _ = FirstPCAfterPrologue(p, fn, true) + } + + selectedPCs = append(selectedPCs, selectedPC) + } + } + + return selectedPCs, nil +} + +// inlRnage is the range of an inlined call +type inlRange struct { + off dwarf.Offset + depth uint32 + rng [2]uint64 +} + +// allInlineCallRanges returns all inlined calls contained inside 'tree' in +// reverse nesting order (i.e. the most nested calls are returned first). +// Note that a single inlined call might not have a continuous range of +// addresses and therefore appear multiple times in the returned slice. +func allInlineCallRanges(tree *godwarf.Tree) []inlRange { + r := []inlRange{} + + var visit func(*godwarf.Tree, uint32) + visit = func(n *godwarf.Tree, depth uint32) { + if n.Tag == dwarf.TagInlinedSubroutine { + for _, rng := range n.Ranges { + r = append(r, inlRange{off: n.Offset, depth: depth, rng: rng}) + } + } + for _, child := range n.Children { + visit(child, depth+1) + } + } + visit(tree, 0) + + sort.SliceStable(r, func(i, j int) bool { return r[i].depth > r[j].depth }) + return r } // FindFunctionLocation finds address of a function's line @@ -175,27 +312,28 @@ func FindFunctionLocation(p Process, funcName string, lineOffset int) ([]uint64, return nil, &ErrFunctionNotFound{funcName} } - if lineOffset <= 0 { - 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 + if lineOffset > 0 { + filename, lineno := origfn.cu.lineInfo.PCToLine(origfn.Entry, origfn.Entry) + return FindFileLocation(p, filename, lineno+lineOffset) } - filename, lineno := origfn.cu.lineInfo.PCToLine(origfn.Entry, origfn.Entry) - return bi.LineToPC(filename, lineno+lineOffset) + + 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 } // FirstPCAfterPrologue returns the address of the first @@ -521,69 +659,6 @@ func (err *ErrCouldNotFindLine) Error() string { return fmt.Sprintf("could not find file %s", err.filename) } -// LineToPC converts a file:line into a list of matching memory addresses, -// 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 - var pc uint64 -pcsearch: - for _, image := range bi.Images { - for _, cu := range image.compileUnits { - if cu.lineInfo == nil || cu.lineInfo.Lookup[filename] == nil { - continue - } - fileFound = true - pc = cu.lineInfo.LineToPC(filename, lineno) - if pc != 0 { - break pcsearch - } - } - } - - 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 -} - -func appendLineToPCIn(pcs []uint64, filename string, lineno int, cu *compileUnit, containingFn *Function, lowPC, highPC uint64) []uint64 { - var entry uint64 - if containingFn != nil { - entry = containingFn.Entry - } - 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 func (bi *BinaryInfo) AllPCsForFileLines(filename string, linenos []int) map[int][]uint64 { r := make(map[int][]uint64) @@ -616,27 +691,6 @@ func (bi *BinaryInfo) PCToFunc(pc uint64) *Function { 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) - dwarfTree, err := fn.cu.image.getDwarfTree(fn.offset) - if err != nil { - return fn - } - entries := reader.InlineStack(dwarfTree, pc) - if len(entries) == 0 { - return fn - } - - fnname, okname := entries[0].Val(dwarf.AttrName).(string) - if !okname { - return fn - } - - return bi.LookupFunc[fnname] -} - // PCToImage returns the image containing the given PC address. func (bi *BinaryInfo) PCToImage(pc uint64) *Image { fn := bi.PCToFunc(pc) diff --git a/pkg/proc/proc_test.go b/pkg/proc/proc_test.go index c7d097b3..b8ac59af 100644 --- a/pkg/proc/proc_test.go +++ b/pkg/proc/proc_test.go @@ -3575,27 +3575,27 @@ func TestDeclLine(t *testing.T) { setFileBreakpoint(p, t, fixture.Source, 11) setFileBreakpoint(p, t, fixture.Source, 14) - assertNoError(p.Continue(), t, "Continue") + assertNoError(p.Continue(), t, "Continue 1") if goversion.VersionAfterOrEqual(runtime.Version(), 1, 15) { testDeclLineCount(t, p, 8, []string{}) } else { testDeclLineCount(t, p, 8, []string{"a"}) } - assertNoError(p.Continue(), t, "Continue") + assertNoError(p.Continue(), t, "Continue 2") testDeclLineCount(t, p, 9, []string{"a"}) - assertNoError(p.Continue(), t, "Continue") + assertNoError(p.Continue(), t, "Continue 3") if goversion.VersionAfterOrEqual(runtime.Version(), 1, 15) { testDeclLineCount(t, p, 10, []string{"a"}) } else { testDeclLineCount(t, p, 10, []string{"a", "b"}) } - assertNoError(p.Continue(), t, "Continue") + assertNoError(p.Continue(), t, "Continue 4") testDeclLineCount(t, p, 11, []string{"a", "b"}) - assertNoError(p.Continue(), t, "Continue") + assertNoError(p.Continue(), t, "Continue 5") testDeclLineCount(t, p, 14, []string{"a", "b"}) }) } @@ -3811,7 +3811,7 @@ func TestInlinedStacktraceAndVariables(t *testing.T) { } withTestProcessArgs("testinline", t, ".", []string{}, protest.EnableInlining, func(p *proc.Target, fixture protest.Fixture) { - pcs, err := p.BinInfo().LineToPC(fixture.Source, 7) + pcs, err := proc.FindFileLocation(p, fixture.Source, 7) assertNoError(err, t, "LineToPC") if len(pcs) < 2 { t.Fatalf("expected at least two locations for %s:%d (got %d: %#x)", fixture.Source, 7, len(pcs), pcs) @@ -3960,7 +3960,7 @@ func TestInlineBreakpoint(t *testing.T) { t.Skip("inlining not supported") } withTestProcessArgs("testinline", t, ".", []string{}, protest.EnableInlining|protest.EnableOptimization, func(p *proc.Target, fixture protest.Fixture) { - pcs, err := p.BinInfo().LineToPC(fixture.Source, 17) + pcs, err := proc.FindFileLocation(p, fixture.Source, 17) t.Logf("%#v\n", pcs) if len(pcs) != 1 { t.Fatalf("unable to get PC for inlined function call: %v", pcs) diff --git a/service/test/integration2_test.go b/service/test/integration2_test.go index f332b9b4..b2ed5bff 100644 --- a/service/test/integration2_test.go +++ b/service/test/integration2_test.go @@ -2475,3 +2475,38 @@ func TestLongStringArg(t *testing.T) { } }) } + +func TestGenericsBreakpoint(t *testing.T) { + if !goversion.VersionAfterOrEqual(runtime.Version(), 1, 18) { + t.Skip("generics") + } + // Tests that setting breakpoints inside a generic function with multiple + // instantiations results in a single logical breakpoint with N physical + // breakpoints (N = number of instantiations). + withTestClient2("genericbp", t, func(c service.Client) { + fp := testProgPath(t, "genericbp") + bp, err := c.CreateBreakpoint(&api.Breakpoint{File: fp, Line: 6}) + assertNoError(err, t, "CreateBreakpoint") + if len(bp.Addrs) != 2 { + t.Fatalf("wrong number of physical breakpoints: %d", len(bp.Addrs)) + } + + frame1Line := func() int { + frames, err := c.Stacktrace(-1, 10, 0, nil) + assertNoError(err, t, "Stacktrace") + return frames[1].Line + } + + state := <-c.Continue() + assertNoError(state.Err, t, "Continue") + if line := frame1Line(); line != 10 { + t.Errorf("wrong line after first continue, expected 10, got %d", line) + } + + state = <-c.Continue() + assertNoError(state.Err, t, "Continue") + if line := frame1Line(); line != 11 { + t.Errorf("wrong line after first continue, expected 11, got %d", line) + } + }) +}