proc: rewrite FindFileLocation to support generics
With generics a single function can have multiple concrete instantiations, the old version of FindFileLocation supported at most one concrete instantiation per function and any number of inlined calls, this supports any number of inlined calls and concrete functions.
This commit is contained in:
parent
878a52539e
commit
4e7b689e1a
12
_fixtures/genericbp.go
Normal file
12
_fixtures/genericbp.go
Normal file
@ -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)
|
||||
}
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user