x/debug: add a function for reading the current (not-dead) goroutines from a target program. Moves some string-reading code from print.go into a separate function in peek.go. Also adds functions for reading string fields in structs, and C-style NUL-terminated strings, Change-Id: I399960891ce7d3ab0c989b94021db284dc095a59 Reviewed-on: https://go-review.googlesource.com/19248 Reviewed-by: Dave Day <djd@golang.org>
diff --git a/ogle/demo/ogler/ogler_test.go b/ogle/demo/ogler/ogler_test.go index cab7f94..1e44705 100644 --- a/ogle/demo/ogler/ogler_test.go +++ b/ogle/demo/ogler/ogler_test.go
@@ -458,6 +458,14 @@ log.Fatalf("Resume: %v", err) } + gs, err := prog.Goroutines() + if err != nil { + t.Fatalf("Goroutines(): got error %s", err) + } + for _, g := range gs { + fmt.Println(g) + } + frames, err := prog.Frames(100) if err != nil { log.Fatalf("prog.Frames error: %v", err)
diff --git a/ogle/program/client/client.go b/ogle/program/client/client.go index 6083d04..51bf1a5 100644 --- a/ogle/program/client/client.go +++ b/ogle/program/client/client.go
@@ -241,6 +241,13 @@ return resp.Frames, err } +func (p *Program) Goroutines() ([]*program.Goroutine, error) { + req := proxyrpc.GoroutinesRequest{} + var resp proxyrpc.GoroutinesResponse + err := p.client.Call("Server.Goroutines", &req, &resp) + return resp.Goroutines, err +} + func (p *Program) VarByName(name string) (program.Var, error) { req := proxyrpc.VarByNameRequest{Name: name} var resp proxyrpc.VarByNameResponse
diff --git a/ogle/program/local/local.go b/ogle/program/local/local.go index 55f8c12..ed9bdc8 100644 --- a/ogle/program/local/local.go +++ b/ogle/program/local/local.go
@@ -126,6 +126,13 @@ return resp.Frames, err } +func (l *Local) Goroutines() ([]*program.Goroutine, error) { + req := proxyrpc.GoroutinesRequest{} + var resp proxyrpc.GoroutinesResponse + err := l.s.Goroutines(&req, &resp) + return resp.Goroutines, err +} + func (l *Local) VarByName(name string) (program.Var, error) { req := proxyrpc.VarByNameRequest{Name: name} var resp proxyrpc.VarByNameResponse
diff --git a/ogle/program/program.go b/ogle/program/program.go index b7feb20..6ac49c9 100644 --- a/ogle/program/program.go +++ b/ogle/program/program.go
@@ -6,6 +6,7 @@ package program // import "golang.org/x/debug/ogle/program" import ( + "fmt" "io" ) @@ -105,6 +106,41 @@ // MapElement returns Vars for the key and value of a map element specified by // a 0-based index. MapElement(m Map, index uint64) (Var, Var, error) + + // Goroutines gets the current goroutines. + Goroutines() ([]*Goroutine, error) +} + +type Goroutine struct { + ID int64 + Status GoroutineStatus + StatusString string // A human-readable string explaining the status in more detail. + Function string // Name of the goroutine function. + Caller string // Name of the function that created this goroutine. +} + +type GoroutineStatus byte + +const ( + Running GoroutineStatus = iota + Queued + Blocked +) + +func (g GoroutineStatus) String() string { + switch g { + case Running: + return "running" + case Queued: + return "queued" + case Blocked: + return "blocked" + } + return "invalid status" +} + +func (g *Goroutine) String() string { + return fmt.Sprintf("goroutine %d [%s] %s -> %s", g.ID, g.StatusString, g.Caller, g.Function) } // A reference to a variable in a program.
diff --git a/ogle/program/proxyrpc/proxyrpc.go b/ogle/program/proxyrpc/proxyrpc.go index 8ed02b4..b6636f8 100644 --- a/ogle/program/proxyrpc/proxyrpc.go +++ b/ogle/program/proxyrpc/proxyrpc.go
@@ -156,3 +156,10 @@ Key program.Var Value program.Var } + +type GoroutinesRequest struct { +} + +type GoroutinesResponse struct { + Goroutines []*program.Goroutine +}
diff --git a/ogle/program/server/peek.go b/ogle/program/server/peek.go index e61a974..936b3ab 100644 --- a/ogle/program/server/peek.go +++ b/ogle/program/server/peek.go
@@ -54,6 +54,50 @@ return s.arch.UintN(buf), nil } +// peekString reads a string of the given type at the given address. +// At most byteLimit bytes will be read. If the string is longer, "..." is appended. +func (s *Server) peekString(typ *dwarf.StringType, a uint64, byteLimit uint64) (string, error) { + ptr, err := s.peekPtrStructField(&typ.StructType, a, "str") + if err != nil { + return "", err + } + length, err := s.peekUintOrIntStructField(&typ.StructType, a, "len") + if err != nil { + return "", err + } + if length > byteLimit { + buf := make([]byte, byteLimit, byteLimit+3) + if err := s.peekBytes(ptr, buf); err != nil { + return "", err + } else { + buf = append(buf, '.', '.', '.') + return string(buf), nil + } + } else { + buf := make([]byte, length) + if err := s.peekBytes(ptr, buf); err != nil { + return "", err + } else { + return string(buf), nil + } + } +} + +// peekCString reads a NUL-terminated string at the given address. +// At most byteLimit bytes will be read. If the string is longer, "..." is appended. +// peekCString never returns errors; if an error occurs, the string will be truncated in some way. +func (s *Server) peekCString(a uint64, byteLimit uint64) string { + buf := make([]byte, byteLimit, byteLimit+3) + s.peekBytes(a, buf) + for i, c := range buf { + if c == 0 { + return string(buf[0:i]) + } + } + buf = append(buf, '.', '.', '.') + return string(buf) +} + // peekPtrStructField reads a pointer in the field fieldName of the struct // of type t at addr. func (s *Server) peekPtrStructField(t *dwarf.StructType, addr uint64, fieldName string) (uint64, error) { @@ -122,6 +166,21 @@ return s.peekInt(addr+uint64(f.ByteOffset), it.ByteSize) } +// peekStringStructField reads a string field from the struct of the given type +// at the given address. +// At most byteLimit bytes will be read. If the string is longer, "..." is appended. +func (s *Server) peekStringStructField(t *dwarf.StructType, addr uint64, fieldName string, byteLimit uint64) (string, error) { + f, err := getField(t, fieldName) + if err != nil { + return "", fmt.Errorf("reading field %s: %s", fieldName, err) + } + st, ok := followTypedefs(f.Type).(*dwarf.StringType) + if !ok { + return "", fmt.Errorf("field %s is not a string", fieldName) + } + return s.peekString(st, addr+uint64(f.ByteOffset), byteLimit) +} + // peekMapLocationAndType returns the address and DWARF type of the underlying // struct of a map variable. func (s *Server) peekMapLocationAndType(t *dwarf.MapType, a uint64) (uint64, *dwarf.StructType, error) {
diff --git a/ogle/program/server/print.go b/ogle/program/server/print.go index 6c4be72..0ec0e5c 100644 --- a/ogle/program/server/print.go +++ b/ogle/program/server/print.go
@@ -490,32 +490,11 @@ } func (p *Printer) printStringAt(typ *dwarf.StringType, a uint64) { - // BUG: String header appears to have fields with ByteSize == 0 - ptr, err := p.server.peekPtrStructField(&typ.StructType, a, "str") - if err != nil { - p.errorf("reading string: %s", err) - return - } - length, err := p.server.peekUintOrIntStructField(&typ.StructType, a, "len") - if err != nil { - p.errorf("reading string: %s", err) - return - } const maxStringSize = 100 - if length > maxStringSize { - buf := make([]byte, maxStringSize) - if err := p.server.peekBytes(ptr, buf); err != nil { - p.errorf("reading string: %s", err) - } else { - p.printf("%q...", string(buf)) - } + if s, err := p.server.peekString(typ, a, maxStringSize); err != nil { + p.errorf("reading string: %s", err) } else { - buf := make([]byte, length) - if err := p.server.peekBytes(ptr, buf); err != nil { - p.errorf("reading string: %s", err) - } else { - p.printf("%q", string(buf)) - } + p.printf("%q", s) } }
diff --git a/ogle/program/server/server.go b/ogle/program/server/server.go index ecffc18..ffa72c3 100644 --- a/ogle/program/server/server.go +++ b/ogle/program/server/server.go
@@ -10,6 +10,7 @@ import ( "bytes" + "errors" "fmt" "os" "regexp" @@ -178,6 +179,8 @@ c.errc <- s.handleValue(req, c.resp.(*proxyrpc.ValueResponse)) case *proxyrpc.MapElementRequest: c.errc <- s.handleMapElement(req, c.resp.(*proxyrpc.MapElementResponse)) + case *proxyrpc.GoroutinesRequest: + c.errc <- s.handleGoroutines(req, c.resp.(*proxyrpc.GoroutinesResponse)) default: panic(fmt.Sprintf("unexpected call request type %T", c.req)) } @@ -833,3 +836,192 @@ } return nil } + +func (s *Server) Goroutines(req *proxyrpc.GoroutinesRequest, resp *proxyrpc.GoroutinesResponse) error { + return s.call(s.otherc, req, resp) +} + +const invalidStatus program.GoroutineStatus = 99 + +var ( + gStatus = [...]program.GoroutineStatus{ + 0: program.Queued, // _Gidle + 1: program.Queued, // _Grunnable + 2: program.Running, // _Grunning + 3: program.Blocked, // _Gsyscall + 4: program.Blocked, // _Gwaiting + 5: invalidStatus, // _Gmoribund_unused + 6: invalidStatus, // _Gdead + 7: invalidStatus, // _Genqueue + 8: program.Running, // _Gcopystack + } + gScanStatus = [...]program.GoroutineStatus{ + 0: invalidStatus, // _Gscan + _Gidle + 1: program.Queued, // _Gscanrunnable + 2: program.Running, // _Gscanrunning + 3: program.Blocked, // _Gscansyscall + 4: program.Blocked, // _Gscanwaiting + 5: invalidStatus, // _Gscan + _Gmoribund_unused + 6: invalidStatus, // _Gscan + _Gdead + 7: program.Queued, // _Gscanenqueue + } + gStatusString = [...]string{ + 0: "idle", + 1: "runnable", + 2: "running", + 3: "syscall", + 4: "waiting", + 8: "copystack", + } + gScanStatusString = [...]string{ + 1: "scanrunnable", + 2: "scanrunning", + 3: "scansyscall", + 4: "scanwaiting", + 7: "scanenqueue", + } +) + +func (s *Server) handleGoroutines(req *proxyrpc.GoroutinesRequest, resp *proxyrpc.GoroutinesResponse) error { + // Get DWARF type information for runtime.g. + ge, err := s.dwarfData.LookupEntry("runtime.g") + if err != nil { + return err + } + t, err := s.dwarfData.Type(ge.Offset) + if err != nil { + return err + } + gType, ok := followTypedefs(t).(*dwarf.StructType) + if !ok { + return errors.New("runtime.g is not a struct") + } + + // Read runtime.allg. + allgEntry, err := s.dwarfData.LookupEntry("runtime.allg") + if err != nil { + return err + } + allgAddr, err := s.dwarfData.EntryLocation(allgEntry) + if err != nil { + return err + } + allg, err := s.peekPtr(allgAddr) + if err != nil { + return fmt.Errorf("reading allg: %v", err) + } + + // Read runtime.allglen. + allglenEntry, err := s.dwarfData.LookupEntry("runtime.allglen") + if err != nil { + return err + } + off, err := s.dwarfData.EntryTypeOffset(allglenEntry) + if err != nil { + return err + } + allglenType, err := s.dwarfData.Type(off) + if err != nil { + return err + } + allglenAddr, err := s.dwarfData.EntryLocation(allglenEntry) + if err != nil { + return err + } + var allglen uint64 + switch followTypedefs(allglenType).(type) { + case *dwarf.UintType, *dwarf.IntType: + allglen, err = s.peekUint(allglenAddr, allglenType.Common().ByteSize) + if err != nil { + return fmt.Errorf("reading allglen: %v", err) + } + default: + // Some runtimes don't specify the type for allglen. Assume it's uint32. + allglen, err = s.peekUint(allglenAddr, 4) + if err != nil { + return fmt.Errorf("reading allglen: %v", err) + } + if allglen != 0 { + break + } + // Zero? Let's try uint64. + allglen, err = s.peekUint(allglenAddr, 8) + if err != nil { + return fmt.Errorf("reading allglen: %v", err) + } + } + + for i := uint64(0); i < allglen; i++ { + // allg is an array of pointers to g structs. Read allg[i]. + g, err := s.peekPtr(allg + i*uint64(s.arch.PointerSize)) + if err != nil { + return err + } + gr := program.Goroutine{} + + // Read status from the field named "atomicstatus" or "status". + status, err := s.peekUintStructField(gType, g, "atomicstatus") + if err != nil { + status, err = s.peekUintOrIntStructField(gType, g, "status") + } + if err != nil { + return err + } + if status == 6 { + // _Gdead. + continue + } + gr.Status = invalidStatus + if status < uint64(len(gStatus)) { + gr.Status = gStatus[status] + gr.StatusString = gStatusString[status] + } else if status^0x1000 < uint64(len(gScanStatus)) { + gr.Status = gScanStatus[status^0x1000] + gr.StatusString = gScanStatusString[status^0x1000] + } + if gr.Status == invalidStatus { + return fmt.Errorf("unexpected goroutine status 0x%x", status) + } + if status == 4 || status == 0x1004 { + // _Gwaiting or _Gscanwaiting. + // Try reading waitreason to get a better value for StatusString. + // Depending on the runtime, waitreason may be a Go string or a C string. + if waitreason, err := s.peekStringStructField(gType, g, "waitreason", 80); err == nil { + if waitreason != "" { + gr.StatusString = waitreason + } + } else if ptr, err := s.peekPtrStructField(gType, g, "waitreason"); err == nil { + waitreason := s.peekCString(ptr, 80) + if waitreason != "" { + gr.StatusString = waitreason + } + } + } + + gr.ID, err = s.peekIntStructField(gType, g, "goid") + if err != nil { + return err + } + + // Best-effort attempt to get the names of the goroutine function and the + // function that created the goroutine. They aren't always available. + functionName := func(pc uint64) string { + entry, _, err := s.dwarfData.EntryForPC(pc) + if err != nil { + return "" + } + name, _ := entry.Val(dwarf.AttrName).(string) + return name + } + if startpc, err := s.peekUintStructField(gType, g, "startpc"); err == nil { + gr.Function = functionName(startpc) + } + if gopc, err := s.peekUintStructField(gType, g, "gopc"); err == nil { + gr.Caller = functionName(gopc) + } + + resp.Goroutines = append(resp.Goroutines, &gr) + } + + return nil +}