diff --git a/README.md b/README.md index 4a68451c..37f31005 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,8 @@ Once inside a debugging session, the following commands may be used: * `info $type [regex]` - Outputs information about the symbol table. An optional regex filters the list. Example `info funcs unicode`. Valid types are: * `sources` - Prings the path of all source files * `funcs` - Prings the name of all defined functions + * `locals` - Prints the name and value of all local variables in the current context + * `args` - Prints the name and value of all arguments to the current function * `exit` - Exit the debugger. diff --git a/_fixtures/testvariables.go b/_fixtures/testvariables.go index d14a38c6..96d060c3 100644 --- a/_fixtures/testvariables.go +++ b/_fixtures/testvariables.go @@ -12,7 +12,7 @@ func barfoo() { fmt.Println(a1) } -func foobar(baz string) { +func foobar(baz string, bar FooBar) { var ( a1 = "foo" a2 = 6 @@ -28,9 +28,9 @@ func foobar(baz string) { ) barfoo() - fmt.Println(a1, a2, a3, a4, a5, a6, a7, baz, neg, i8, f32, i32) + fmt.Println(a1, a2, a3, a4, a5, a6, a7, baz, neg, i8, f32, i32, bar) } func main() { - foobar("bazburzum") + foobar("bazburzum", FooBar{Baz: 10, Bur: "lorem"}) } diff --git a/command/command.go b/command/command.go index 4c69c53d..d05ff561 100644 --- a/command/command.go +++ b/command/command.go @@ -54,7 +54,7 @@ func DebugCommands() *Commands { command{aliases: []string{"clear"}, cmdFn: clear, helpMsg: "Deletes breakpoint."}, command{aliases: []string{"goroutines"}, cmdFn: goroutines, helpMsg: "Print out info for every goroutine."}, command{aliases: []string{"print", "p"}, cmdFn: printVar, helpMsg: "Evaluate a variable."}, - command{aliases: []string{"info"}, cmdFn: info, helpMsg: "Provides list of source files with symbols."}, + command{aliases: []string{"info"}, cmdFn: info, helpMsg: "Provides info about source, locals, args, or funcs."}, command{aliases: []string{"exit"}, cmdFn: nullCommand, helpMsg: "Exit the debugger."}, } @@ -261,6 +261,19 @@ func printVar(p *proctl.DebuggedProcess, args ...string) error { return nil } +func filterVariables(vars []*proctl.Variable, filter *regexp.Regexp) []string { + data := make([]string, 0, len(vars)) + for _, v := range vars { + if v == nil { + continue + } + if filter == nil || filter.Match([]byte(v.Name)) { + data = append(data, fmt.Sprintf("%s = %s", v.Name, v.Value)) + } + } + return data +} + func info(p *proctl.DebuggedProcess, args ...string) error { if len(args) == 0 { return fmt.Errorf("not enough arguments. expected info type [regex].") @@ -294,8 +307,22 @@ func info(p *proctl.DebuggedProcess, args ...string) error { } } + case "args": + vars, err := p.CurrentThread.FunctionArguments() + if err != nil { + return nil + } + data = filterVariables(vars, filter) + + case "locals": + vars, err := p.CurrentThread.LocalVariables() + if err != nil { + return nil + } + data = filterVariables(vars, filter) + default: - return fmt.Errorf("unsupported info type, must be sources or functions") + return fmt.Errorf("unsupported info type, must be sources, funcs, locals, or args") } // sort and output data diff --git a/proctl/variables.go b/proctl/variables.go index 35aea49e..d00fee70 100644 --- a/proctl/variables.go +++ b/proctl/variables.go @@ -311,7 +311,7 @@ func (thread *ThreadContext) EvalSymbol(name string) (*Variable, error) { fn := thread.Process.GoSymTable.PCToFunc(pc) if fn == nil { - return nil, fmt.Errorf("could not func function scope") + return nil, fmt.Errorf("could not find function scope") } reader := data.Reader() @@ -329,27 +329,7 @@ func (thread *ThreadContext) EvalSymbol(name string) (*Variable, error) { return nil, err } - offset, ok := entry.Val(dwarf.AttrType).(dwarf.Offset) - if !ok { - return nil, fmt.Errorf("type assertion failed") - } - - t, err := data.Type(offset) - if err != nil { - return nil, err - } - - instructions, ok := entry.Val(dwarf.AttrLocation).([]byte) - if !ok { - return nil, fmt.Errorf("type assertion failed") - } - - val, err := thread.extractValue(instructions, 0, t) - if err != nil { - return nil, err - } - - return &Variable{Name: name, Type: t.String(), Value: val}, nil + return thread.extractVariableFromEntry(entry) } // seekToFunctionEntry is basically used to seek the dwarf.Reader to @@ -380,11 +360,23 @@ func seekToFunctionEntry(name string, reader *dwarf.Reader) error { } func findDwarfEntry(name string, reader *dwarf.Reader, member bool) (*dwarf.Entry, error) { + depth := 1 for entry, err := reader.Next(); entry != nil; entry, err = reader.Next() { if err != nil { return nil, err } + if entry.Children { + depth++ + } + + if entry.Tag == 0 { + depth-- + if depth <= 0 { + return nil, fmt.Errorf("could not find symbol value for %s", name) + } + } + if member { if entry.Tag != dwarf.TagMember { continue @@ -430,6 +422,45 @@ func evaluateStructMember(thread *ThreadContext, data *dwarf.Data, reader *dwarf return &Variable{Name: strings.Join([]string{parent, member}, "."), Type: t.String(), Value: val}, nil } +// Extracts the name, type, and value of a variable from a dwarf entry +func (thread *ThreadContext) extractVariableFromEntry(entry *dwarf.Entry) (*Variable, error) { + if entry == nil { + return nil, fmt.Errorf("invalid entry") + } + + 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()) + } + + n, ok := entry.Val(dwarf.AttrName).(string) + if !ok { + return nil, fmt.Errorf("type assertion failed") + } + + offset, ok := entry.Val(dwarf.AttrType).(dwarf.Offset) + if !ok { + return nil, fmt.Errorf("type assertion failed") + } + + data := thread.Process.Dwarf + t, err := data.Type(offset) + if err != nil { + return nil, err + } + + instructions, ok := entry.Val(dwarf.AttrLocation).([]byte) + if !ok { + return nil, fmt.Errorf("type assertion failed") + } + + val, err := thread.extractValue(instructions, 0, t) + if err != nil { + return nil, err + } + + return &Variable{Name: n, Type: t.String(), Value: val}, nil +} + // Extracts the value from the instructions given in the DW_AT_location entry. // We execute the stack program described in the DW_OP_* instruction stream, and // then grab the value from the other processes memory. @@ -624,6 +655,63 @@ func (thread *ThreadContext) readMemory(addr uintptr, size uintptr) ([]byte, err return buf, nil } +// Fetches all variables of a specific type in the current function scope +func (thread *ThreadContext) variablesByTag(tag dwarf.Tag) ([]*Variable, error) { + data := thread.Process.Dwarf + + pc, err := thread.CurrentPC() + if err != nil { + return nil, err + } + + fn := thread.Process.GoSymTable.PCToFunc(pc) + if fn == nil { + return nil, fmt.Errorf("could not find function scope") + } + + reader := data.Reader() + if err = seekToFunctionEntry(fn.Name, reader); err != nil { + return nil, err + } + + vars := make([]*Variable, 0) + + for entry, err := reader.Next(); entry != nil; entry, err = reader.Next() { + if err != nil { + return nil, err + } + + // End of function + if entry.Tag == 0 { + break + } + + if entry.Tag == tag { + val, err := thread.extractVariableFromEntry(entry) + if err != nil { + return nil, err + } + + vars = append(vars, val) + } + + // Only care about top level + reader.SkipChildren() + } + + return vars, nil +} + +// LocalVariables returns all local variables from the current function scope +func (thread *ThreadContext) LocalVariables() ([]*Variable, error) { + return thread.variablesByTag(dwarf.TagVariable) +} + +// FunctionArguments returns the name, value, and type of all current function arguments +func (thread *ThreadContext) FunctionArguments() ([]*Variable, error) { + return thread.variablesByTag(dwarf.TagFormalParameter) +} + // Sets the length of a slice. func setSliceLength(ptr unsafe.Pointer, l int) { lptr := (*int)(unsafe.Pointer(uintptr(ptr) + ptrsize)) diff --git a/proctl/variables_test.go b/proctl/variables_test.go index 5ef1e6d5..03b2f8eb 100644 --- a/proctl/variables_test.go +++ b/proctl/variables_test.go @@ -2,9 +2,30 @@ package proctl import ( "path/filepath" + "sort" "testing" ) +type varTest struct { + name string + value string + varType string +} + +func assertVariable(t *testing.T, variable *Variable, expected varTest) { + if variable.Name != expected.name { + t.Fatalf("Expected %s got %s\n", expected.name, variable.Name) + } + + if variable.Type != expected.varType { + t.Fatalf("Expected %s got %s\n", expected.varType, variable.Type) + } + + if variable.Value != expected.value { + t.Fatalf("Expected %#v got %#v\n", expected.value, variable.Value) + } +} + func TestVariableEvaluation(t *testing.T) { executablePath := "../_fixtures/testvariables" @@ -13,11 +34,7 @@ func TestVariableEvaluation(t *testing.T) { t.Fatal(err) } - testcases := []struct { - name string - value string - varType string - }{ + testcases := []varTest{ {"a1", "foo", "struct string"}, {"a2", "6", "int"}, {"a3", "7.23", "float64"}, @@ -44,18 +61,123 @@ func TestVariableEvaluation(t *testing.T) { for _, tc := range testcases { variable, err := p.EvalSymbol(tc.name) - assertNoError(err, t, "Variable() returned an error") + assertNoError(err, t, "EvalSymbol() returned an error") + assertVariable(t, variable, tc) + } + }) +} - if variable.Name != tc.name { - t.Fatalf("Expected %s got %s\n", tc.name, variable.Name) +func TestVariableFunctionScoping(t *testing.T) { + executablePath := "../_fixtures/testvariables" + + fp, err := filepath.Abs(executablePath + ".go") + if err != nil { + t.Fatal(err) + } + + withTestProcess(executablePath, t, func(p *DebuggedProcess) { + pc, _, _ := p.GoSymTable.LineToPC(fp, 30) + + _, err := p.Break(uintptr(pc)) + assertNoError(err, t, "Break() returned an error") + + err = p.Continue() + assertNoError(err, t, "Continue() returned an error") + + _, err = p.EvalSymbol("a1") + assertNoError(err, t, "Unable to find variable a1") + + _, err = p.EvalSymbol("a2") + assertNoError(err, t, "Unable to find variable a1") + + // Move scopes, a1 exists here by a2 does not + pc, _, _ = p.GoSymTable.LineToPC(fp, 12) + + _, err = p.Break(uintptr(pc)) + assertNoError(err, t, "Break() returned an error") + + err = p.Continue() + assertNoError(err, t, "Continue() returned an error") + + _, err = p.EvalSymbol("a1") + assertNoError(err, t, "Unable to find variable a1") + + _, err = p.EvalSymbol("a2") + if err == nil { + t.Fatalf("Can eval out of scope variable a2") + } + }) +} + +type varArray []*Variable + +// Len is part of sort.Interface. +func (s varArray) Len() int { + return len(s) +} + +// Swap is part of sort.Interface. +func (s varArray) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +// Less is part of sort.Interface. It is implemented by calling the "by" closure in the sorter. +func (s varArray) Less(i, j int) bool { + return s[i].Name < s[j].Name +} + +func TestLocalVariables(t *testing.T) { + executablePath := "../_fixtures/testvariables" + + fp, err := filepath.Abs(executablePath + ".go") + if err != nil { + t.Fatal(err) + } + + testcases := []struct { + fn func(*ThreadContext) ([]*Variable, error) + output []varTest + }{ + {(*ThreadContext).LocalVariables, + []varTest{ + {"a1", "foo", "struct string"}, + {"a2", "6", "int"}, + {"a3", "7.23", "float64"}, + {"a4", "[2]int [1 2]", "[2]int"}, + {"a5", "len: 5 cap: 5 [1 2 3 4 5]", "struct []int"}, + {"a6", "main.FooBar {Baz: 8, Bur: word}", "main.FooBar"}, + {"a7", "*main.FooBar {Baz: 5, Bur: strum}", "*main.FooBar"}, + {"f32", "1.2", "float32"}, + {"i32", "[2]int32 [1 2]", "[2]int32"}, + {"i8", "1", "int8"}, + {"neg", "-1", "int"}}}, + {(*ThreadContext).FunctionArguments, + []varTest{ + {"bar", "main.FooBar {Baz: 10, Bur: lorem}", "main.FooBar"}, + {"baz", "bazburzum", "struct string"}}}, + } + + withTestProcess(executablePath, t, func(p *DebuggedProcess) { + pc, _, _ := p.GoSymTable.LineToPC(fp, 30) + + _, err := p.Break(uintptr(pc)) + assertNoError(err, t, "Break() returned an error") + + err = p.Continue() + assertNoError(err, t, "Continue() returned an error") + + for _, tc := range testcases { + vars, err := tc.fn(p.CurrentThread) + assertNoError(err, t, "LocalVariables() returned an error") + + sort.Sort(varArray(vars)) + + if len(tc.output) != len(vars) { + t.Fatalf("Invalid variable count. Expected %d got %d.", len(tc.output), len(vars)) } - if variable.Type != tc.varType { - t.Fatalf("Expected %s got %s\n", tc.varType, variable.Type) - } - - if variable.Value != tc.value { - t.Fatalf("Expected %#v got %#v\n", tc.value, variable.Value) + for i, variable := range vars { + assertVariable(t, variable, tc.output[i]) } } })