From 07473f04c56ddeb0bf8e142fbccc68c2951e5255 Mon Sep 17 00:00:00 2001 From: aarzilli Date: Wed, 17 Jun 2015 19:11:57 +0200 Subject: [PATCH] Implement stack command Finishes #63 #64 --- _fixtures/goroutinestackprog.go | 25 ++++++++ _fixtures/stacktraceprog.go | 18 ++++++ proc/proc.go | 18 +++++- proc/proc_test.go | 110 ++++++++++++++++++++++++++++++++ proc/stack.go | 53 ++++++++++----- proc/threads.go | 1 + proc/variables.go | 5 +- service/api/conversions.go | 48 ++++++++------ service/api/types.go | 7 ++ service/client.go | 3 + service/debugger/debugger.go | 46 +++++++++++++ service/rest/client.go | 9 +++ service/rest/server.go | 24 +++++++ terminal/command.go | 44 ++++++++++++- 14 files changed, 371 insertions(+), 40 deletions(-) create mode 100644 _fixtures/goroutinestackprog.go create mode 100644 _fixtures/stacktraceprog.go diff --git a/_fixtures/goroutinestackprog.go b/_fixtures/goroutinestackprog.go new file mode 100644 index 00000000..ade776bd --- /dev/null +++ b/_fixtures/goroutinestackprog.go @@ -0,0 +1,25 @@ +package main + +import "runtime" + +const N = 10 + +func agoroutine(done chan<- struct{}) { + done <- struct{}{} +} + +func stacktraceme() { + return +} + +func main() { + done := make(chan struct{}) + for i := 0; i < N; i++ { + go agoroutine(done) + } + runtime.Gosched() + stacktraceme() + for i := 0; i < N; i++ { + <-done + } +} diff --git a/_fixtures/stacktraceprog.go b/_fixtures/stacktraceprog.go new file mode 100644 index 00000000..09fbe1c1 --- /dev/null +++ b/_fixtures/stacktraceprog.go @@ -0,0 +1,18 @@ +package main + +func stacktraceme() { + return +} + +func func1() { + stacktraceme() +} + +func func2(f func()) { + f() +} + +func main() { + func1() + func2(func1) +} diff --git a/proc/proc.go b/proc/proc.go index e6cd2a55..f51f377e 100644 --- a/proc/proc.go +++ b/proc/proc.go @@ -424,10 +424,21 @@ func (dbp *DebuggedProcess) SwitchThread(tid int) error { // Delve cares about from the internal runtime G structure. func (dbp *DebuggedProcess) GoroutinesInfo() ([]*G, error) { var ( - allg []*G - rdr = dbp.DwarfReader() + threadg = map[int]*Thread{} + allg []*G + rdr = dbp.DwarfReader() ) + for i := range dbp.Threads { + if dbp.Threads[i].blocked() { + continue + } + g, _ := dbp.Threads[i].getG() + if g != nil { + threadg[g.Id] = dbp.Threads[i] + } + } + addr, err := rdr.AddrFor("runtime.allglen") if err != nil { return nil, err @@ -451,6 +462,9 @@ func (dbp *DebuggedProcess) GoroutinesInfo() ([]*G, error) { if err != nil { return nil, err } + if thread, allocated := threadg[g.Id]; allocated { + g.thread = thread + } allg = append(allg, g) } return allg, nil diff --git a/proc/proc_test.go b/proc/proc_test.go index 6fd427d4..894dc30c 100644 --- a/proc/proc_test.go +++ b/proc/proc_test.go @@ -409,3 +409,113 @@ func TestSwitchThread(t *testing.T) { } }) } + +type loc struct { + line int + fn string +} + +func (l1 *loc) match(l2 Location) bool { + if l1.line >= 0 { + if l1.line != l2.Line-1 { + return false + } + } + + return l1.fn == l2.Fn.Name +} + +func TestStacktrace(t *testing.T) { + stacks := [][]loc{ + []loc{{8, "main.func1"}, {16, "main.main"}}, + []loc{{8, "main.func1"}, {12, "main.func2"}, {17, "main.main"}}, + } + withTestProcess("stacktraceprog", t, func(p *DebuggedProcess, fixture protest.Fixture) { + bp, err := p.BreakByLocation("main.stacktraceme") + assertNoError(err, t, "BreakByLocation()") + + for i := range stacks { + assertNoError(p.Continue(), t, "Continue()") + locations, err := p.CurrentThread.Stacktrace(40) + assertNoError(err, t, "Stacktrace()") + + if len(locations) != len(stacks[i])+2 { + t.Fatalf("Wrong stack trace size %d %d\n", len(locations), len(stacks[i])+2) + } + + for j := range stacks[i] { + if !stacks[i][j].match(locations[j]) { + t.Fatalf("Wrong stack trace pos %d\n", j) + } + } + } + + p.Clear(bp.Addr) + p.Continue() + }) +} + +func stackMatch(stack []loc, locations []Location) bool { + if len(stack) > len(locations) { + return false + } + for i := range stack { + if !stack[i].match(locations[i]) { + return false + } + } + return true +} + +func TestStacktraceGoroutine(t *testing.T) { + mainStack := []loc{{21, "main.main"}} + agoroutineStack := []loc{{-1, "runtime.goparkunlock"}, {-1, "runtime.chansend"}, {-1, "runtime.chansend1"}, {8, "main.agoroutine"}} + + withTestProcess("goroutinestackprog", t, func(p *DebuggedProcess, fixture protest.Fixture) { + bp, err := p.BreakByLocation("main.stacktraceme") + assertNoError(err, t, "BreakByLocation()") + + assertNoError(p.Continue(), t, "Continue()") + + gs, err := p.GoroutinesInfo() + assertNoError(err, t, "GoroutinesInfo") + + agoroutineCount := 0 + mainCount := 0 + + for _, g := range gs { + locations, _ := p.GoroutineStacktrace(g, 40) + assertNoError(err, t, "GoroutineStacktrace()") + + if stackMatch(mainStack, locations) { + mainCount++ + } + + if stackMatch(agoroutineStack, locations) { + agoroutineCount++ + } else { + t.Logf("Non-goroutine stack: (%d)", len(locations)) + for i := range locations { + name := "" + if locations[i].Fn != nil { + name = locations[i].Fn.Name + } + t.Logf("\t%s:%d %s\n", locations[i].File, locations[i].Line, name) + } + + } + + } + + if mainCount != 1 { + t.Fatalf("Main goroutine stack not found") + } + + if agoroutineCount != 10 { + t.Fatalf("Goroutine stacks not found (%d)", agoroutineCount) + } + + p.Clear(bp.Addr) + p.Continue() + }) +} diff --git a/proc/stack.go b/proc/stack.go index 1bad1e47..aeb3673c 100644 --- a/proc/stack.go +++ b/proc/stack.go @@ -1,29 +1,45 @@ package proc import ( - "debug/gosym" "encoding/binary" ) -type stackLocation struct { - addr uint64 - file string - line int - fn *gosym.Func -} - // Takes an offset from RSP and returns the address of the // instruction the currect function is going to return to. func (thread *Thread) ReturnAddress() (uint64, error) { - regs, err := thread.Registers() + locations, err := thread.Stacktrace(1) if err != nil { return 0, err } - locations, err := thread.dbp.stacktrace(regs.PC(), regs.SP(), 1) + return locations[0].PC, nil +} + +// Returns the stack trace for thread +// Note that it doesn't include the current frame and the locations in the array are return addresses not call addresses +func (thread *Thread) Stacktrace(depth int) ([]Location, error) { + regs, err := thread.Registers() if err != nil { - return 0, err + return nil, err + } + locations, err := thread.dbp.stacktrace(regs.PC(), regs.SP(), depth) + if err != nil { + return nil, err } - return locations[0].addr, nil + return locations, nil +} + +// Returns the stack trace for a goroutine +// Note that it doesn't include the current frame and the locations in the array are return addresses not call addresses +func (dbp *DebuggedProcess) GoroutineStacktrace(g *G, depth int) ([]Location, error) { + if g.thread != nil { + return g.thread.Stacktrace(depth) + } + return dbp.stacktrace(g.PC, g.SP, depth) +} + +func (dbp *DebuggedProcess) GoroutineLocation(g *G) *Location { + f, l, fn := dbp.PCToLine(g.PC) + return &Location{PC: g.PC, File: f, Line: l, Fn: fn} } type NullAddrError struct{} @@ -32,12 +48,12 @@ func (n NullAddrError) Error() string { return "NULL address" } -func (dbp *DebuggedProcess) stacktrace(pc, sp uint64, depth int) ([]stackLocation, error) { +func (dbp *DebuggedProcess) stacktrace(pc, sp uint64, depth int) ([]Location, error) { var ( ret = pc data = make([]byte, dbp.arch.PtrSize()) btoffset int64 - locations []stackLocation + locations []Location retaddr uintptr ) for i := int64(0); i < int64(depth); i++ { @@ -55,8 +71,15 @@ func (dbp *DebuggedProcess) stacktrace(pc, sp uint64, depth int) ([]stackLocatio return nil, err } ret = binary.LittleEndian.Uint64(data) + if ret <= 0 { + break + } f, l, fn := dbp.goSymTable.PCToLine(ret) - locations = append(locations, stackLocation{addr: ret, file: f, line: l, fn: fn}) + locations = append(locations, Location{PC: ret, File: f, Line: l, Fn: fn}) + if fn != nil && fn.Name == "runtime.goexit" { + break + } + } return locations, nil } diff --git a/proc/threads.go b/proc/threads.go index c22c1b8b..f2a431b3 100644 --- a/proc/threads.go +++ b/proc/threads.go @@ -308,5 +308,6 @@ func (thread *Thread) getG() (g *G, err error) { return nil, err } g, err = parseG(thread, regs.CX(), false) + g.thread = thread return } diff --git a/proc/variables.go b/proc/variables.go index 279f5aac..c0f7777e 100644 --- a/proc/variables.go +++ b/proc/variables.go @@ -53,6 +53,9 @@ type G struct { // PC of entry to top-most deferred function. DeferPC uint64 + + // Thread that this goroutine is currently allocated to + thread *Thread } // Returns whether the goroutine is blocked on @@ -68,7 +71,7 @@ func (g *G) chanRecvReturnAddr(dbp *DebuggedProcess) (uint64, error) { return 0, err } topLoc := locs[len(locs)-1] - return topLoc.addr, nil + return topLoc.PC, nil } // NoGError returned when a G could not be found diff --git a/service/api/conversions.go b/service/api/conversions.go index e326c09a..a09fc55f 100644 --- a/service/api/conversions.go +++ b/service/api/conversions.go @@ -1,6 +1,9 @@ package api -import "github.com/derekparker/delve/proc" +import ( + "debug/gosym" + "github.com/derekparker/delve/proc" +) // convertBreakpoint converts an internal breakpoint to an API Breakpoint. func ConvertBreakpoint(bp *proc.Breakpoint) *Breakpoint { @@ -27,14 +30,7 @@ func ConvertThread(th *proc.Thread) *Thread { pc = loc.PC file = loc.File line = loc.Line - if loc.Fn != nil { - function = &Function{ - Name: loc.Fn.Name, - Type: loc.Fn.Type, - Value: loc.Fn.Value, - GoType: loc.Fn.GoType, - } - } + function = ConvertFunction(loc.Fn) } return &Thread{ @@ -55,23 +51,35 @@ func ConvertVar(v *proc.Variable) Variable { } } -// convertGoroutine converts an internal Goroutine to an API Goroutine. -func ConvertGoroutine(g *proc.G) *Goroutine { - var function *Function - if g.Func != nil { - function = &Function{ - Name: g.Func.Name, - Type: g.Func.Type, - Value: g.Func.Value, - GoType: g.Func.GoType, - } +func ConvertFunction(fn *gosym.Func) *Function { + if fn == nil { + return nil + } + + return &Function{ + Name: fn.Name, + Type: fn.Type, + Value: fn.Value, + GoType: fn.GoType, } +} +// convertGoroutine converts an internal Goroutine to an API Goroutine. +func ConvertGoroutine(g *proc.G) *Goroutine { return &Goroutine{ ID: g.Id, PC: g.PC, File: g.File, Line: g.Line, - Function: function, + Function: ConvertFunction(g.Func), + } +} + +func ConvertLocation(loc proc.Location) Location { + return Location{ + PC: loc.PC, + File: loc.File, + Line: loc.Line, + Function: ConvertFunction(loc.Fn), } } diff --git a/service/api/types.go b/service/api/types.go index d28b1a93..3d4ec03a 100644 --- a/service/api/types.go +++ b/service/api/types.go @@ -41,6 +41,13 @@ type Thread struct { Function *Function `json:"function,omitempty"` } +type Location struct { + PC uint64 `json:"pc"` + File string `json:"file"` + Line int `json:"line"` + Function *Function `json:"function,omitempty"` +} + // Function represents thread-scoped function information. type Function struct { // Name is the function name. diff --git a/service/client.go b/service/client.go index 2fc92168..062ce2c5 100644 --- a/service/client.go +++ b/service/client.go @@ -60,4 +60,7 @@ type Client interface { // ListGoroutines lists all goroutines. ListGoroutines() ([]*api.Goroutine, error) + + // Returns stacktrace + Stacktrace(goroutineId, depth int) ([]*api.Location, error) } diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go index 128e1962..2343fe7e 100644 --- a/service/debugger/debugger.go +++ b/service/debugger/debugger.go @@ -309,3 +309,49 @@ func (d *Debugger) Goroutines() ([]*api.Goroutine, error) { } return goroutines, err } + +func (d *Debugger) Stacktrace(goroutineId, depth int) ([]api.Location, error) { + var rawlocs []proc.Location + var rawloc *proc.Location + var err error + + if goroutineId < 0 { + rawlocs, err = d.process.CurrentThread.Stacktrace(depth) + if err != nil { + return nil, err + } + rawloc, err = d.process.CurrentThread.Location() + if err != nil { + return nil, err + } + } else { + gs, err := d.process.GoroutinesInfo() + if err != nil { + return nil, err + } + for _, g := range gs { + if g.Id == goroutineId { + rawlocs, err = d.process.GoroutineStacktrace(g, depth) + if err != nil { + return nil, err + } + rawloc = d.process.GoroutineLocation(g) + break + } + } + + if rawlocs == nil { + return nil, fmt.Errorf("Unknown goroutine id %d\n", goroutineId) + } + } + + locations := make([]api.Location, 0, len(rawlocs)+1) + + locations = append(locations, api.ConvertLocation(*rawloc)) + for i := range rawlocs { + rawlocs[i].Line-- + locations = append(locations, api.ConvertLocation(rawlocs[i])) + } + + return locations, nil +} diff --git a/service/rest/client.go b/service/rest/client.go index a886634c..f5667234 100644 --- a/service/rest/client.go +++ b/service/rest/client.go @@ -266,6 +266,15 @@ func (c *RESTClient) ListGoroutines() ([]*api.Goroutine, error) { return goroutines, nil } +func (c *RESTClient) Stacktrace(goroutineId, depth int) ([]*api.Location, error) { + var locations []*api.Location + err := c.doGET(fmt.Sprintf("/goroutines/%d/trace?depth=%d", goroutineId, depth), &locations) + if err != nil { + return nil, err + } + return locations, nil +} + // TODO: how do we use http.Client with a UNIX socket URI? func (c *RESTClient) url(path string) string { return fmt.Sprintf("http://%s%s", c.addr, path) diff --git a/service/rest/server.go b/service/rest/server.go index 2244739f..7e23eb7d 100644 --- a/service/rest/server.go +++ b/service/rest/server.go @@ -82,6 +82,7 @@ func (s *RESTServer) Run() error { Route(ws.GET("/threads/{thread-id}/vars").To(s.listThreadPackageVars)). Route(ws.GET("/threads/{thread-id}/eval/{symbol}").To(s.evalThreadSymbol)). Route(ws.GET("/goroutines").To(s.listGoroutines)). + Route(ws.GET("/goroutines/{goroutine-id}/trace").To(s.stacktraceGoroutine)). Route(ws.POST("/command").To(s.doCommand)). Route(ws.GET("/sources").To(s.listSources)). Route(ws.GET("/functions").To(s.listFunctions)). @@ -171,6 +172,29 @@ func (s *RESTServer) getBreakpoint(request *restful.Request, response *restful.R response.WriteEntity(found) } +func (s *RESTServer) stacktraceGoroutine(request *restful.Request, response *restful.Response) { + goroutineId, err := strconv.Atoi(request.PathParameter("goroutine-id")) + if err != nil { + writeError(response, http.StatusBadRequest, "invalid goroutine id") + return + } + + depth, err := strconv.Atoi(request.QueryParameter("depth")) + if err != nil { + writeError(response, http.StatusBadRequest, "invalid depth") + return + } + + locations, err := s.debugger.Stacktrace(goroutineId, depth) + if err != nil { + writeError(response, http.StatusBadRequest, err.Error()) + return + } + + response.WriteHeader(http.StatusOK) + response.WriteEntity(locations) +} + func (s *RESTServer) listBreakpoints(request *restful.Request, response *restful.Response) { response.WriteEntity(s.debugger.Breakpoints()) } diff --git a/terminal/command.go b/terminal/command.go index 1681dfa9..891fa931 100644 --- a/terminal/command.go +++ b/terminal/command.go @@ -59,6 +59,7 @@ func DebugCommands(client service.Client) *Commands { {aliases: []string{"print", "p"}, cmdFn: printVar, helpMsg: "Evaluate a variable."}, {aliases: []string{"info"}, cmdFn: info, helpMsg: "Subcommands: args, funcs, locals, sources, vars, or regs."}, {aliases: []string{"exit"}, cmdFn: nullCommand, helpMsg: "Exit the debugger."}, + {aliases: []string{"stack"}, cmdFn: stackCommand, helpMsg: "stack [ []]. Prints stack."}, } return c @@ -189,7 +190,7 @@ func goroutines(client service.Client, args ...string) error { if g.Function != nil { fname = g.Function.Name } - fmt.Printf("Goroutine %d - %s:%d %s\n", g.ID, g.File, g.Line, fname) + fmt.Printf("Goroutine %d - %s:%d %s (%#v)\n", g.ID, g.File, g.Line, fname, g.PC) } return nil } @@ -399,7 +400,7 @@ func info(client service.Client, args ...string) error { data = filterVariables(vars, filter) default: - return fmt.Errorf("unsupported info type, must be args, funcs, locals, sources, or vars") + return fmt.Errorf("unsupported info type, must be args, funcs, locals, sources or vars") } // sort and output data @@ -411,6 +412,45 @@ func info(client service.Client, args ...string) error { return nil } +func stackCommand(client service.Client, args ...string) error { + var err error + + goroutineid := -1 + depth := 10 + + 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") + } + + default: + return fmt.Errorf("Wrong number of arguments to stack") + } + + stack, err := client.Stacktrace(goroutineid, depth) + if err != nil { + return err + } + for i := range stack { + name := "(nil)" + if stack[i].Function != nil { + name = stack[i].Function.Name + } + fmt.Printf("%d. %s\n\t%s:%d (%#v)\n", i, name, stack[i].File, stack[i].Line, stack[i].PC) + } + return nil +} + func printcontext(state *api.DebuggerState) error { if state.CurrentThread == nil { fmt.Println("No current thread available") -- GitLab