diff --git a/Documentation/KnownBugs.md b/Documentation/KnownBugs.md index 4929290d16ffae031212e76330d85511c473300d..a4031dece22f2655087f2e6b08a8c4182e66eef5 100644 --- a/Documentation/KnownBugs.md +++ b/Documentation/KnownBugs.md @@ -3,3 +3,4 @@ - When a function defines two (or more) variables with the same name delve is unable to distinguish between them: `locals` will print both variables, `print` will randomly pick one. See [Issue #106](https://github.com/derekparker/delve/issues/106). - Delve does not currently support 32bit systems. This will usually manifest as a compiler error in `proc/disasm.go`. See [Issue #20](https://github.com/derekparker/delve/issues/20). - When Delve is compiled with versions of go prior to 1.7.0 it is not possible to set a breakpoint on a function in a remote package using the `Receiver.MethodName` syntax. See [Issue #528](https://github.com/derekparker/delve/issues/528). +- When running Delve on binaries compiled with a version of go prior to 1.9.0 `locals` will print all local variables, including ones that are out of scope. If there are multiple variables defined with the same name in the current function `print` will not be able to select the correct one for the current line. diff --git a/_fixtures/scopeescapevareval.go b/_fixtures/scopeescapevareval.go new file mode 100644 index 0000000000000000000000000000000000000000..907f05297a26682536c504f3323ea521290650bd --- /dev/null +++ b/_fixtures/scopeescapevareval.go @@ -0,0 +1,17 @@ +package main + +import ( + "fmt" + "runtime" +) + +func main() { + a := 2 + { + a := 3 + p := &a + runtime.Breakpoint() + fmt.Println(a, p) + } + fmt.Println(a) +} diff --git a/_fixtures/scopetest.go b/_fixtures/scopetest.go new file mode 100644 index 0000000000000000000000000000000000000000..68d17be80f6b5d4978d227577209f2b2525869ed --- /dev/null +++ b/_fixtures/scopetest.go @@ -0,0 +1,165 @@ +package main + +func f1(x int) {} +func f2(x int) {} +func f3(x int) {} +func f4(x int) {} +func f5(x int) {} +func f6(x int) {} +func gret1(x int) int { + return x - 1 +} + +var boolvar = true + +func gretbool() bool { + x := boolvar + boolvar = !boolvar + return x +} +func gret3() (int, int, int) { return 0, 1, 2 } + +var v = []int{0, 1, 2} +var ch = make(chan int, 1) +var floatch = make(chan float64, 1) +var iface interface{} = 13 + +func TestNestedFor() { + a := 0 + f1(a) // a int = 0 + for i := 0; i < 5; i++ { + f2(i) // i int + for i := 1; i < 5; i++ { + f3(i) // a int = 0, i int = 0, i int = 1 + i++ + f3(i) + } + f4(i) // a int = 0, i int = 0 + } + f5(a) +} +func TestOas2() { + if a, b, c := gret3(); a != 1 { + f1(a) // a int = 0, b int = 1, c int = 2 + f1(b) // a int = 0, b int = 1, c int = 2 + f1(c) // a int = 0, b int = 1, c int = 2 + } + for i, x := range v { + f1(i) // i int = 0, x int = 0 + f1(x) // i int = 0, x int = 0 + } + if a, ok := <-ch; ok { + f1(a) // a int = 12, ok bool = true + } + if a, ok := iface.(int); ok { + f1(a) // a int = 13, ok bool = true + } +} +func TestIfElse(x int) { + if x := gret1(x); x != 0 { + a := 0 + f1(a) // x int = 2, x int = 1, a int = 0 + f1(x) + } else { + b := 1 + f1(b) // x int = 1, x int = 0, b int = 1 + f1(x + 1) + } +} +func TestSwitch(in int) { + switch x := gret1(in); x { + case 0: + i := x + 5 + f1(x) // in int = 1, x int = 0, i int = 5 + f1(i) + case 1: + j := x + 10 + f1(x) + f1(j) // in int = 2, x int = 1, j int = 11 + case 2: + k := x + 2 + f1(x) + f1(k) // in int = 3, x int = 2, k int = 4 + } +} +func TestTypeSwitch(iface interface{}) { + switch x := iface.(type) { + case int: + f1(x) // iface interface{}, x int = 1 + case uint8: + f1(int(x)) // iface interface{}, x uint8 = 2 + case float64: + f1(int(x) + 1) // iface interface{}, x float64 = 3.0 + } +} +func TestSelectScope() { + select { + case i := <-ch: + f1(i) // i int = 13 + case f := <-floatch: + f1(int(f)) // f float64 = 14.0 + } +} +func TestBlock() { + a := 1 + f1(a) // a int = 1 + { + b := 2 + a := 3 + f1(b) // a int = 1, a int = 3, b int = 2 + f1(a) // a int = 1, a int = 3, b int = 2 + } +} +func TestDiscontiguousRanges() { + a := 0 + f1(a) // a int = 0 + { + b := 0 + f2(b) // a int = 0, b int = 0 + if gretbool() { + c := 0 + f3(c) // a int = 0, b int = 0, c int = 0 + } else { + c := 1.1 + f4(int(c)) // a int = 0, b int = 0, c float64 = 1.1 + } + f5(b) // a int = 0, b int = 0 + } + f6(a) // a int = 0 +} +func TestClosureScope() { + a := 1 + b := 1 + f := func(c int) { + d := 3 + f1(a) // a int = 1, c int = 3, d int = 3 + f1(c) + f1(d) + if e := 3; e != 0 { + f1(e) // a int = 1, c int = 3, d int = 3, e int = 3 + } + } + f(3) + f1(b) +} +func main() { + ch <- 12 + TestNestedFor() + TestOas2() + TestIfElse(2) + TestIfElse(1) + TestSwitch(3) + TestSwitch(2) + TestSwitch(1) + TestTypeSwitch(1) + TestTypeSwitch(uint8(2)) + TestTypeSwitch(float64(3.0)) + ch <- 13 + TestSelectScope() + floatch <- 14.0 + TestSelectScope() + TestBlock() + TestDiscontiguousRanges() + TestDiscontiguousRanges() + TestClosureScope() +} diff --git a/_fixtures/testshadow.go b/_fixtures/testshadow.go new file mode 100644 index 0000000000000000000000000000000000000000..6156a10b7c59955941beb0d0606263d5a868f80d --- /dev/null +++ b/_fixtures/testshadow.go @@ -0,0 +1,16 @@ +package main + +import ( + "fmt" + "runtime" +) + +func main() { + a := 0 + { + a := 1 + runtime.Breakpoint() + fmt.Println(a) + } + fmt.Println(a) +} diff --git a/pkg/dwarf/reader/reader.go b/pkg/dwarf/reader/reader.go index 927133587439b890fe41c638659e715203fe0d96..cd56b2ade94304afd314f17ecc4f8cebaef3151f 100755 --- a/pkg/dwarf/reader/reader.go +++ b/pkg/dwarf/reader/reader.go @@ -257,30 +257,6 @@ func (reader *Reader) InstructionsForEntry(entry *dwarf.Entry) ([]byte, error) { return append([]byte{}, instructions...), nil } -// NextScopeVariable moves the reader to the next debug entry that describes a local variable and returns the entry. -func (reader *Reader) NextScopeVariable() (*dwarf.Entry, error) { - for entry, err := reader.Next(); entry != nil; entry, err = reader.Next() { - if err != nil { - return nil, err - } - - // All scope variables will be at the same depth - reader.SkipChildren() - - // End of the current depth - if entry.Tag == 0 { - break - } - - if entry.Tag == dwarf.TagVariable || entry.Tag == dwarf.TagFormalParameter { - return entry, nil - } - } - - // No more items - return nil, nil -} - // NextMememberVariable moves the reader to the next debug entry that describes a member variable and returns the entry. func (reader *Reader) NextMemberVariable() (*dwarf.Entry, error) { for entry, err := reader.Next(); entry != nil; entry, err = reader.Next() { diff --git a/pkg/dwarf/reader/variables.go b/pkg/dwarf/reader/variables.go new file mode 100644 index 0000000000000000000000000000000000000000..78705842136c4c1dd3d4984c32586fb193bc50da --- /dev/null +++ b/pkg/dwarf/reader/variables.go @@ -0,0 +1,103 @@ +package reader + +import ( + "errors" + + "debug/dwarf" +) + +// VariableReader provides a way of reading the local variables and formal +// parameters of a function that are visible at the specified PC address. +type VariableReader struct { + dwarf *dwarf.Data + reader *dwarf.Reader + entry *dwarf.Entry + depth int + onlyVisible bool + pc uint64 + err error +} + +// Variables returns a VariableReader for the function or lexical block at off. +// If onlyVisible is true only variables visible at pc will be returned by +// the VariableReader. +func Variables(dwarf *dwarf.Data, off dwarf.Offset, pc uint64, onlyVisible bool) *VariableReader { + reader := dwarf.Reader() + reader.Seek(off) + return &VariableReader{dwarf: dwarf, reader: reader, entry: nil, depth: 0, onlyVisible: onlyVisible, pc: pc, err: nil} +} + +// Next reads the next variable entry, returns false if there aren't any. +func (vrdr *VariableReader) Next() bool { + if vrdr.err != nil { + return false + } + + for { + vrdr.entry, vrdr.err = vrdr.reader.Next() + if vrdr.entry == nil || vrdr.err != nil { + return false + } + + switch vrdr.entry.Tag { + case 0: + vrdr.depth-- + if vrdr.depth == 0 { + return false + } + + case dwarf.TagLexDwarfBlock, dwarf.TagSubprogram: + recur := true + if vrdr.onlyVisible { + recur, vrdr.err = vrdr.entryRangesContains() + if vrdr.err != nil { + return false + } + } + + if recur { + vrdr.depth++ + } else { + if vrdr.depth == 0 { + return false + } + vrdr.reader.SkipChildren() + } + + default: + if vrdr.depth == 0 { + vrdr.err = errors.New("offset was not lexical block or subprogram") + return false + } + return true + } + } +} + +func (vrdr *VariableReader) entryRangesContains() (bool, error) { + rngs, err := vrdr.dwarf.Ranges(vrdr.entry) + if err != nil { + return false, err + } + for _, rng := range rngs { + if vrdr.pc >= rng[0] && vrdr.pc < rng[1] { + return true, nil + } + } + return false, nil +} + +// Entry returns the current variable entry. +func (vrdr *VariableReader) Entry() *dwarf.Entry { + return vrdr.entry +} + +// Depth returns the depth of the current scope +func (vrdr *VariableReader) Depth() int { + return vrdr.depth +} + +// Err returns the error if there was one. +func (vrdr *VariableReader) Err() error { + return vrdr.err +} diff --git a/pkg/proc/eval.go b/pkg/proc/eval.go index ff1382911ef3889ba5fead3601ef966660c43503..be92f516e73383550f033f19ae2764c475370aeb 100644 --- a/pkg/proc/eval.go +++ b/pkg/proc/eval.go @@ -2,6 +2,7 @@ package proc import ( "bytes" + "debug/dwarf" "encoding/binary" "errors" "fmt" @@ -445,28 +446,34 @@ func (scope *EvalScope) evalIdent(node *ast.Ident) (*Variable, error) { return nilVariable, nil } - // try to interpret this as a local variable - v, err := scope.extractVarInfo(node.Name) - if err == nil { - return v, nil + vars, err := scope.variablesByTag(dwarf.TagVariable, nil) + if err != nil { + return nil, err } - origErr := err - // workaround: sometimes go inserts an entry for '&varname' instead of varname - v, err = scope.extractVarInfo("&" + node.Name) - if err == nil { - v = v.maybeDereference() - v.Name = node.Name - return v, nil + for i := range vars { + if vars[i].Name == node.Name && vars[i].Flags&VariableShadowed == 0 { + return vars[i], nil + } } + args, err := scope.variablesByTag(dwarf.TagFormalParameter, nil) + if err != nil { + return nil, err + } + for i := range args { + if args[i].Name == node.Name { + return args[i], nil + } + } + // if it's not a local variable then it could be a package variable w/o explicit package name _, _, fn := scope.BinInfo.PCToLine(scope.PC) if fn != nil { - if v, err = scope.packageVarAddr(fn.PackageName() + "." + node.Name); err == nil { + if v, err := scope.packageVarAddr(fn.PackageName() + "." + node.Name); err == nil { v.Name = node.Name return v, nil } } - return nil, origErr + return nil, fmt.Errorf("could not find symbol value for %s", node.Name) } // Evaluates expressions . where subexpr is not a package name diff --git a/pkg/proc/proc_test.go b/pkg/proc/proc_test.go index e9c467b9098d6786ad666f77dc7e2fa1f1ac864e..523775a420f3453839b8ac864b2f7a2e9bbace59 100644 --- a/pkg/proc/proc_test.go +++ b/pkg/proc/proc_test.go @@ -3032,3 +3032,43 @@ func TestIssue871(t *testing.T) { } }) } + +func TestShadowedFlag(t *testing.T) { + if ver, _ := goversion.Parse(runtime.Version()); ver.Major >= 0 && !ver.AfterOrEqual(goversion.GoVersion{1, 9, -1, 0, 0, ""}) { + return + } + withTestProcess("testshadow", t, func(p proc.Process, fixture protest.Fixture) { + assertNoError(proc.Continue(p), t, "Continue") + scope, err := proc.GoroutineScope(p.CurrentThread()) + assertNoError(err, t, "GoroutineScope") + locals, err := scope.LocalVariables(normalLoadConfig) + assertNoError(err, t, "LocalVariables") + foundShadowed := false + foundNonShadowed := false + for _, v := range locals { + if v.Flags&proc.VariableShadowed != 0 { + if v.Name != "a" { + t.Errorf("wrong shadowed variable %s", v.Name) + } + foundShadowed = true + if n, _ := constant.Int64Val(v.Value); n != 0 { + t.Errorf("wrong value for shadowed variable a: %d", n) + } + } else { + if v.Name != "a" { + t.Errorf("wrong non-shadowed variable %s", v.Name) + } + foundNonShadowed = true + if n, _ := constant.Int64Val(v.Value); n != 1 { + t.Errorf("wrong value for non-shadowed variable a: %d", n) + } + } + } + if !foundShadowed { + t.Error("could not find any shadowed variable") + } + if !foundNonShadowed { + t.Error("could not find any non-shadowed variable") + } + }) +} diff --git a/pkg/proc/scope_test.go b/pkg/proc/scope_test.go new file mode 100644 index 0000000000000000000000000000000000000000..5ded1771514d3d19a8f3dd0e0edd3db6d31135b1 --- /dev/null +++ b/pkg/proc/scope_test.go @@ -0,0 +1,312 @@ +package proc_test + +import ( + "fmt" + "go/constant" + "go/parser" + "go/token" + "math" + "path/filepath" + "reflect" + "runtime" + "strconv" + "strings" + "testing" + + "github.com/derekparker/delve/pkg/goversion" + "github.com/derekparker/delve/pkg/proc" + protest "github.com/derekparker/delve/pkg/proc/test" +) + +func TestScopeWithEscapedVariable(t *testing.T) { + if ver, _ := goversion.Parse(runtime.Version()); ver.Major >= 0 && !ver.AfterOrEqual(goversion.GoVersion{1, 9, -1, 3, 0, ""}) { + return + } + + withTestProcess("scopeescapevareval", t, func(p proc.Process, fixture protest.Fixture) { + assertNoError(proc.Continue(p), t, "Continue") + + // On the breakpoint there are two 'a' variables in scope, the one that + // isn't shadowed is a variable that escapes to the heap and figures in + // debug_info as '&a'. Evaluating 'a' should yield the escaped variable. + + avar, err := evalVariable(p, "a") + assertNoError(err, t, "EvalVariable(a)") + if aval, _ := constant.Int64Val(avar.Value); aval != 3 { + t.Errorf("wrong value for variable a: %d", aval) + } + + if avar.Flags&proc.VariableEscaped == 0 { + t.Errorf("variale a isn't escaped to the heap") + } + }) +} + +// TestScope will: +// - run _fixtures/scopetest.go +// - set a breakpoint on all lines containing a comment +// - continue until the program ends +// - every time a breakpoint is hit it will check that +// scope.FunctionArguments+scope.LocalVariables and scope.EvalExpression +// return what the corresponding comment describes they should return and +// removes the breakpoint. +// +// Each comment is a comma separated list of variable declarations, with +// each variable declaration having the following format: +// +// name type = initialvalue +// +// the = and the initial value are optional and can only be specified if the +// type is an integer type, float32, float64 or bool. +// +// If multiple variables with the same name are specified +// LocalVariables+FunctionArguments should return them in the same order and +// EvalExpression should return the last one. +func TestScope(t *testing.T) { + if ver, _ := goversion.Parse(runtime.Version()); ver.Major >= 0 && !ver.AfterOrEqual(goversion.GoVersion{1, 9, -1, 0, 0, ""}) { + return + } + + fixturesDir := protest.FindFixturesDir() + scopetestPath := filepath.Join(fixturesDir, "scopetest.go") + + scopeChecks := getScopeChecks(scopetestPath, t) + + withTestProcess("scopetest", t, func(p proc.Process, fixture protest.Fixture) { + for i := range scopeChecks { + setFileBreakpoint(p, t, fixture, scopeChecks[i].line) + } + + t.Logf("%d breakpoints set", len(scopeChecks)) + + for { + if err := proc.Continue(p); err != nil { + if _, exited := err.(proc.ProcessExitedError); exited { + break + } + assertNoError(err, t, "Continue()") + } + bp, _, _ := p.CurrentThread().Breakpoint() + + scopeCheck := findScopeCheck(scopeChecks, bp.Line) + if scopeCheck == nil { + t.Errorf("unknown stop position %s:%d %#x", bp.File, bp.Line, bp.Addr) + } + + scope, err := proc.GoroutineScope(p.CurrentThread()) + assertNoError(err, t, "GoroutineScope()") + + args, err := scope.FunctionArguments(normalLoadConfig) + assertNoError(err, t, "FunctionArguments()") + locals, err := scope.LocalVariables(normalLoadConfig) + assertNoError(err, t, "LocalVariables()") + + for _, arg := range args { + scopeCheck.checkVar(arg, t) + } + + for _, local := range locals { + scopeCheck.checkVar(local, t) + } + + for i := range scopeCheck.varChecks { + if !scopeCheck.varChecks[i].ok { + t.Errorf("%d: variable %s not found", scopeCheck.line, scopeCheck.varChecks[i].name) + } + } + + var prev *varCheck + for i := range scopeCheck.varChecks { + vc := &scopeCheck.varChecks[i] + if prev != nil && prev.name != vc.name { + prev.checkInScope(scopeCheck.line, scope, t) + } + prev = vc + } + if prev != nil { + prev.checkInScope(scopeCheck.line, scope, t) + } + + scopeCheck.ok = true + _, err = p.ClearBreakpoint(bp.Addr) + assertNoError(err, t, "ClearBreakpoint") + } + }) + + for i := range scopeChecks { + if !scopeChecks[i].ok { + t.Errorf("breakpoint at line %d not hit", scopeChecks[i].line) + } + } + +} + +type scopeCheck struct { + line int + varChecks []varCheck + ok bool // this scope check was passed +} + +type varCheck struct { + name string + typ string + kind reflect.Kind + hasVal bool + intVal int64 + uintVal uint64 + floatVal float64 + boolVal bool + + ok bool // this variable check was passed +} + +func getScopeChecks(path string, t *testing.T) []scopeCheck { + var fset token.FileSet + root, err := parser.ParseFile(&fset, path, nil, parser.ParseComments) + if err != nil { + t.Fatalf("could not parse %s: %v", path, err) + } + + scopeChecks := []scopeCheck{} + + for _, cmtg := range root.Comments { + for _, cmt := range cmtg.List { + pos := fset.Position(cmt.Slash) + + scopeChecks = append(scopeChecks, scopeCheck{line: pos.Line}) + scopeChecks[len(scopeChecks)-1].Parse(cmt.Text[2:], t) + } + } + + return scopeChecks +} + +func findScopeCheck(scopeChecks []scopeCheck, line int) *scopeCheck { + for i := range scopeChecks { + if scopeChecks[i].line == line { + return &scopeChecks[i] + } + } + return nil +} + +func (check *scopeCheck) Parse(descr string, t *testing.T) { + decls := strings.Split(descr, ",") + check.varChecks = make([]varCheck, len(decls)) + for i, decl := range decls { + varcheck := &check.varChecks[i] + value := "" + if equal := strings.Index(decl, "="); equal >= 0 { + value = strings.TrimSpace(decl[equal+1:]) + decl = strings.TrimSpace(decl[:equal]) + varcheck.hasVal = true + } else { + decl = strings.TrimSpace(decl) + } + + space := strings.Index(decl, " ") + if space < 0 { + t.Fatalf("could not parse scope comment %q (%q)", descr, decl) + } + varcheck.name = strings.TrimSpace(decl[:space]) + varcheck.typ = strings.TrimSpace(decl[space+1:]) + if strings.Index(varcheck.typ, " ") >= 0 { + t.Fatalf("could not parse scope comment %q (%q)", descr, decl) + } + + if !varcheck.hasVal { + continue + } + + switch varcheck.typ { + case "int", "int8", "int16", "int32", "int64": + var err error + varcheck.kind = reflect.Int + varcheck.intVal, err = strconv.ParseInt(value, 10, 64) + if err != nil { + t.Fatalf("could not parse scope comment %q: %v", descr, err) + } + + case "uint", "uint8", "uint16", "uint32", "uint64", "uintptr": + var err error + varcheck.kind = reflect.Uint + varcheck.uintVal, err = strconv.ParseUint(value, 10, 64) + if err != nil { + t.Fatalf("could not parse scope comment %q: %v", descr, err) + } + + case "float32", "float64": + var err error + varcheck.kind = reflect.Float64 + varcheck.floatVal, err = strconv.ParseFloat(value, 64) + if err != nil { + t.Fatalf("could not parse scope comment %q: %v", descr, err) + } + + case "bool": + var err error + varcheck.kind = reflect.Bool + varcheck.boolVal, err = strconv.ParseBool(value) + if err != nil { + t.Fatalf("could not parse scope comment %q: %v", descr, err) + } + + } + } +} + +func (check *scopeCheck) checkVar(v *proc.Variable, t *testing.T) { + var varCheck *varCheck + for i := range check.varChecks { + varCheck = &check.varChecks[i] + if !varCheck.ok && (varCheck.name == v.Name) { + varCheck = &check.varChecks[i] + break + } + } + + if varCheck == nil { + t.Errorf("%d: unexpected variable %s", check.line, v.Name) + } + + varCheck.check(check.line, v, t, "FunctionArguments+LocalVariables") + varCheck.ok = true +} + +func (varCheck *varCheck) checkInScope(line int, scope *proc.EvalScope, t *testing.T) { + v, err := scope.EvalVariable(varCheck.name, normalLoadConfig) + assertNoError(err, t, fmt.Sprintf("EvalVariable(%s)", varCheck.name)) + varCheck.check(line, v, t, "EvalExpression") + +} + +func (varCheck *varCheck) check(line int, v *proc.Variable, t *testing.T, ctxt string) { + typ := v.DwarfType.String() + typ = strings.Replace(typ, " ", "", -1) + if typ != varCheck.typ { + t.Errorf("%d: wrong type for %s (%s), got %s, expected %s", line, v.Name, ctxt, typ, varCheck.typ) + } + + if !varCheck.hasVal { + return + } + + switch varCheck.kind { + case reflect.Int: + if vv, _ := constant.Int64Val(v.Value); vv != varCheck.intVal { + t.Errorf("%d: wrong value for %s (%s), got %d expected %d", line, v.Name, ctxt, vv, varCheck.intVal) + } + case reflect.Uint: + if vv, _ := constant.Uint64Val(v.Value); vv != varCheck.uintVal { + t.Errorf("%d: wrong value for %s (%s), got %d expected %d", line, v.Name, ctxt, vv, varCheck.uintVal) + } + case reflect.Float64: + if vv, _ := constant.Float64Val(v.Value); math.Abs(vv-varCheck.floatVal) > 0.001 { + t.Errorf("%d: wrong value for %s (%s), got %g expected %g", line, v.Name, ctxt, vv, varCheck.floatVal) + } + case reflect.Bool: + if vv := constant.BoolVal(v.Value); vv != varCheck.boolVal { + t.Errorf("%d: wrong value for %s (%s), got %v expected %v", line, v.Name, ctxt, vv, varCheck.boolVal) + } + } +} diff --git a/pkg/proc/variables.go b/pkg/proc/variables.go index 1eec76f09bbd510ebe1811270a26bdd6ca0f5fe6..8a473810e9855cf4244f6f36817880208d68d730 100644 --- a/pkg/proc/variables.go +++ b/pkg/proc/variables.go @@ -11,6 +11,7 @@ import ( "go/token" "math" "reflect" + "sort" "strings" "unsafe" @@ -45,6 +46,9 @@ type VariableFlags uint16 const ( // VariableEscaped is set for local variables that escaped to the heap VariableEscaped VariableFlags = (1 << iota) + // VariableShadowed is set for local variables that are shadowed by a + // variable with the same name in another scope + VariableShadowed ) // Variable represents a variable. It contains the address, name, @@ -579,52 +583,22 @@ func (scope *EvalScope) SetVariable(name, value string) error { return xv.setValue(yv) } -func (scope *EvalScope) extractVariableFromEntry(entry *dwarf.Entry, cfg LoadConfig) (*Variable, error) { - rdr := scope.DwarfReader() - v, err := scope.extractVarInfoFromEntry(entry, rdr) +func (scope *EvalScope) extractVariableFromEntry(entry *dwarf.Entry) (*Variable, error) { + v, err := scope.extractVarInfoFromEntry(entry) if err != nil { return nil, err } return v, nil } -func (scope *EvalScope) extractVarInfo(varName string) (*Variable, error) { - reader := scope.DwarfReader() - off, err := scope.BinInfo.findFunctionDebugInfo(scope.PC) - if err != nil { - return nil, err - } - reader.Seek(off) - reader.Next() - - for entry, err := reader.NextScopeVariable(); entry != nil; entry, err = reader.NextScopeVariable() { - if err != nil { - return nil, err - } - if entry.Tag == 0 { - break - } - - n, ok := entry.Val(dwarf.AttrName).(string) - if !ok { - continue - } - - if n == varName { - return scope.extractVarInfoFromEntry(entry, reader) - } - } - return nil, fmt.Errorf("could not find symbol value for %s", varName) -} - // LocalVariables returns all local variables from the current function scope. func (scope *EvalScope) LocalVariables(cfg LoadConfig) ([]*Variable, error) { - return scope.variablesByTag(dwarf.TagVariable, cfg) + return scope.variablesByTag(dwarf.TagVariable, &cfg) } // FunctionArguments returns the name, value, and type of all current function arguments. func (scope *EvalScope) FunctionArguments(cfg LoadConfig) ([]*Variable, error) { - return scope.variablesByTag(dwarf.TagFormalParameter, cfg) + return scope.variablesByTag(dwarf.TagFormalParameter, &cfg) } // PackageVariables returns the name, value, and type of all package variables in the application. @@ -648,7 +622,7 @@ func (scope *EvalScope) PackageVariables(cfg LoadConfig) ([]*Variable, error) { } // Ignore errors trying to extract values - val, err := scope.extractVariableFromEntry(entry, cfg) + val, err := scope.extractVariableFromEntry(entry) if err != nil { continue } @@ -668,7 +642,7 @@ func (scope *EvalScope) packageVarAddr(name string) (*Variable, error) { if err != nil { return nil, err } - return scope.extractVarInfoFromEntry(entry, reader) + return scope.extractVarInfoFromEntry(entry) } } return nil, fmt.Errorf("could not find symbol value for %s", name) @@ -735,7 +709,7 @@ func (v *Variable) structMember(memberName string) (*Variable, error) { // 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 (scope *EvalScope) extractVarInfoFromEntry(entry *dwarf.Entry, rdr *reader.Reader) (*Variable, error) { +func (scope *EvalScope) extractVarInfoFromEntry(entry *dwarf.Entry) (*Variable, error) { if entry == nil { return nil, fmt.Errorf("invalid entry") } @@ -1665,39 +1639,61 @@ func (v *Variable) loadInterface(recurseLevel int, loadData bool, cfg LoadConfig } } +type variablesByDepth struct { + vars []*Variable + depths []int +} + +func (v *variablesByDepth) Len() int { return len(v.vars) } + +func (v *variablesByDepth) Less(i int, j int) bool { return v.depths[i] < v.depths[j] } + +func (v *variablesByDepth) Swap(i int, j int) { + v.depths[i], v.depths[j] = v.depths[j], v.depths[i] + v.vars[i], v.vars[j] = v.vars[j], v.vars[i] +} + // Fetches all variables of a specific type in the current function scope -func (scope *EvalScope) variablesByTag(tag dwarf.Tag, cfg LoadConfig) ([]*Variable, error) { - reader := scope.DwarfReader() +func (scope *EvalScope) variablesByTag(tag dwarf.Tag, cfg *LoadConfig) ([]*Variable, error) { off, err := scope.BinInfo.findFunctionDebugInfo(scope.PC) if err != nil { return nil, err } - reader.Seek(off) - reader.Next() var vars []*Variable - for entry, err := reader.NextScopeVariable(); entry != nil; entry, err = reader.NextScopeVariable() { + var depths []int + varReader := reader.Variables(scope.BinInfo.dwarf, off, scope.PC, tag == dwarf.TagVariable) + hasScopes := false + for varReader.Next() { + entry := varReader.Entry() + if entry.Tag != tag { + continue + } + val, err := scope.extractVariableFromEntry(entry) if err != nil { - return nil, err + // skip variables that we can't parse yet + continue } - if entry.Tag == 0 { - break + vars = append(vars, val) + depth := varReader.Depth() + depths = append(depths, depth) + if depth > 1 { + hasScopes = true } + } - if entry.Tag == tag { - val, err := scope.extractVariableFromEntry(entry, cfg) - if err != nil { - // skip variables that we can't parse yet - continue - } - - vars = append(vars, val) - } + if err := varReader.Err(); err != nil { + return nil, err } + if len(vars) <= 0 { return vars, nil } + if hasScopes { + sort.Stable(&variablesByDepth{vars, depths}) + } + // prefetch the whole chunk of memory relative to these variables minaddr := vars[0].Addr @@ -1728,6 +1724,8 @@ func (scope *EvalScope) variablesByTag(tag dwarf.Tag, cfg LoadConfig) ([]*Variab } } + lvn := map[string]*Variable{} // lvn[n] is the last variable we saw named n + for i, v := range vars { if name := v.Name; len(name) > 1 && name[0] == '&' { v = v.maybeDereference() @@ -1735,7 +1733,15 @@ func (scope *EvalScope) variablesByTag(tag dwarf.Tag, cfg LoadConfig) ([]*Variab v.Flags |= VariableEscaped vars[i] = v } - v.loadValue(cfg) + if hasScopes { + if otherv := lvn[v.Name]; otherv != nil { + otherv.Flags |= VariableShadowed + } + lvn[v.Name] = v + } + if cfg != nil { + v.loadValue(*cfg) + } } return vars, nil diff --git a/pkg/terminal/command.go b/pkg/terminal/command.go index 3c77f4634ff57da52eb2d2a572426b86ac935db5..105ff29251e17259ee9b40b722428bb85fec41fb 100644 --- a/pkg/terminal/command.go +++ b/pkg/terminal/command.go @@ -169,6 +169,8 @@ If regex is specified only function arguments with a name matching it will be re {aliases: []string{"locals"}, allowedPrefixes: scopePrefix | onPrefix, cmdFn: locals, helpMsg: `Print local variables. [goroutine ] [frame ] locals [-v] [] + +The name of variables that are shadowed in the current scope will be shown in parenthesis. If regex is specified only local variables with a name matching it will be returned. If -v is specified more information about each local variable will be shown.`}, {aliases: []string{"vars"}, cmdFn: vars, helpMsg: `Print package variables. @@ -935,10 +937,14 @@ func printFilteredVariables(varType string, vars []api.Variable, filter string, for _, v := range vars { if reg == nil || reg.Match([]byte(v.Name)) { match = true + name := v.Name + if v.Flags&api.VariableShadowed != 0 { + name = "(" + name + ")" + } if cfg == ShortLoadConfig { - fmt.Printf("%s = %s\n", v.Name, v.SinglelineString()) + fmt.Printf("%s = %s\n", name, v.SinglelineString()) } else { - fmt.Printf("%s = %s\n", v.Name, v.MultilineString("")) + fmt.Printf("%s = %s\n", name, v.MultilineString("")) } } } diff --git a/service/api/types.go b/service/api/types.go index b64f37bb661430268f21ff7d2c7ea2bac9037410..7181fd33935465afd2073e7f8c61526a8ff56c08 100644 --- a/service/api/types.go +++ b/service/api/types.go @@ -147,10 +147,16 @@ type Function struct { GoType uint64 `json:"goType"` } +// VariableFlags is the type of the Flags field of Variable. type VariableFlags uint16 const ( + // VariableEscaped is set for local variables that escaped to the heap VariableEscaped = VariableFlags(proc.VariableEscaped) + + // VariableShadowed is set for local variables that are shadowed by a + // variable with the same name in another scope + VariableShadowed = VariableFlags(proc.VariableShadowed) ) // Variable describes a variable.