From 18f2a4c46b8712188cc23f68cb0dc884a0a99f8b Mon Sep 17 00:00:00 2001 From: aarzilli Date: Thu, 16 Sep 2021 21:26:36 +0200 Subject: [PATCH] proc,dwarf/godwarf: support parametric types with dictionaries Change debug_info type reader and proc to convert parametric types into their real types by reading the corresponding dictionary entry and using the same method used for interfaces to retrieve the DIE from a runtime._type address. '2586e9b1'. --- _fixtures/testvariables_generic.go | 22 +++++++++++++++ pkg/dwarf/godwarf/type.go | 16 ++++++++++- pkg/proc/eval.go | 36 ++++++++++++++++++++++--- pkg/proc/fncall.go | 2 +- pkg/proc/types.go | 28 +++++++++++++++++++ pkg/proc/variables.go | 9 ++++++- service/test/variables_test.go | 43 +++++++++++++++++++++++++++++- 7 files changed, 149 insertions(+), 7 deletions(-) create mode 100644 _fixtures/testvariables_generic.go diff --git a/_fixtures/testvariables_generic.go b/_fixtures/testvariables_generic.go new file mode 100644 index 00000000..25860026 --- /dev/null +++ b/_fixtures/testvariables_generic.go @@ -0,0 +1,22 @@ +package main + +import ( + "fmt" + "runtime" +) + +type astruct struct { + x, y int +} + +func testfn[T any, K comparable](arg1 T, arg2 K) { + m := make(map[K]T) + m[arg2] = arg1 + runtime.Breakpoint() + fmt.Println(arg1, arg2, m) +} + +func main() { + testfn[int, float32](3, 2.1) + testfn(&astruct{0, 1}, astruct{2, 3}) +} diff --git a/pkg/dwarf/godwarf/type.go b/pkg/dwarf/godwarf/type.go index ba189115..49ff1c0a 100644 --- a/pkg/dwarf/godwarf/type.go +++ b/pkg/dwarf/godwarf/type.go @@ -27,6 +27,7 @@ const ( AttrGoEmbeddedField dwarf.Attr = 0x2903 AttrGoRuntimeType dwarf.Attr = 0x2904 AttrGoPackageName dwarf.Attr = 0x2905 + AttrGoDictIndex dwarf.Attr = 0x2906 ) // Basic type encodings -- the value for AttrEncoding in a TagBaseType Entry. @@ -513,6 +514,11 @@ func (t *ChanType) stringIntl(recCheck recCheck) string { return "chan " + t.ElemType.String() } +type ParametricType struct { + TypedefType + DictIndex int64 +} + // An UnsupportedType is a placeholder returned in situations where we // encounter a type that isn't supported. type UnsupportedType struct { @@ -1011,7 +1017,15 @@ func readType(d *dwarf.Data, name string, r *dwarf.Reader, off dwarf.Offset, typ typeCache[off] = it t = &it.TypedefType default: - typ = t + if dictIndex, ok := e.Val(AttrGoDictIndex).(int64); ok { + pt := new(ParametricType) + pt.DictIndex = dictIndex + typ = pt + typeCache[off] = pt + t = &pt.TypedefType + } else { + typ = t + } } typeCache[off] = typ t.Name, _ = e.Val(dwarf.AttrName).(string) diff --git a/pkg/proc/eval.go b/pkg/proc/eval.go index edf81804..d67c1e68 100644 --- a/pkg/proc/eval.go +++ b/pkg/proc/eval.go @@ -20,10 +20,13 @@ import ( "github.com/go-delve/delve/pkg/dwarf/op" "github.com/go-delve/delve/pkg/dwarf/reader" "github.com/go-delve/delve/pkg/goversion" + "github.com/go-delve/delve/pkg/logflags" ) var errOperationOnSpecialFloat = errors.New("operations on non-finite floats not implemented") +const goDictionaryName = ".dict" + // EvalScope is the scope for variable evaluation. Contains the thread, // current location (PC), and canonical frame address. type EvalScope struct { @@ -49,6 +52,8 @@ type EvalScope struct { // The goroutine executing the expression evaluation shall signal that the // evaluation is complete by closing the continueRequest channel. callCtx *callContext + + dictAddr uint64 // dictionary address for instantiated generic functions } type localsFlags uint8 @@ -230,10 +235,35 @@ func (scope *EvalScope) Locals(flags localsFlags) ([]*Variable, error) { } varEntries := reader.Variables(dwarfTree, scope.PC, scope.Line, variablesFlags) + + // look for dictionary entry + if scope.dictAddr == 0 { + for _, entry := range varEntries { + name, _ := entry.Val(dwarf.AttrName).(string) + if name == goDictionaryName { + dictVar, err := extractVarInfoFromEntry(scope.target, scope.BinInfo, scope.image(), scope.Regs, scope.Mem, entry.Tree, 0) + if err != nil { + logflags.DebuggerLogger().Errorf("could not load %s variable: %v", name, err) + } else if dictVar.Unreadable != nil { + logflags.DebuggerLogger().Errorf("could not load %s variable: %v", name, dictVar.Unreadable) + } else { + scope.dictAddr, err = readUintRaw(dictVar.mem, dictVar.Addr, int64(scope.BinInfo.Arch.PtrSize())) + if err != nil { + logflags.DebuggerLogger().Errorf("could not load %s variable: %v", name, err) + } + } + break + } + } + } + vars := make([]*Variable, 0, len(varEntries)) depths := make([]int, 0, len(varEntries)) for _, entry := range varEntries { - val, err := extractVarInfoFromEntry(scope.target, scope.BinInfo, scope.image(), scope.Regs, scope.Mem, entry.Tree) + if name, _ := entry.Val(dwarf.AttrName).(string); name == goDictionaryName { + continue + } + val, err := extractVarInfoFromEntry(scope.target, scope.BinInfo, scope.image(), scope.Regs, scope.Mem, entry.Tree, scope.dictAddr) if err != nil { // skip variables that we can't parse yet continue @@ -489,7 +519,7 @@ func (scope *EvalScope) PackageVariables(cfg LoadConfig) ([]*Variable, error) { } // Ignore errors trying to extract values - val, err := extractVarInfoFromEntry(scope.target, scope.BinInfo, pkgvar.cu.image, regsReplaceStaticBase(scope.Regs, pkgvar.cu.image), scope.Mem, godwarf.EntryToTree(entry)) + val, err := extractVarInfoFromEntry(scope.target, scope.BinInfo, pkgvar.cu.image, regsReplaceStaticBase(scope.Regs, pkgvar.cu.image), scope.Mem, godwarf.EntryToTree(entry), 0) if val != nil && val.Kind == reflect.Invalid { continue } @@ -526,7 +556,7 @@ func (scope *EvalScope) findGlobalInternal(name string) (*Variable, error) { if err != nil { return nil, err } - return extractVarInfoFromEntry(scope.target, scope.BinInfo, pkgvar.cu.image, regsReplaceStaticBase(scope.Regs, pkgvar.cu.image), scope.Mem, godwarf.EntryToTree(entry)) + return extractVarInfoFromEntry(scope.target, scope.BinInfo, pkgvar.cu.image, regsReplaceStaticBase(scope.Regs, pkgvar.cu.image), scope.Mem, godwarf.EntryToTree(entry), 0) } } for _, fn := range scope.BinInfo.Functions { diff --git a/pkg/proc/fncall.go b/pkg/proc/fncall.go index 6226edcc..4ce9cc4d 100644 --- a/pkg/proc/fncall.go +++ b/pkg/proc/fncall.go @@ -566,7 +566,7 @@ func funcCallCopyOneArg(scope *EvalScope, fncall *functionCallState, actualArg * var formalArgVar *Variable if formalArg.dwarfEntry != nil { var err error - formalArgVar, err = extractVarInfoFromEntry(scope.target, formalScope.BinInfo, formalScope.image(), formalScope.Regs, formalScope.Mem, formalArg.dwarfEntry) + formalArgVar, err = extractVarInfoFromEntry(scope.target, formalScope.BinInfo, formalScope.image(), formalScope.Regs, formalScope.Mem, formalArg.dwarfEntry, 0) if err != nil { return err } diff --git a/pkg/proc/types.go b/pkg/proc/types.go index 4626a2bf..b52914ac 100644 --- a/pkg/proc/types.go +++ b/pkg/proc/types.go @@ -174,6 +174,34 @@ func runtimeTypeToDIE(_type *Variable, dataAddr uint64) (typ godwarf.Type, kind return typ, kind, nil } +// resolveParametricType returns the real type of t if t is a parametric +// type, by reading the correct dictionary entry. +func resolveParametricType(tgt *Target, bi *BinaryInfo, mem MemoryReadWriter, t godwarf.Type, dictAddr uint64) (godwarf.Type, error) { + ptyp, _ := t.(*godwarf.ParametricType) + if ptyp == nil { + return t, nil + } + if dictAddr == 0 { + return ptyp.TypedefType.Type, errors.New("parametric type without a dictionary") + } + rtypeAddr, err := readUintRaw(mem, dictAddr+uint64(ptyp.DictIndex*int64(bi.Arch.PtrSize())), int64(bi.Arch.PtrSize())) + if err != nil { + return ptyp.TypedefType.Type, err + } + runtimeType, err := bi.findType("runtime._type") + if err != nil { + return ptyp.TypedefType.Type, err + } + _type := newVariable("", rtypeAddr, runtimeType, bi, mem) + + typ, _, err := runtimeTypeToDIE(_type, 0) + if err != nil { + return ptyp.TypedefType.Type, err + } + + return typ, nil +} + type nameOfRuntimeTypeEntry struct { typename string kind int64 diff --git a/pkg/proc/variables.go b/pkg/proc/variables.go index 30ad20ff..e8bc08d4 100644 --- a/pkg/proc/variables.go +++ b/pkg/proc/variables.go @@ -18,6 +18,7 @@ import ( "github.com/go-delve/delve/pkg/dwarf/godwarf" "github.com/go-delve/delve/pkg/dwarf/op" "github.com/go-delve/delve/pkg/goversion" + "github.com/go-delve/delve/pkg/logflags" ) const ( @@ -1135,7 +1136,7 @@ func readVarEntry(entry *godwarf.Tree, image *Image) (name string, typ godwarf.T // Extracts the name and type of a variable from a dwarf entry // then executes the instructions given in the DW_AT_location attribute to grab the variable's address -func extractVarInfoFromEntry(tgt *Target, bi *BinaryInfo, image *Image, regs op.DwarfRegisters, mem MemoryReadWriter, entry *godwarf.Tree) (*Variable, error) { +func extractVarInfoFromEntry(tgt *Target, bi *BinaryInfo, image *Image, regs op.DwarfRegisters, mem MemoryReadWriter, entry *godwarf.Tree, dictAddr uint64) (*Variable, error) { if entry.Tag != dwarf.TagFormalParameter && entry.Tag != dwarf.TagVariable { return nil, fmt.Errorf("invalid entry tag, only supports FormalParameter and Variable, got %s", entry.Tag.String()) } @@ -1145,6 +1146,12 @@ func extractVarInfoFromEntry(tgt *Target, bi *BinaryInfo, image *Image, regs op. return nil, err } + t, err = resolveParametricType(tgt, bi, mem, t, dictAddr) + if err != nil { + // Log the error, keep going with t, which will be the shape type + logflags.DebuggerLogger().Errorf("could not resolve parametric type of %s", n) + } + addr, pieces, descr, err := bi.Location(entry, dwarf.AttrLocation, regs.PC(), regs, mem) if pieces != nil { var cmem *compositeMemory diff --git a/service/test/variables_test.go b/service/test/variables_test.go index 44474084..74e80110 100644 --- a/service/test/variables_test.go +++ b/service/test/variables_test.go @@ -1602,7 +1602,48 @@ func TestCgoEval(t *testing.T) { t.Fatalf("Unexpected error. Expected %s got %s", tc.err.Error(), err.Error()) } } - + } + }) +} + +func TestEvalExpressionGenerics(t *testing.T) { + if !goversion.VersionAfterOrEqual(runtime.Version(), 1, 18) { + t.Skip("generics not supported") + } + + testcases := [][]varTest{ + // testfn[int, float32] + []varTest{ + {"arg1", true, "3", "", "int", nil}, + {"arg2", true, "2.1", "", "float32", nil}, + {"m", true, "map[float32]int [2.1: 3, ]", "", "map[float32]int", nil}, + }, + + // testfn[*astruct, astruct] + []varTest{ + {"arg1", true, "*main.astruct {x: 0, y: 1}", "", "*main.astruct", nil}, + {"arg2", true, "main.astruct {x: 2, y: 3}", "", "main.astruct", nil}, + {"m", true, "map[main.astruct]*main.astruct [{x: 2, y: 3}: *{x: 0, y: 1}, ]", "", "map[main.astruct]*main.astruct", nil}, + }, + } + + withTestProcess("testvariables_generic", t, func(p *proc.Target, fixture protest.Fixture) { + for i, tcs := range testcases { + assertNoError(p.Continue(), t, fmt.Sprintf("Continue() returned an error (%d)", i)) + for _, tc := range tcs { + variable, err := evalVariable(p, tc.name, pnormalLoadConfig) + if tc.err == nil { + assertNoError(err, t, fmt.Sprintf("EvalExpression(%s) returned an error", tc.name)) + assertVariable(t, variable, tc) + } else { + if err == nil { + t.Fatalf("Expected error %s, got no error (%s)", tc.err.Error(), tc.name) + } + if tc.err.Error() != err.Error() { + t.Fatalf("Unexpected error. Expected %s got %s", tc.err.Error(), err.Error()) + } + } + } } }) }