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:
aarzilli 2021-09-16 16:51:51 +02:00 committed by Alessandro Arzilli
parent 878a52539e
commit 4e7b689e1a
5 changed files with 235 additions and 175 deletions

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)
}
})
}