| // Copyright 2018 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| //go:build !aix && !plan9 && !wasm |
| |
| // (go.dev/issue/32839) |
| |
| package main |
| |
| import ( |
| "fmt" |
| "html" |
| "math" |
| "net/http" |
| "strconv" |
| |
| "golang.org/x/debug/internal/core" |
| "golang.org/x/debug/internal/gocore" |
| ) |
| |
| // serveHTML starts and serves a webserver on the port. |
| // If async is true, it returns immediately after starting the server. |
| func serveHTML(c *gocore.Process, port int, async bool) { |
| http.HandleFunc("/object", func(w http.ResponseWriter, r *http.Request) { |
| objs, ok := r.URL.Query()["o"] |
| if !ok || len(objs) != 1 { |
| fmt.Fprintf(w, "wrong or missing o= object specification") |
| return |
| } |
| obj, err := strconv.ParseInt(objs[0], 16, 64) |
| if err != nil { |
| fmt.Fprintf(w, "unparseable o= object specification: %s", err) |
| return |
| } |
| a := core.Address(obj) |
| x, _ := c.FindObject(a) |
| if x == 0 { |
| fmt.Fprintf(w, "can't find object at %x", a) |
| return |
| } |
| addr := c.Addr(x) |
| size := c.Size(x) |
| typ, repeat := c.Type(x) |
| |
| tableStyle(w) |
| fmt.Fprintf(w, "<h1>object %x</h1>\n", a) |
| fmt.Fprintf(w, "<h3>%s</h3>\n", html.EscapeString(typeName(c, x))) |
| fmt.Fprintf(w, "<h3>%d bytes</h3>\n", size) |
| |
| if typ != nil && repeat == 1 && typ.String() == "runtime.g" { |
| found := false |
| for _, g := range c.Goroutines() { |
| if g.Addr() == addr { |
| found = true |
| break |
| } |
| } |
| if found { |
| fmt.Fprintf(w, "<h3><a href=\"goroutine?g=%x\">goroutine stack</a></h3>\n", addr) |
| } |
| } |
| |
| fmt.Fprintf(w, "<table>\n") |
| fmt.Fprintf(w, "<tr><th align=left>field</th><th align=left colspan=\"2\">type</th><th align=left>value</th></tr>\n") |
| var end int64 |
| if typ != nil { |
| n := size / typ.Size |
| if n > 1 { |
| for i := int64(0); i < n; i++ { |
| htmlObject(w, c, fmt.Sprintf("[%d]", i), addr.Add(i*typ.Size), typ, nil) |
| } |
| } else { |
| htmlObject(w, c, "", addr, typ, nil) |
| } |
| end = n * typ.Size |
| } |
| for i := end; i < size; i += c.Process().PtrSize() { |
| fmt.Fprintf(w, "<tr><td>f%d</td><td colspan=\"2\">?</td>", i) |
| if c.IsPtr(addr.Add(i)) { |
| fmt.Fprintf(w, "<td>%s</td>", htmlPointer(c, c.Process().ReadPtr(addr.Add(i)))) |
| } else { |
| fmt.Fprintf(w, "<td><pre>") |
| for j := int64(0); j < c.Process().PtrSize(); j++ { |
| fmt.Fprintf(w, "%02x ", c.Process().ReadUint8(addr.Add(i+j))) |
| } |
| fmt.Fprintf(w, "</pre></td><td><pre>") |
| for j := int64(0); j < c.Process().PtrSize(); j++ { |
| r := c.Process().ReadUint8(addr.Add(i + j)) |
| if r >= 32 && r <= 126 { |
| fmt.Fprintf(w, "%s", html.EscapeString(string(rune(r)))) |
| } else { |
| fmt.Fprintf(w, ".") |
| } |
| } |
| fmt.Fprintf(w, "</pre></td>") |
| } |
| fmt.Fprintf(w, "</tr>\n") |
| } |
| fmt.Fprintf(w, "</table>\n") |
| fmt.Fprintf(w, "<h3>references to this object</h3>\n") |
| nrev := 0 |
| c.ForEachReversePtr(x, func(z gocore.Object, r *gocore.Root, i, j int64) bool { |
| if nrev == 10 { |
| fmt.Fprintf(w, "...additional references elided...<br/>\n") |
| return false |
| } |
| if r != nil { |
| fmt.Fprintf(w, "%s%s", r.Name, typeFieldName(r.Type, i)) |
| } else { |
| t, r := c.Type(z) |
| if t == nil { |
| fmt.Fprintf(w, "%s", htmlPointer(c, c.Addr(z).Add(i))) |
| } else { |
| idx := "" |
| if r > 1 { |
| idx = fmt.Sprintf("[%d]", i/t.Size) |
| i %= t.Size |
| } |
| fmt.Fprintf(w, "%s%s%s", htmlPointer(c, c.Addr(z)), idx, typeFieldName(t, i)) |
| } |
| } |
| fmt.Fprintf(w, " → %s<br/>\n", htmlPointer(c, a.Add(j))) |
| nrev++ |
| return true |
| }) |
| }) |
| http.HandleFunc("/goroutines", func(w http.ResponseWriter, r *http.Request) { |
| fmt.Fprintf(w, "<h1>goroutines</h1>\n") |
| tableStyle(w) |
| fmt.Fprintf(w, "<table>\n") |
| fmt.Fprintf(w, "<tr><th align=left>goroutine</th><th align=left>top of stack</th></tr>\n") |
| for _, g := range c.Goroutines() { |
| fmt.Fprintf(w, "<tr><td><a href=\"goroutine?g=%x\">%x</a></td><td>%s</td></tr>\n", g.Addr(), g.Addr(), g.Frames()[0].Func().Name()) |
| } |
| fmt.Fprintf(w, "</table>\n") |
| // TODO: export goroutine state (runnable, running, syscall, ...) and print it here. |
| }) |
| http.HandleFunc("/goroutine", func(w http.ResponseWriter, r *http.Request) { |
| gs, ok := r.URL.Query()["g"] |
| if !ok || len(gs) != 1 { |
| fmt.Fprintf(w, "wrong or missing g= goroutine specification") |
| return |
| } |
| addr, err := strconv.ParseInt(gs[0], 16, 64) |
| if err != nil { |
| fmt.Fprintf(w, "unparseable g= goroutine specification: %s\n", err) |
| return |
| } |
| a := core.Address(addr) |
| var g *gocore.Goroutine |
| for _, x := range c.Goroutines() { |
| if x.Addr() == a { |
| g = x |
| break |
| } |
| } |
| if g == nil { |
| fmt.Fprintf(w, "goroutine %x not found\n", a) |
| return |
| } |
| |
| tableStyle(w) |
| fmt.Fprintf(w, "<h1>goroutine %x</h1>\n", g.Addr()) |
| fmt.Fprintf(w, "<h3>%s</h3>\n", htmlPointer(c, g.Addr())) |
| fmt.Fprintf(w, "<h3>%d bytes of stack</h3>\n", g.Stack()) |
| for _, f := range g.Frames() { |
| fmt.Fprintf(w, "<h3>%s+%d</h3>\n", f.Func().Name(), f.PC().Sub(f.Func().Entry())) |
| // TODO: convert fn+off to file+lineno. |
| fmt.Fprintf(w, "<table>\n") |
| fmt.Fprintf(w, "<tr><th align=left>field</th><th align=left colspan=\"2\">type</th><th align=left>value</th></tr>\n") |
| for _, r := range f.Roots() { |
| htmlObject(w, c, r.Name, r.Addr, r.Type, f.Live) |
| } |
| fmt.Fprintf(w, "</table>\n") |
| } |
| }) |
| http.HandleFunc("/globals", func(w http.ResponseWriter, r *http.Request) { |
| fmt.Fprintf(w, "<h1>globals</h1>\n") |
| tableStyle(w) |
| fmt.Fprintf(w, "<table>\n") |
| fmt.Fprintf(w, "<tr><th align=left>field</th><th align=left colspan=\"2\">type</th><th align=left>value</th></tr>\n") |
| for _, r := range c.Globals() { |
| htmlObject(w, c, r.Name, r.Addr, r.Type, nil) |
| } |
| fmt.Fprintf(w, "</table>\n") |
| }) |
| http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { |
| fmt.Fprintf(w, "<h1>core dump viewer</h1>\n") |
| fmt.Fprintf(w, "%s<br/>\n", c.Process().Arch()) |
| fmt.Fprintf(w, "%s<br/>\n", c.BuildVersion()) |
| fmt.Fprintf(w, "<a href=\"goroutines\">goroutines</a><br/>\n") |
| fmt.Fprintf(w, "<a href=\"globals\">globals</a><br/>\n") |
| tableStyle(w) |
| fmt.Fprintf(w, "<table>\n") |
| fmt.Fprintf(w, "<tr><th align=left>category</th><th align=left>bytes</th><th align=left>percent</th></tr>\n") |
| all := c.Stats().Size |
| var p func(*gocore.Stats, string) |
| p = func(s *gocore.Stats, prefix string) { |
| fmt.Fprintf(w, "<tr><td>%s%s</td><td align=right>%d</td><td align=right>%.2f</td></tr>\n", prefix, s.Name, s.Size, float64(s.Size)/float64(all)*100) |
| for _, c := range s.Children { |
| p(c, prefix+"..") |
| } |
| } |
| p(c.Stats(), "") |
| fmt.Fprintf(w, "</table>\n") |
| }) |
| |
| if port <= 0 { |
| port = 8080 |
| } |
| fmt.Printf("start serving on http://localhost:%d\n", port) |
| |
| httpAddr := fmt.Sprintf(":%d", port) |
| if async { |
| go http.ListenAndServe(httpAddr, nil) |
| return |
| } |
| http.ListenAndServe(httpAddr, nil) |
| } |
| |
| func htmlObject(w http.ResponseWriter, c *gocore.Process, name string, a core.Address, t *gocore.Type, live map[core.Address]bool) { |
| switch t.Kind { |
| case gocore.KindBool: |
| v := c.Process().ReadUint8(a) != 0 |
| fmt.Fprintf(w, "<tr><td>%s</td><td colspan=\"2\">%s</td><td>%t</td></tr>\n", name, html.EscapeString(t.String()), v) |
| case gocore.KindInt: |
| var v int64 |
| switch t.Size { |
| case 1: |
| v = int64(c.Process().ReadInt8(a)) |
| case 2: |
| v = int64(c.Process().ReadInt16(a)) |
| case 4: |
| v = int64(c.Process().ReadInt32(a)) |
| case 8: |
| v = c.Process().ReadInt64(a) |
| } |
| fmt.Fprintf(w, "<tr><td>%s</td><td colspan=\"2\">%s</td><td>%d</td></tr>\n", name, html.EscapeString(t.String()), v) |
| case gocore.KindUint: |
| var v uint64 |
| switch t.Size { |
| case 1: |
| v = uint64(c.Process().ReadUint8(a)) |
| case 2: |
| v = uint64(c.Process().ReadUint16(a)) |
| case 4: |
| v = uint64(c.Process().ReadUint32(a)) |
| case 8: |
| v = c.Process().ReadUint64(a) |
| } |
| fmt.Fprintf(w, "<tr><td>%s</td><td colspan=\"2\">%s</td><td>%d</td></tr>\n", name, html.EscapeString(t.String()), v) |
| case gocore.KindFloat: |
| var v float64 |
| switch t.Size { |
| case 4: |
| v = float64(math.Float32frombits(c.Process().ReadUint32(a))) |
| case 8: |
| v = math.Float64frombits(c.Process().ReadUint64(a)) |
| } |
| fmt.Fprintf(w, "<tr><td>%s</td><td colspan=\"2\">%s</td><td>%f</td></tr>\n", name, html.EscapeString(t.String()), v) |
| case gocore.KindComplex: |
| var v complex128 |
| switch t.Size { |
| case 8: |
| v = complex128(complex( |
| math.Float32frombits(c.Process().ReadUint32(a)), |
| math.Float32frombits(c.Process().ReadUint32(a.Add(4))))) |
| |
| case 16: |
| v = complex( |
| math.Float64frombits(c.Process().ReadUint64(a)), |
| math.Float64frombits(c.Process().ReadUint64(a.Add(8)))) |
| } |
| fmt.Fprintf(w, "<tr><td>%s</td><td colspan=\"2\">%s</td><td>%f</td></tr>\n", name, html.EscapeString(t.String()), v) |
| case gocore.KindEface: |
| fmt.Fprintf(w, "<tr><td rowspan=\"2\">%s</td><td rowspan=\"2\">interface{}</td><td>*runtime._type</td><td>%s</td>", name, htmlPointerAt(c, a, live)) |
| if live == nil || live[a] { |
| dt := c.DynamicType(t, a) |
| if dt != nil { |
| fmt.Fprintf(w, "<td>%s</td>", dt.Name) |
| } |
| } |
| fmt.Fprintf(w, "</tr>\n") |
| fmt.Fprintf(w, "<tr><td>unsafe.Pointer</td><td>%s</td></tr>\n", htmlPointerAt(c, a.Add(c.Process().PtrSize()), live)) |
| case gocore.KindIface: |
| fmt.Fprintf(w, "<tr><td rowspan=\"2\">%s</td><td rowspan=\"2\">interface{...}</td><td>*runtime.itab</td><td>%s</td>", name, htmlPointerAt(c, a, live)) |
| if live == nil || live[a] { |
| dt := c.DynamicType(t, a) |
| if dt != nil { |
| fmt.Fprintf(w, "<td>%s</td>", dt.Name) |
| } |
| } |
| fmt.Fprintf(w, "</tr>\n") |
| fmt.Fprintf(w, "<tr><td>unsafe.Pointer</td><td>%s</td></tr>\n", htmlPointerAt(c, a.Add(c.Process().PtrSize()), live)) |
| case gocore.KindPtr: |
| fmt.Fprintf(w, "<tr><td>%s</td><td colspan=\"2\">%s</td><td>%s</td></tr>\n", name, html.EscapeString(t.String()), htmlPointerAt(c, a, live)) |
| case gocore.KindFunc: |
| fmt.Fprintf(w, "<tr><td>%s</td><td colspan=\"2\">%s</td><td>%s</td>", name, html.EscapeString(t.String()), htmlPointerAt(c, a, live)) |
| if fn := c.Process().ReadPtr(a); fn != 0 { |
| pc := c.Process().ReadPtr(fn) |
| if f := c.FindFunc(pc); f != nil && f.Entry() == pc { |
| fmt.Fprintf(w, "<td>%s</td>", f.Name()) |
| } |
| } |
| fmt.Fprintf(w, "</tr>\n") |
| case gocore.KindString: |
| n := c.Process().ReadInt(a.Add(c.Process().PtrSize())) |
| fmt.Fprintf(w, "<tr><td rowspan=\"2\">%s</td><td rowspan=\"2\">string</td><td>*uint8</td><td>%s</td>", name, htmlPointerAt(c, a, live)) |
| if live == nil || live[a] { |
| if n > 0 { |
| n2 := n |
| ddd := "" |
| if n > 100 { |
| n2 = 100 |
| ddd = "..." |
| } |
| b := make([]byte, n2) |
| c.Process().ReadAt(b, c.Process().ReadPtr(a)) |
| fmt.Fprintf(w, "<td rowspan=\"2\">\"%s\"%s</td>", html.EscapeString(string(b)), ddd) |
| } else { |
| fmt.Fprintf(w, "<td rowspan=\"2\">\"\"</td>") |
| } |
| } |
| fmt.Fprintf(w, "</tr>\n") |
| fmt.Fprintf(w, "<tr><td>int</td><td>%d</td></tr>\n", n) |
| case gocore.KindSlice: |
| fmt.Fprintf(w, "<tr><td rowspan=\"3\">%s</td><td rowspan=\"3\">%s</td><td>*%s</td><td>%s</td></tr>\n", name, t, t.Elem, htmlPointerAt(c, a, live)) |
| fmt.Fprintf(w, "<tr><td>int</td><td>%d</td></tr>\n", c.Process().ReadInt(a.Add(c.Process().PtrSize()))) |
| fmt.Fprintf(w, "<tr><td>int</td><td>%d</td></tr>\n", c.Process().ReadInt(a.Add(c.Process().PtrSize()*2))) |
| case gocore.KindArray: |
| s := t.Elem.Size |
| n := t.Count |
| if n*s > 16384 { |
| n = (16384 + s - 1) / s |
| } |
| for i := int64(0); i < n; i++ { |
| htmlObject(w, c, fmt.Sprintf("%s[%d]", name, i), a.Add(i*s), t.Elem, live) |
| } |
| if n*s != t.Size { |
| fmt.Fprintf(w, "<tr><td>...</td><td>...</td><td>...</td></tr>\n") |
| } |
| case gocore.KindStruct: |
| for _, f := range t.Fields { |
| htmlObject(w, c, name+"."+f.Name, a.Add(f.Off), f.Type, live) |
| } |
| } |
| } |
| |
| func htmlPointer(c *gocore.Process, a core.Address) string { |
| if a == 0 { |
| return "nil" |
| } |
| x, i := c.FindObject(a) |
| if x == 0 { |
| return fmt.Sprintf("%x", a) |
| } |
| s := fmt.Sprintf("<a href=\"/object?o=%x\">object %x</a>", c.Addr(x), c.Addr(x)) |
| if i == 0 { |
| return s |
| } |
| t, r := c.Type(x) |
| if t == nil || i >= r*t.Size { |
| return fmt.Sprintf("%s+%d", s, i) |
| } |
| idx := "" |
| if r > 1 { |
| idx = fmt.Sprintf("[%d]", i/t.Size) |
| i %= t.Size |
| } |
| return fmt.Sprintf("%s%s%s", s, idx, typeFieldName(t, i)) |
| } |
| |
| func htmlPointerAt(c *gocore.Process, a core.Address, live map[core.Address]bool) string { |
| if live != nil && !live[a] { |
| return "<text style=\"color:LightGray\">dead</text>" |
| } |
| return htmlPointer(c, c.Process().ReadPtr(a)) |
| } |
| |
| func tableStyle(w http.ResponseWriter) { |
| fmt.Fprintf(w, "<style>\n") |
| fmt.Fprintf(w, "table, th, td {\n") |
| fmt.Fprintf(w, " border: 1px solid black;\n") |
| fmt.Fprintf(w, " border-collapse: collapse;\n") |
| fmt.Fprintf(w, " align: left;\n") |
| fmt.Fprintf(w, "}\n") |
| fmt.Fprintf(w, "table, th, td {\n") |
| fmt.Fprintf(w, " padding: 2px;\n") |
| fmt.Fprintf(w, "}\n") |
| fmt.Fprintf(w, "tr:hover {background-color: #f5f5f5}\n") |
| fmt.Fprintf(w, "</style>\n") |
| |
| } |
| |
| // Returns the name of the field at offset off in x. |
| func objField(c *gocore.Process, x gocore.Object, off int64) string { |
| t, r := c.Type(x) |
| if t == nil { |
| return fmt.Sprintf("f%d", off) |
| } |
| s := "" |
| if r > 1 { |
| s = fmt.Sprintf("[%d]", off/t.Size) |
| off %= t.Size |
| } |
| return s + typeFieldName(t, off) |
| } |
| |
| func objRegion(c *gocore.Process, x gocore.Object, off int64) string { |
| t, r := c.Type(x) |
| if t == nil { |
| return fmt.Sprintf("f%d", off) |
| } |
| if off == 0 { |
| return "" |
| } |
| s := "" |
| if r > 1 { |
| s = fmt.Sprintf("[%d]", off/t.Size) |
| off %= t.Size |
| } |
| return s + typeFieldName(t, off) |
| } |