Added info locals and info args commands
This commit is contained in:
parent
cc8563a2a2
commit
07940dc59e
@ -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:
|
* `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
|
* `sources` - Prings the path of all source files
|
||||||
* `funcs` - Prings the name of all defined functions
|
* `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.
|
* `exit` - Exit the debugger.
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ func barfoo() {
|
|||||||
fmt.Println(a1)
|
fmt.Println(a1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func foobar(baz string) {
|
func foobar(baz string, bar FooBar) {
|
||||||
var (
|
var (
|
||||||
a1 = "foo"
|
a1 = "foo"
|
||||||
a2 = 6
|
a2 = 6
|
||||||
@ -28,9 +28,9 @@ func foobar(baz string) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
barfoo()
|
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() {
|
func main() {
|
||||||
foobar("bazburzum")
|
foobar("bazburzum", FooBar{Baz: 10, Bur: "lorem"})
|
||||||
}
|
}
|
||||||
|
@ -54,7 +54,7 @@ func DebugCommands() *Commands {
|
|||||||
command{aliases: []string{"clear"}, cmdFn: clear, helpMsg: "Deletes breakpoint."},
|
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{"goroutines"}, cmdFn: goroutines, helpMsg: "Print out info for every goroutine."},
|
||||||
command{aliases: []string{"print", "p"}, cmdFn: printVar, helpMsg: "Evaluate a variable."},
|
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."},
|
command{aliases: []string{"exit"}, cmdFn: nullCommand, helpMsg: "Exit the debugger."},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -261,6 +261,19 @@ func printVar(p *proctl.DebuggedProcess, args ...string) error {
|
|||||||
return nil
|
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 {
|
func info(p *proctl.DebuggedProcess, args ...string) error {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return fmt.Errorf("not enough arguments. expected info type [regex].")
|
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:
|
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
|
// sort and output data
|
||||||
|
@ -311,7 +311,7 @@ func (thread *ThreadContext) EvalSymbol(name string) (*Variable, error) {
|
|||||||
|
|
||||||
fn := thread.Process.GoSymTable.PCToFunc(pc)
|
fn := thread.Process.GoSymTable.PCToFunc(pc)
|
||||||
if fn == nil {
|
if fn == nil {
|
||||||
return nil, fmt.Errorf("could not func function scope")
|
return nil, fmt.Errorf("could not find function scope")
|
||||||
}
|
}
|
||||||
|
|
||||||
reader := data.Reader()
|
reader := data.Reader()
|
||||||
@ -329,27 +329,7 @@ func (thread *ThreadContext) EvalSymbol(name string) (*Variable, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
offset, ok := entry.Val(dwarf.AttrType).(dwarf.Offset)
|
return thread.extractVariableFromEntry(entry)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// seekToFunctionEntry is basically used to seek the dwarf.Reader to
|
// 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) {
|
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() {
|
for entry, err := reader.Next(); entry != nil; entry, err = reader.Next() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 member {
|
||||||
if entry.Tag != dwarf.TagMember {
|
if entry.Tag != dwarf.TagMember {
|
||||||
continue
|
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
|
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.
|
// 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
|
// We execute the stack program described in the DW_OP_* instruction stream, and
|
||||||
// then grab the value from the other processes memory.
|
// 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
|
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.
|
// Sets the length of a slice.
|
||||||
func setSliceLength(ptr unsafe.Pointer, l int) {
|
func setSliceLength(ptr unsafe.Pointer, l int) {
|
||||||
lptr := (*int)(unsafe.Pointer(uintptr(ptr) + ptrsize))
|
lptr := (*int)(unsafe.Pointer(uintptr(ptr) + ptrsize))
|
||||||
|
@ -2,9 +2,30 @@ package proctl
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
"testing"
|
"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) {
|
func TestVariableEvaluation(t *testing.T) {
|
||||||
executablePath := "../_fixtures/testvariables"
|
executablePath := "../_fixtures/testvariables"
|
||||||
|
|
||||||
@ -13,11 +34,7 @@ func TestVariableEvaluation(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
testcases := []struct {
|
testcases := []varTest{
|
||||||
name string
|
|
||||||
value string
|
|
||||||
varType string
|
|
||||||
}{
|
|
||||||
{"a1", "foo", "struct string"},
|
{"a1", "foo", "struct string"},
|
||||||
{"a2", "6", "int"},
|
{"a2", "6", "int"},
|
||||||
{"a3", "7.23", "float64"},
|
{"a3", "7.23", "float64"},
|
||||||
@ -44,18 +61,123 @@ func TestVariableEvaluation(t *testing.T) {
|
|||||||
|
|
||||||
for _, tc := range testcases {
|
for _, tc := range testcases {
|
||||||
variable, err := p.EvalSymbol(tc.name)
|
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 {
|
func TestVariableFunctionScoping(t *testing.T) {
|
||||||
t.Fatalf("Expected %s got %s\n", tc.name, variable.Name)
|
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 {
|
for i, variable := range vars {
|
||||||
t.Fatalf("Expected %s got %s\n", tc.varType, variable.Type)
|
assertVariable(t, variable, tc.output[i])
|
||||||
}
|
|
||||||
|
|
||||||
if variable.Value != tc.value {
|
|
||||||
t.Fatalf("Expected %#v got %#v\n", tc.value, variable.Value)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user