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
+}