From da39258bec1b16f62f5a2b0bc03e5837f03bcbbf Mon Sep 17 00:00:00 2001 From: aarzilli Date: Thu, 17 Sep 2015 10:42:34 +0200 Subject: [PATCH] stack command: -full flag prints local variables and arguments of all the functions on the stack trace --- _fixtures/retstack.go | 19 +++++++ proc/proc.go | 2 +- proc/proc_test.go | 48 ++++++++++++++---- proc/stack.go | 34 +++++++++---- proc/threads.go | 2 +- proc/variables.go | 2 +- proc/variables_test.go | 2 +- service/api/types.go | 28 ++++++++-- service/client.go | 2 +- service/debugger/debugger.go | 46 ++++++++++++----- service/rpc/client.go | 6 +-- service/rpc/server.go | 5 +- service/test/integration_test.go | 87 +++++++++++++++++++++++++++++--- terminal/command.go | 67 ++++++++++++++++-------- 14 files changed, 276 insertions(+), 74 deletions(-) create mode 100644 _fixtures/retstack.go diff --git a/_fixtures/retstack.go b/_fixtures/retstack.go new file mode 100644 index 00000000..b862bb2f --- /dev/null +++ b/_fixtures/retstack.go @@ -0,0 +1,19 @@ +package main + +import "runtime" +import "fmt" + +func f() { + runtime.Breakpoint() +} + +func g() int { + runtime.Breakpoint() + return 3 +} + +func main() { + f() + n := g() + 1 + fmt.Println(n) +} diff --git a/proc/proc.go b/proc/proc.go index 9312ad79..8359208d 100644 --- a/proc/proc.go +++ b/proc/proc.go @@ -776,7 +776,7 @@ func (dbp *Process) ConvertEvalScope(gid, frame int) (*EvalScope, error) { return nil, fmt.Errorf("Frame %d does not exist in goroutine %d", frame, gid) } - out.PC, out.CFA = locs[frame].PC, locs[frame].CFA + out.PC, out.CFA = locs[frame].Current.PC, locs[frame].CFA return &out, nil } diff --git a/proc/proc_test.go b/proc/proc_test.go index 3341e133..8cb29382 100644 --- a/proc/proc_test.go +++ b/proc/proc_test.go @@ -534,18 +534,17 @@ type loc struct { func (l1 *loc) match(l2 Stackframe) bool { if l1.line >= 0 { - if l1.line != l2.Line-1 { + if l1.line != l2.Call.Line { return false } } - - return l1.fn == l2.Fn.Name + return l1.fn == l2.Call.Fn.Name } func TestStacktrace(t *testing.T) { stacks := [][]loc{ - []loc{{3, "main.stacktraceme"}, {8, "main.func1"}, {16, "main.main"}}, - []loc{{3, "main.stacktraceme"}, {8, "main.func1"}, {12, "main.func2"}, {17, "main.main"}}, + []loc{{4, "main.stacktraceme"}, {8, "main.func1"}, {16, "main.main"}}, + []loc{{4, "main.stacktraceme"}, {8, "main.func1"}, {12, "main.func2"}, {17, "main.main"}}, } withTestProcess("stacktraceprog", t, func(p *Process, fixture protest.Fixture) { bp, err := setFunctionBreakpoint(p, "main.stacktraceme") @@ -560,7 +559,10 @@ func TestStacktrace(t *testing.T) { t.Fatalf("Wrong stack trace size %d %d\n", len(locations), len(stacks[i])+2) } - t.Logf("Stacktrace %d: %v\n", i, locations) + t.Logf("Stacktrace %d:\n", i) + for i := range locations { + t.Logf("\t%s:%d\n", locations[i].Call.File, locations[i].Call.Line) + } for j := range stacks[i] { if !stacks[i][j].match(locations[j]) { @@ -574,6 +576,32 @@ func TestStacktrace(t *testing.T) { }) } +func TestStacktrace2(t *testing.T) { + withTestProcess("retstack", t, func(p *Process, fixture protest.Fixture) { + assertNoError(p.Continue(), t, "Continue()") + + locations, err := p.CurrentThread.Stacktrace(40) + assertNoError(err, t, "Stacktrace()") + if !stackMatch([]loc{loc{-1, "main.f"}, loc{16, "main.main"}}, locations) { + for i := range locations { + t.Logf("\t%s:%d [%s]\n", locations[i].Call.File, locations[i].Call.Line, locations[i].Call.Fn.Name) + } + t.Fatalf("Stack error at main.f()\n", locations) + } + + assertNoError(p.Continue(), t, "Continue()") + locations, err = p.CurrentThread.Stacktrace(40) + assertNoError(err, t, "Stacktrace()") + if !stackMatch([]loc{loc{-1, "main.g"}, loc{17, "main.main"}}, locations) { + for i := range locations { + t.Logf("\t%s:%d [%s]\n", locations[i].Call.File, locations[i].Call.Line, locations[i].Call.Fn.Name) + } + t.Fatalf("Stack error at main.g()\n", locations) + } + }) + +} + func stackMatch(stack []loc, locations []Stackframe) bool { if len(stack) > len(locations) { return false @@ -587,7 +615,7 @@ func stackMatch(stack []loc, locations []Stackframe) bool { } func TestStacktraceGoroutine(t *testing.T) { - mainStack := []loc{{11, "main.stacktraceme"}, {21, "main.main"}} + mainStack := []loc{{12, "main.stacktraceme"}, {21, "main.main"}} agoroutineStack := []loc{{-1, "runtime.gopark"}, {-1, "runtime.goparkunlock"}, {-1, "runtime.chansend"}, {-1, "runtime.chansend1"}, {8, "main.agoroutine"}} withTestProcess("goroutinestackprog", t, func(p *Process, fixture protest.Fixture) { @@ -616,10 +644,10 @@ func TestStacktraceGoroutine(t *testing.T) { t.Logf("Non-goroutine stack: %d (%d)", i, len(locations)) for i := range locations { name := "" - if locations[i].Fn != nil { - name = locations[i].Fn.Name + if locations[i].Call.Fn != nil { + name = locations[i].Call.Fn.Name } - t.Logf("\t%s:%d %s\n", locations[i].File, locations[i].Line, name) + t.Logf("\t%s:%d %s\n", locations[i].Call.File, locations[i].Call.Line, name) } } } diff --git a/proc/stack.go b/proc/stack.go index 0454089e..df0fcc9c 100644 --- a/proc/stack.go +++ b/proc/stack.go @@ -14,9 +14,16 @@ func (nra NoReturnAddr) Error() string { } type Stackframe struct { - Location - CFA int64 - Ret uint64 + // Address the function above this one on the call stack will return to + Current Location + // Address of the call instruction for the function above on the call stack. + Call Location + CFA int64 + Ret uint64 +} + +func (frame *Stackframe) Scope(thread *Thread) *EvalScope { + return &EvalScope{Thread: thread, PC: frame.Current.PC, CFA: frame.CFA} } // Takes an offset from RSP and returns the address of the @@ -27,9 +34,9 @@ func (thread *Thread) ReturnAddress() (uint64, error) { return 0, err } if len(locations) < 2 { - return 0, NoReturnAddr{locations[0].Fn.BaseName()} + return 0, NoReturnAddr{locations[0].Current.Fn.BaseName()} } - return locations[1].PC, nil + return locations[1].Current.PC, nil } // Returns the stack trace for thread. @@ -63,7 +70,7 @@ func (n NullAddrError) Error() string { return "NULL address" } -func (dbp *Process) frameInfo(pc, sp uint64) (Stackframe, error) { +func (dbp *Process) frameInfo(pc, sp uint64, top bool) (Stackframe, error) { f, l, fn := dbp.PCToLine(pc) fde, err := dbp.frameEntries.FDEForPC(pc) if err != nil { @@ -80,18 +87,25 @@ func (dbp *Process) frameInfo(pc, sp uint64) (Stackframe, error) { if err != nil { return Stackframe{}, err } - return Stackframe{Location: Location{PC: pc, File: f, Line: l, Fn: fn}, CFA: cfa, Ret: binary.LittleEndian.Uint64(data)}, nil + r := Stackframe{Current: Location{PC: pc, File: f, Line: l, Fn: fn}, CFA: cfa, Ret: binary.LittleEndian.Uint64(data)} + if !top { + r.Call.File, r.Call.Line, r.Call.Fn = dbp.PCToLine(pc - 1) + r.Call.PC, _, _ = dbp.goSymTable.LineToPC(r.Call.File, r.Call.Line) + } else { + r.Call = r.Current + } + return r, nil } func (dbp *Process) stacktrace(pc, sp uint64, depth int) ([]Stackframe, error) { frames := make([]Stackframe, 0, depth+1) for i := 0; i < depth+1; i++ { - frame, err := dbp.frameInfo(pc, sp) + frame, err := dbp.frameInfo(pc, sp, i == 0) if err != nil { return nil, err } - if frame.Fn == nil { + if frame.Current.Fn == nil { break } frames = append(frames, frame) @@ -99,7 +113,7 @@ func (dbp *Process) stacktrace(pc, sp uint64, depth int) ([]Stackframe, error) { break } // Look for "top of stack" functions. - if frame.Fn.Name == "runtime.goexit" || frame.Fn.Name == "runtime.rt0_go" { + if frame.Current.Fn.Name == "runtime.goexit" || frame.Current.Fn.Name == "runtime.rt0_go" { break } diff --git a/proc/threads.go b/proc/threads.go index 8ce4a12f..fd8944d8 100644 --- a/proc/threads.go +++ b/proc/threads.go @@ -327,5 +327,5 @@ func (thread *Thread) Scope() (*EvalScope, error) { if err != nil { return nil, err } - return &EvalScope{Thread: thread, PC: locations[0].PC, CFA: locations[0].CFA}, nil + return locations[0].Scope(thread), nil } diff --git a/proc/variables.go b/proc/variables.go index f6b5ff0b..9ca22b4a 100644 --- a/proc/variables.go +++ b/proc/variables.go @@ -104,7 +104,7 @@ func (g *G) chanRecvReturnAddr(dbp *Process) (uint64, error) { return 0, err } topLoc := locs[len(locs)-1] - return topLoc.PC, nil + return topLoc.Current.PC, nil } // NoGError returned when a G could not be found diff --git a/proc/variables_test.go b/proc/variables_test.go index 900a8e24..cdd6537b 100644 --- a/proc/variables_test.go +++ b/proc/variables_test.go @@ -252,7 +252,7 @@ func TestFrameEvaluation(t *testing.T) { frames, err := p.GoroutineStacktrace(g, 10) assertNoError(err, t, "GoroutineStacktrace()") for i := range frames { - if frames[i].Fn != nil && frames[i].Fn.Name == "main.agoroutine" { + if frames[i].Call.Fn != nil && frames[i].Call.Fn.Name == "main.agoroutine" { frame = i break } diff --git a/service/api/types.go b/service/api/types.go index bcb5afd0..3877cb54 100644 --- a/service/api/types.go +++ b/service/api/types.go @@ -65,6 +65,26 @@ type Location struct { Function *Function `json:"function,omitempty"` } +type Stackframe struct { + Location + Locals []Variable + Arguments []Variable +} + +func (frame *Stackframe) Var(name string) *Variable { + for i := range frame.Locals { + if frame.Locals[i].Name == name { + return &frame.Locals[i] + } + } + for i := range frame.Arguments { + if frame.Arguments[i].Name == name { + return &frame.Arguments[i] + } + } + return nil +} + // Function represents thread-scoped function information. type Function struct { // Name is the function name. @@ -114,10 +134,10 @@ type DebuggerCommand struct { // Informations about the current breakpoint type BreakpointInfo struct { - Stacktrace []Location `json:"stacktrace,omitempty"` - Goroutine *Goroutine `json:"goroutine,omitempty"` - Variables []Variable `json:"variables,omitempty"` - Arguments []Variable `json:"arguments,omitempty"` + Stacktrace []Stackframe `json:"stacktrace,omitempty"` + Goroutine *Goroutine `json:"goroutine,omitempty"` + Variables []Variable `json:"variables,omitempty"` + Arguments []Variable `json:"arguments,omitempty"` } type EvalScope struct { diff --git a/service/client.go b/service/client.go index fcb0a67a..410993e3 100644 --- a/service/client.go +++ b/service/client.go @@ -68,7 +68,7 @@ type Client interface { ListGoroutines() ([]*api.Goroutine, error) // Returns stacktrace - Stacktrace(goroutineId, depth int) ([]api.Location, error) + Stacktrace(goroutineId, depth int, full bool) ([]api.Stackframe, error) // Returns whether we attached to a running process or not AttachedToExistingProcess() bool diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go index 4e52041b..0f29d687 100644 --- a/service/debugger/debugger.go +++ b/service/debugger/debugger.go @@ -288,7 +288,10 @@ func (d *Debugger) collectBreakpointInformation(state *api.DebuggerState) error if err != nil { return err } - bpi.Stacktrace = convertStacktrace(rawlocs) + bpi.Stacktrace, err = d.convertStacktrace(rawlocs, false) + if err != nil { + return err + } } s, err := d.process.CurrentThread.Scope() @@ -386,6 +389,14 @@ func (d *Debugger) Registers(threadID int) (string, error) { return regs.String(), err } +func convertVars(pv []*proc.Variable) []api.Variable { + vars := make([]api.Variable, 0, len(pv)) + for _, v := range pv { + vars = append(vars, api.ConvertVar(v)) + } + return vars +} + func (d *Debugger) LocalVariables(scope api.EvalScope) ([]api.Variable, error) { s, err := d.process.ConvertEvalScope(scope.GoroutineID, scope.Frame) if err != nil { @@ -395,11 +406,7 @@ func (d *Debugger) LocalVariables(scope api.EvalScope) ([]api.Variable, error) { if err != nil { return nil, err } - vars := make([]api.Variable, 0, len(pv)) - for _, v := range pv { - vars = append(vars, api.ConvertVar(v)) - } - return vars, err + return convertVars(pv), err } func (d *Debugger) FunctionArguments(scope api.EvalScope) ([]api.Variable, error) { @@ -447,7 +454,7 @@ func (d *Debugger) Goroutines() ([]*api.Goroutine, error) { return goroutines, err } -func (d *Debugger) Stacktrace(goroutineId, depth int) ([]api.Location, error) { +func (d *Debugger) Stacktrace(goroutineId, depth int, full bool) ([]api.Stackframe, error) { var rawlocs []proc.Stackframe var err error @@ -483,17 +490,30 @@ func (d *Debugger) Stacktrace(goroutineId, depth int) ([]api.Location, error) { } } - return convertStacktrace(rawlocs), nil + return d.convertStacktrace(rawlocs, full) } -func convertStacktrace(rawlocs []proc.Stackframe) []api.Location { - locations := make([]api.Location, 0, len(rawlocs)) +func (d *Debugger) convertStacktrace(rawlocs []proc.Stackframe, full bool) ([]api.Stackframe, error) { + locations := make([]api.Stackframe, 0, len(rawlocs)) for i := range rawlocs { - rawlocs[i].Line-- - locations = append(locations, api.ConvertLocation(rawlocs[i].Location)) + frame := api.Stackframe{Location: api.ConvertLocation(rawlocs[i].Call)} + if full { + scope := rawlocs[i].Scope(d.process.CurrentThread) + lv, err := scope.LocalVariables() + if err != nil { + return nil, err + } + av, err := scope.FunctionArguments() + if err != nil { + return nil, err + } + frame.Locals = convertVars(lv) + frame.Arguments = convertVars(av) + } + locations = append(locations, frame) } - return locations + return locations, nil } func (d *Debugger) FindLocation(scope api.EvalScope, locStr string) ([]api.Location, error) { diff --git a/service/rpc/client.go b/service/rpc/client.go index de2686fc..5561cc6b 100644 --- a/service/rpc/client.go +++ b/service/rpc/client.go @@ -203,9 +203,9 @@ func (c *RPCClient) ListGoroutines() ([]*api.Goroutine, error) { return goroutines, err } -func (c *RPCClient) Stacktrace(goroutineId, depth int) ([]api.Location, error) { - var locations []api.Location - err := c.call("StacktraceGoroutine", &StacktraceGoroutineArgs{Id: goroutineId, Depth: depth}, &locations) +func (c *RPCClient) Stacktrace(goroutineId, depth int, full bool) ([]api.Stackframe, error) { + var locations []api.Stackframe + err := c.call("StacktraceGoroutine", &StacktraceGoroutineArgs{Id: goroutineId, Depth: depth, Full: full}, &locations) return locations, err } diff --git a/service/rpc/server.go b/service/rpc/server.go index 952756df..242419af 100644 --- a/service/rpc/server.go +++ b/service/rpc/server.go @@ -113,10 +113,11 @@ func (s *RPCServer) GetBreakpoint(id int, breakpoint *api.Breakpoint) error { type StacktraceGoroutineArgs struct { Id int Depth int + Full bool } -func (s *RPCServer) StacktraceGoroutine(args *StacktraceGoroutineArgs, locations *[]api.Location) error { - locs, err := s.debugger.Stacktrace(args.Id, args.Depth) +func (s *RPCServer) StacktraceGoroutine(args *StacktraceGoroutineArgs, locations *[]api.Stackframe) error { + locs, err := s.debugger.Stacktrace(args.Id, args.Depth, args.Full) if err != nil { return err } diff --git a/service/test/integration_test.go b/service/test/integration_test.go index 9129c42d..3814560f 100644 --- a/service/test/integration_test.go +++ b/service/test/integration_test.go @@ -20,6 +20,14 @@ func init() { runtime.GOMAXPROCS(2) } +func assertNoError(err error, t *testing.T, s string) { + if err != nil { + _, file, line, _ := runtime.Caller(1) + fname := filepath.Base(file) + t.Fatalf("failed assertion at %s:%d: %s - %s\n", fname, line, s, err) + } +} + func TestMain(m *testing.M) { os.Exit(protest.RunTestsWithFixtures(m)) } @@ -607,9 +615,7 @@ func TestClientServer_EvalVariable(t *testing.T) { withTestClient("testvariables", t, func(c service.Client) { fp := testProgPath(t, "testvariables") _, err := c.CreateBreakpoint(&api.Breakpoint{File: fp, Line: 59}) - if err != nil { - t.Fatalf("CreateBreakpoint(): %v", err) - } + assertNoError(err, t, "CreateBreakpoint()") state := <-c.Continue() @@ -618,9 +624,7 @@ func TestClientServer_EvalVariable(t *testing.T) { } var1, err := c.EvalVariable(api.EvalScope{-1, 0}, "a1") - if err != nil { - t.Fatalf("EvalVariable(): %v", err) - } + assertNoError(err, t, "EvalVariable") t.Logf("var1: <%s>", var1.Value) @@ -629,3 +633,74 @@ func TestClientServer_EvalVariable(t *testing.T) { } }) } + +func TestClientServer_FullStacktrace(t *testing.T) { + withTestClient("goroutinestackprog", t, func(c service.Client) { + _, err := c.CreateBreakpoint(&api.Breakpoint{FunctionName: "main.stacktraceme", Line: -1}) + assertNoError(err, t, "CreateBreakpoint()") + state := <-c.Continue() + if state.Err != nil { + t.Fatalf("Continue(): %v\n", state.Err) + } + + gs, err := c.ListGoroutines() + assertNoError(err, t, "GoroutinesInfo()") + found := make([]bool, 10) + for _, g := range gs { + frames, err := c.Stacktrace(g.ID, 10, true) + assertNoError(err, t, fmt.Sprintf("Stacktrace(%d)", g.ID)) + for i, frame := range frames { + if frame.Function == nil { + continue + } + if frame.Function.Name != "main.agoroutine" { + continue + } + t.Logf("frame %d: %v", i, frame) + for _, arg := range frame.Arguments { + if arg.Name != "i" { + continue + } + n, err := strconv.Atoi(arg.Value) + assertNoError(err, t, fmt.Sprintf("Wrong value for i in goroutine %d (%s)", g.ID, arg.Value)) + found[n] = true + } + } + } + + for i := range found { + if !found[i] { + t.Fatalf("Goroutine %d not found", i) + } + } + + state = <-c.Continue() + if state.Err != nil { + t.Fatalf("Continue(): %v\n", state.Err) + } + + frames, err := c.Stacktrace(-1, 10, true) + assertNoError(err, t, "Stacktrace") + + cur := 3 + for i, frame := range frames { + if i == 0 { + continue + } + t.Logf("frame %d: %v", i, frame) + v := frame.Var("n") + if v == nil { + t.Fatalf("Could not find value of variable n in frame %d", i) + } + n, err := strconv.Atoi(v.Value) + assertNoError(err, t, fmt.Sprintf("Wrong value for n: %s", v.Value)) + if n != cur { + t.Fatalf("Expected value %d got %d", cur, n) + } + cur-- + if cur < 0 { + break + } + } + }) +} diff --git a/terminal/command.go b/terminal/command.go index 430b3e9b..b18de250 100644 --- a/terminal/command.go +++ b/terminal/command.go @@ -6,6 +6,7 @@ import ( "bufio" "fmt" "io" + "math" "os" "regexp" "sort" @@ -73,8 +74,8 @@ func DebugCommands(client service.Client) *Commands { {aliases: []string{"vars"}, cmdFn: filterSortAndOutput(vars), helpMsg: "Print package variables, optionally filtered by a regexp."}, {aliases: []string{"regs"}, cmdFn: regs, helpMsg: "Print contents of CPU registers."}, {aliases: []string{"exit", "quit", "q"}, cmdFn: exitCommand, helpMsg: "Exit the debugger."}, - {aliases: []string{"stack", "bt"}, cmdFn: stackCommand, helpMsg: "stack [ []]. Prints stack."}, {aliases: []string{"list", "ls"}, cmdFn: listCommand, helpMsg: "list . Show source around current point or provided linespec."}, + {aliases: []string{"stack", "bt"}, cmdFn: stackCommand, helpMsg: "stack [-] [-full] []. Prints stack."}, {aliases: []string{"frame"}, cmdFn: frame, helpMsg: "Sets current stack frame (0 is the top of the stack)"}, } @@ -313,7 +314,7 @@ func scopePrefix(client service.Client, cmdname string, pargs ...string) error { i++ case "list", "ls": frame, gid := scope.Frame, scope.GoroutineID - locs, err := client.Stacktrace(gid, frame) + locs, err := client.Stacktrace(gid, frame, false) if err != nil { return err } @@ -627,27 +628,27 @@ func stackCommand(client service.Client, args ...string) error { goroutineid := -1 depth := 10 + full := false - switch len(args) { - case 0: - // nothing to do - case 2: - goroutineid, err = strconv.Atoi(args[1]) - if err != nil { - return fmt.Errorf("Wrong argument: expected integer") - } - fallthrough - case 1: - depth, err = strconv.Atoi(args[0]) - if err != nil { - return fmt.Errorf("Wrong argument: expected integer") + for i := range args { + if args[i] == "-full" { + full = true + } else if args[i][0] == '-' { + n, err := strconv.Atoi(args[i][1:]) + if err != nil { + return fmt.Errorf("unknown option: %s", args[i]) + } + depth = n + } else { + n, err := strconv.Atoi(args[i]) + if err != nil { + return fmt.Errorf("goroutine id must be a number") + } + goroutineid = n } - - default: - return fmt.Errorf("Wrong number of arguments to stack") } - stack, err := client.Stacktrace(goroutineid, depth) + stack, err := client.Stacktrace(goroutineid, depth, full) if err != nil { return err } @@ -676,13 +677,37 @@ func listCommand(client service.Client, args ...string) error { return nil } -func printStack(stack []api.Location, ind string) { +func digits(n int) int { + return int(math.Floor(math.Log10(float64(n)))) + 1 +} + +func spaces(n int) string { + spaces := make([]byte, n) + for i := range spaces { + spaces[i] = ' ' + } + return string(spaces) +} + +func printStack(stack []api.Stackframe, ind string) { + d := digits(len(stack) - 1) + fmtstr := "%s%" + strconv.Itoa(d) + "d 0x%016x in %s\n" + s := spaces(d + 2 + len(ind)) + for i := range stack { name := "(nil)" if stack[i].Function != nil { name = stack[i].Function.Name } - fmt.Printf("%s%d. %s %s:%d (%#v)\n", ind, i, name, shortenFilePath(stack[i].File), stack[i].Line, stack[i].PC) + fmt.Printf(fmtstr, ind, i, stack[i].PC, name) + fmt.Printf("%sat %s:%d\n", s, shortenFilePath(stack[i].File), stack[i].Line) + + for j := range stack[i].Arguments { + fmt.Printf("%s %s = %s\n", s, stack[i].Arguments[j].Name, stack[i].Arguments[j].Value) + } + for j := range stack[i].Locals { + fmt.Printf("%s %s = %s\n", s, stack[i].Locals[j].Name, stack[i].Locals[j].Value) + } } } -- GitLab