internal/gocore: move core of runReachable to Process.Reachable
...and turn it into an iteration so that we can later offer the option
to printe more (all) paths.
Having Process.Reachable also makes it easier to write a test for some
upcoming functionality (defer/panic as roots). In all, it seems like a
nicer separation of concerns.
I tested the main functionality by comparing the output pre- and
post-this CL. It looks identical:
$ ~/gob/go/debug-readonly/viewcore ~/tmp/viewcore/core.server_test.3024 --exe ~/tmp/viewcore/server_test --base ~/src/server reachable 203cbbf287e8 2>/dev/null
rpc.NewStreamImpl.func1
rpc.(*eventQueue).pop.q →
203cbc0dba40 rpc.eventQueue .list.ptr →
203cbbf287e8 *rpc.clientEvent
The output of the test is the more similar to the way I'd like to show
this output in the end:
gocore_test.go:475: [goroutine]
gocore_test.go:481: 0x55a36e513aa8 main.main.func1.gowrap7
gocore_test.go:481: 0x55a36e512ed4 main.complicatedRetain
gocore_test.go:512: unk [SP+0x108] 0xc000047fa0 (unsafe.Pointer) → 0xc00009a0a0 (main.myObj)
gocore_test.go:475: [goroutine]
gocore_test.go:481: 0x55a36e513aa8 main.main.func1.gowrap7
gocore_test.go:481: 0x55a36e512ed4 main.complicatedRetain
gocore_test.go:512: unk [SP+0xe8] 0xc000047f80 (unsafe.Pointer) → 0xc00009a0a0 (main.myObj)
gocore_test.go:475: [goroutine]
gocore_test.go:481: 0x55a36e513aa8 main.main.func1.gowrap7
gocore_test.go:481: 0x55a36e512ed4 main.complicatedRetain
gocore_test.go:512: unk [SP+0xc8] 0xc000047f60 (unsafe.Pointer) → 0xc00009a0a0 (main.myObj)
gocore_test.go:475: [goroutine]
gocore_test.go:481: 0x55a36e513aa8 main.main.func1.gowrap7
gocore_test.go:481: 0x55a36e512ed4 main.complicatedRetain
gocore_test.go:512: unk [SP+0x50] 0xc000047ee8 (unsafe.Pointer) → 0xc00009a0a0 (main.myObj)
gocore_test.go:475: [goroutine]
gocore_test.go:481: 0x55a36e513aa8 main.main.func1.gowrap7
gocore_test.go:481: 0x55a36e512ed4 main.complicatedRetain
gocore_test.go:512: ref [SP+0x120] 0xc000047fb8 (*main.myObj) → 0xc00009a0a0 (main.myObj)
gocore_test.go:475: [goroutine]
gocore_test.go:481: 0x55a36e513b08 main.main.func1.gowrap6
gocore_test.go:481: 0x55a36e5135c8 main.hardAliasRetain
gocore_test.go:512: otherRef [SP+0x20] 0xc000047790 (*main.myObj) → 0xc00009a0a0 (main.myObj)
gocore_test.go:475: [goroutine]
gocore_test.go:481: 0x55a36e513b08 main.main.func1.gowrap6
gocore_test.go:481: 0x55a36e5135c8 main.hardAliasRetain
gocore_test.go:512: ref [SP+0x48] 0xc0000477b8 (*main.myObj) → 0xc00009a0a0 (main.myObj)
gocore_test.go:475: [goroutine]
gocore_test.go:481: 0x55a36e513b68 main.main.func1.gowrap5
gocore_test.go:481: 0x55a36e5134d7 main.aliasRetain
gocore_test.go:512: otherRef [SP+0x30] 0xc000046fb8 (*main.myObj) → 0xc00009a0a0 (main.myObj)
gocore_test.go:475: [goroutine]
gocore_test.go:481: 0x55a36e513b68 main.main.func1.gowrap5
gocore_test.go:481: 0x55a36e5134d7 main.aliasRetain
gocore_test.go:512: ref [SP+0x30] 0xc000046fb8 (*main.myObj) → 0xc00009a0a0 (main.myObj)
gocore_test.go:475: [goroutine]
gocore_test.go:481: 0x55a36e513bc8 main.main.func1.gowrap4
gocore_test.go:481: 0x55a36e513347 main.hardRenameRetain
gocore_test.go:512: otherRef [SP+0x18] 0xc000046798 (*main.myObj) → 0xc00009a0a0 (main.myObj)
gocore_test.go:475: [goroutine]
gocore_test.go:481: 0x55a36e513c28 main.main.func1.gowrap3
gocore_test.go:481: 0x55a36e513273 main.renameRetain
gocore_test.go:512: otherRef [SP+0x30] 0xc000045fb8 (*main.myObj) → 0xc00009a0a0 (main.myObj)
gocore_test.go:475: [goroutine]
gocore_test.go:481: 0x55a36e513c28 main.main.func1.gowrap3
gocore_test.go:481: 0x55a36e513273 main.renameRetain
gocore_test.go:512: ref [SP+0x30] 0xc000045fb8 (*main.myObj) → 0xc00009a0a0 (main.myObj)
gocore_test.go:475: [goroutine]
gocore_test.go:481: 0x55a36e513c88 main.main.func1.gowrap2
gocore_test.go:481: 0x55a36e5133f3 main.multiFrame1Retain
gocore_test.go:481: 0x55a36e513433 main.multiFrame2Retain
gocore_test.go:481: 0x55a36e513473 main.multiFrame3Retain
gocore_test.go:481: 0x55a36e5131b3 main.retain
gocore_test.go:512: ref [SP+0x30] 0xc000045740 (*main.myObj) → 0xc00009a0a0 (main.myObj)
gocore_test.go:475: [goroutine]
gocore_test.go:481: 0x55a36e513ce8 main.main.func1.gowrap1
gocore_test.go:481: 0x55a36e5131b3 main.retain
gocore_test.go:512: ref [SP+0x30] 0xc000044fb8 (*main.myObj) → 0xc00009a0a0 (main.myObj)
gocore_test.go:473: [global]
gocore_test.go:512: main.gPlainMyObj 0x55a36e65d080 (*main.myObj) → 0xc00009a0a0 (main.myObj)
Change-Id: I953e39fc0c3357c7c636144e80c42a6fd2d737b2
Reviewed-on: https://go-review.googlesource.com/c/debug/+/750481
Reviewed-by: Keith Randall <khr@golang.org>
Auto-Submit: Nicolas Hillegeer <aktau@google.com>
Reviewed-by: Keith Randall <khr@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/cmd/viewcore/html.go b/cmd/viewcore/html.go
index 5180514..d67e20c 100644
--- a/cmd/viewcore/html.go
+++ b/cmd/viewcore/html.go
@@ -411,6 +411,21 @@
return s + typeFieldName(t, off)
}
+// objRegion is like [objField], but doesn't print anything if pointing to the
+// start of typed objects.
+//
+// This is useful to avoid visual clutter when dealing with non-interior
+// pointers (offset 0), which could (in source code) be either a reference to
+// the entire object or to the first field. For example, suppose there is a
+// pointer to a struct like so:
+//
+// type x struct {
+// mu sync.Mutex
+// el []byte
+// }
+//
+// And there is user code holding a reference (&x), then objField(c, x, 0x0)
+// would return ".mu.state", while objRegion would return "".
func objRegion(c *gocore.Process, x gocore.Object, off int64) string {
t, r := c.Type(x)
if t == nil {
diff --git a/cmd/viewcore/main.go b/cmd/viewcore/main.go
index 8386507..0244589 100644
--- a/cmd/viewcore/main.go
+++ b/cmd/viewcore/main.go
@@ -630,73 +630,40 @@
exitf("can't find object at address %s\n", args[0])
}
- // Breadth-first search backwards until we reach a root.
- type hop struct {
- i int64 // offset in "from" object (the key in the path map) where the pointer is
- x gocore.Object // the "to" object
- j int64 // the offset in the "to" object
- }
- depth := map[gocore.Object]int{}
- depth[obj] = 0
- q := []gocore.Object{obj}
- done := false
- for !done {
- if len(q) == 0 {
- panic("can't find a root that can reach the object")
+ for r, path := range c.Reachable(obj) {
+ if r.Frame == nil {
+ // Print global
+ fmt.Printf("%s", r.Name)
+ } else {
+ // Print stack up to frame in question.
+ var frames []*gocore.Frame
+ for f := r.Frame.Parent(); f != nil; f = f.Parent() {
+ frames = append(frames, f)
+ }
+ for k := len(frames) - 1; k >= 0; k-- {
+ fmt.Printf("%s\n", frames[k].Func().Name())
+ }
+ // Print frame + variable in frame.
+ fmt.Printf("%s.%s", r.Frame.Func().Name(), r.Name)
}
- y := q[0]
- q = q[1:]
- c.ForEachReversePtr(y, func(x gocore.Object, r *gocore.Root, i, j int64) bool {
- if r != nil {
- // found it.
- if r.Frame == nil {
- // Print global
- fmt.Printf("%s", r.Name)
- } else {
- // Print stack up to frame in question.
- var frames []*gocore.Frame
- for f := r.Frame.Parent(); f != nil; f = f.Parent() {
- frames = append(frames, f)
- }
- for k := len(frames) - 1; k >= 0; k-- {
- fmt.Printf("%s\n", frames[k].Func().Name())
- }
- // Print frame + variable in frame.
- fmt.Printf("%s.%s", r.Frame.Func().Name(), r.Name)
- }
- fmt.Printf("%s → \n", typeFieldName(r.Type, i))
+ for i, z := range path {
+ if i == 0 {
+ // TODO: Perhaps also print destination "region", to unify format with
+ // non-Root:
+ // fmt.Printf("%s → %s\n", typeFieldName(r.Type, z.SrcOff), objRegion(c, z.Dst, z.DstOff))
+ fmt.Printf("%s → \n", typeFieldName(r.Type, z.SrcOff))
+ } else {
+ prev := path[i-1]
+ fmt.Printf(" %s → %s\n", objField(c, prev.Dst, z.SrcOff), objRegion(c, z.Dst, z.DstOff))
+ }
+ fmt.Printf("%x %s", c.Addr(z.Dst), typeName(c, z.Dst))
+ }
+ fmt.Println()
- z := y
- for {
- fmt.Printf("%x %s", c.Addr(z), typeName(c, z))
- if z == obj {
- fmt.Println()
- break
- }
- // Find an edge out of z which goes to an object
- // closer to obj.
- c.ForEachPtr(z, func(i int64, w gocore.Object, j int64) bool {
- if d, ok := depth[w]; ok && d < depth[z] {
- fmt.Printf(" %s → %s", objField(c, z, i), objRegion(c, w, j))
- z = w
- return false
- }
- return true
- })
- fmt.Println()
- }
- done = true
- return false
- }
- if _, ok := depth[x]; ok {
- // we already found a shorter path to this object.
- return true
- }
- depth[x] = depth[y] + 1
- q = append(q, x)
- return true
- })
+ return // Exit after a single path.
}
+
+ panic("can't find a root that can reach the object")
}
// httpServer is the singleton http server, initialized by
diff --git a/internal/gocore/gocore_test.go b/internal/gocore/gocore_test.go
index 268fbd7..fc4d091 100644
--- a/internal/gocore/gocore_test.go
+++ b/internal/gocore/gocore_test.go
@@ -11,10 +11,13 @@
"cmp"
"errors"
"fmt"
+ "maps"
"os"
"os/exec"
"path/filepath"
"runtime"
+ "slices"
+ "strconv"
"strings"
"testing"
@@ -36,8 +39,10 @@
return p
}
-// createAndLoadCore generates a core from a binary built with runtime.GOROOT().
-func createAndLoadCore(t *testing.T, srcFile string, buildFlags, env []string) *Process {
+// createAndLoadCore generates a core from a binary built with [runtime.GOROOT].
+// Returns the core as a [gocore.Process] and the crasher output (combined
+// stdout and stderr).
+func createAndLoadCore(t *testing.T, srcFile string, buildFlags, env []string) (proc *Process, output string) {
t.Helper()
testenv.MustHaveGoBuild(t)
switch runtime.GOOS {
@@ -56,12 +61,12 @@
}
dir := t.TempDir()
- file, output, err := generateCore(srcFile, dir, buildFlags, env)
- t.Logf("crasher output: %s", output)
+ file, out, err := generateCore(srcFile, dir, buildFlags, env)
+ t.Logf("crasher output: %s", out)
if err != nil {
t.Fatalf("generateCore() got err %v want nil", err)
}
- return loadCore(t, file, "", "")
+ return loadCore(t, file, "", ""), string(out)
}
func setupCorePattern(t *testing.T) func() {
@@ -140,7 +145,7 @@
// doRunCrasher spawns the supplied cmd, propagating parent state (see
// [exec.Cmd.Run]), and returns an error if the process failed to start or did
// *NOT* crash.
-func doRunCrasher(cmd *exec.Cmd) (pid int, output []byte, err error) {
+func doRunCrasher(cmd *exec.Cmd) (pid int, outputt []byte, err error) {
var b bytes.Buffer
cmd.Stdout = &b
cmd.Stderr = &b
@@ -262,7 +267,7 @@
for _, test := range variations {
for _, src := range testSrcFiles(t) {
t.Run(test.String()+"/"+filepath.Base(src), func(t *testing.T) {
- p := createAndLoadCore(t, src, test.buildFlags, test.env)
+ p, _ := createAndLoadCore(t, src, test.buildFlags, test.env)
checkProcess(t, p)
})
}
@@ -277,7 +282,7 @@
for _, test := range variations {
t.Run(test.String(), func(t *testing.T) {
t.Run("bigslice.go", func(t *testing.T) {
- p := createAndLoadCore(t, "testdata/testprogs/bigslice.go", test.buildFlags, test.env)
+ p, _ := createAndLoadCore(t, "testdata/testprogs/bigslice.go", test.buildFlags, test.env)
// Statistics to check.
largeObjects := 0 // Number of objects larger than (or equal to largeObjectThreshold)
@@ -306,7 +311,7 @@
}
})
t.Run("large.go", func(t *testing.T) {
- p := createAndLoadCore(t, "testdata/testprogs/large.go", test.buildFlags, test.env)
+ p, _ := createAndLoadCore(t, "testdata/testprogs/large.go", test.buildFlags, test.env)
// Statistics to check.
largeObjects := 0 // Number of objects larger than (or equal to largeObjectThreshold)
@@ -324,7 +329,7 @@
}
})
t.Run("trees.go", func(t *testing.T) {
- p := createAndLoadCore(t, "testdata/testprogs/trees.go", test.buildFlags, test.env)
+ p, _ := createAndLoadCore(t, "testdata/testprogs/trees.go", test.buildFlags, test.env)
// Statistics to check.
n := 0
@@ -375,7 +380,7 @@
for _, test := range variations {
t.Run(test.String(), func(t *testing.T) {
t.Run("globals.go", func(t *testing.T) {
- p := createAndLoadCore(t, "testdata/testprogs/globals.go", test.buildFlags, test.env)
+ p, _ := createAndLoadCore(t, "testdata/testprogs/globals.go", test.buildFlags, test.env)
for _, g := range p.Globals() {
var want []bool
switch g.Name {
@@ -420,3 +425,206 @@
}
return name
}
+
+func TestReachable(t *testing.T) {
+ t.Run("goroot", func(t *testing.T) {
+ for _, test := range variations {
+ t.Run(test.String(), func(t *testing.T) {
+ p, output := createAndLoadCore(t, "testdata/testprogs/reachable.go", test.buildFlags, test.env)
+
+ // Find OBJPOINTER <addr> in output.
+ var addrStr string
+ for line := range strings.SplitSeq(output, "\n") {
+ if s, ok := strings.CutPrefix(line, "OBJPOINTER "); ok {
+ addrStr = s
+ break
+ }
+ }
+ if addrStr == "" {
+ t.Fatalf("OBJPOINTER not found in output")
+ }
+ addr, err := strconv.ParseUint(addrStr, 0, 64)
+ if err != nil {
+ t.Fatalf("can't parse %q as an object address: %v", addrStr, err)
+ }
+ obj, _ := p.FindObject(core.Address(addr))
+ if obj == 0 {
+ t.Fatalf("can't find object at address %s", addrStr)
+ }
+
+ var (
+ foundRoots = make(map[string]int)
+ numPaths int
+ numGlobalRoots int
+ )
+ for r, chain := range p.Reachable(obj) {
+ foundRoots[r.Name]++
+ numPaths++
+ if r.Frame == nil {
+ numGlobalRoots++ // Globals are roots without frames.
+ }
+
+ // Debug logging.
+ //
+ // This aims to be easy to read, without too much post-processing. For
+ // end-user output (e.g.: `reachable`), we may consider aggregating
+ // further.
+
+ // 1. Print a stack trace (if not global).
+ if r.Frame == nil {
+ t.Logf("[global]")
+ } else {
+ t.Logf("[goroutine]") // TODO(aktau): Print goroutine ID.
+ }
+ for _, fr := range slices.Backward(collectFrames(r.Frame)) {
+ // TODO: Print file/line information.
+ // TODO: We could print the binary PC (without load offset). But:
+ // Delve prints the PC with offset, `go tool objdump` without.
+ t.Logf("0x%x %s", fr.PC(), fr.Func().Name())
+ }
+
+ // 2. Print the object structure, starting with the root.
+ //
+ // NOTE: It's possible for two roots to have the exact same source
+ // offset: if there are multiple references in the source, but the
+ // compiler has physically deduplicated them.
+ var sb strings.Builder
+ fmt.Fprintf(&sb, "\t%s\t", r.Name)
+ for i, o := range chain {
+ if i == 0 {
+ loc := "[reg/imm]"
+ if r.HasAddress() {
+ loc = ""
+ if r.Frame != nil {
+ loc += fmt.Sprintf("[SP+0x%x]", r.Addr()-r.Frame.Min())
+ }
+ loc += fmt.Sprintf(" 0x%x", r.Addr())
+ }
+ fmt.Fprintf(&sb, "%s (%s%s)", loc, r.Type.String(), objRegionRaw(r.Type, 0, o.SrcOff))
+ }
+ fmt.Fprintf(&sb, " → 0x%x (%s%s)", p.Addr(o.Dst), typeName(p, o.Dst), objRegion(p, o.Dst, o.DstOff))
+ if i != 0 {
+ prev := chain[i-1]
+ // TODO(aktau): Make this follow a better format once we actually
+ // find multi-level objects. For example, if prev.dstOff ==
+ // o.srcOff, we can avoid repeating the same thing.
+ fmt.Fprintf(&sb, " | %s → %s", objField(p, prev.Dst, o.SrcOff), objRegion(p, o.Dst, o.DstOff))
+ }
+ }
+ t.Log(sb.String())
+ }
+
+ // TODO(aktau): Return all possible paths per root, not just the first one.
+ // TODO(aktau): Ensure "compoundWrapperVal" in complicatedRetain is found.
+ expectedRoots := map[string]int{
+ "main.gPlainMyObj": 1, // global
+ "ref": 6, // This is actually one too many, see TODOs in reachable.go's renameRetain.
+ "otherRef": 4,
+ "unk": 4, // TODO(aktau): these are: compoundWrapperRef, anyWrapperRef, multiWrapperRef, multiWrapperRefReuse. Fix type resolution.
+ }
+ var totalRoots int
+ for name, want := range expectedRoots {
+ if got := foundRoots[name]; got != want {
+ t.Errorf("root %q: got %d paths, want %d", name, got, want)
+ }
+ totalRoots += want
+ }
+ if numPaths != totalRoots {
+ t.Errorf("got %d total roots, want %d", numPaths, totalRoots)
+ }
+ if got, want := len(foundRoots), 4; got != want {
+ t.Errorf("got %d unique roots (%v), want %d", got, slices.Collect(maps.Keys(foundRoots)), want)
+ }
+ if numGlobalRoots != 1 {
+ t.Errorf("got %d global roots, want 1", numGlobalRoots)
+ }
+ })
+ }
+ })
+}
+
+func collectFrames(fr *Frame) []*Frame {
+ var frames []*Frame
+ for ; /* f := fr.Parent()*/ fr != nil; fr = fr.Parent() {
+ frames = append(frames, fr)
+ }
+ return frames
+}
+
+// typeFieldName returns the name of the field at offset off in t.
+func typeFieldName(t *Type, off int64) string {
+ switch t.Kind {
+ case KindBool, KindInt, KindUint, KindFloat:
+ return ""
+ case KindComplex:
+ if off == 0 {
+ return ".real"
+ }
+ return ".imag"
+ case KindIface, KindEface:
+ if off == 0 {
+ return ".type"
+ }
+ return ".data"
+ case KindPtr, KindFunc:
+ return ""
+ case KindString:
+ if off == 0 {
+ return ".ptr"
+ }
+ return ".len"
+ case KindSlice:
+ if off == 0 {
+ return ".ptr"
+ }
+ if off <= t.Size/2 {
+ return ".len"
+ }
+ return ".cap"
+ case KindArray:
+ s := t.Elem.Size
+ i := off / s
+ return fmt.Sprintf("[%d]%s", i, typeFieldName(t.Elem, off-i*s))
+ case KindStruct:
+ for _, f := range t.Fields {
+ if f.Off <= off && off < f.Off+f.Type.Size {
+ return "." + f.Name + typeFieldName(f.Type, off-f.Off)
+ }
+ }
+ }
+ return ".???"
+}
+
+// Returns the name of the field at offset off in x.
+func objField(c *Process, x Object, off int64) string {
+ t, r := c.Type(x)
+ if t == nil {
+ return fmt.Sprintf("f+0x%x", off)
+ }
+ s := ""
+ if r > 1 {
+ s = fmt.Sprintf("[%d]", off/t.Size)
+ off %= t.Size
+ }
+ return s + typeFieldName(t, off)
+}
+
+func objRegion(c *Process, x Object, off int64) string {
+ t, r := c.Type(x)
+ return objRegionRaw(t, r, off)
+}
+
+func objRegionRaw(t *Type, r, off int64) string {
+ if t == nil {
+ return fmt.Sprintf("+0x%x", off)
+ }
+ if off == 0 {
+ return ""
+ }
+ s := ""
+ if r > 1 {
+ s = fmt.Sprintf("[%d]", off/t.Size)
+ off %= t.Size
+ }
+ return s + typeFieldName(t, off)
+}
diff --git a/internal/gocore/reverse.go b/internal/gocore/reverse.go
index bb611b2..43e0e50 100644
--- a/internal/gocore/reverse.go
+++ b/internal/gocore/reverse.go
@@ -4,6 +4,10 @@
package gocore
+import (
+ "iter"
+)
+
func (p *Process) reverseEdges() {
p.initReverseEdges.Do(func() {
// First, count the number of edges into each object.
@@ -106,3 +110,112 @@
}
}
}
+
+// A Link represents a pointer to [Link.Dst].
+//
+// [Link.DstOff] is non-zero if the pointer is an interior pointer.
+//
+// [Link.SrcOff] is non-zero if the location of the pointer itself is in the
+// interior of the source. The source object itself is not in the Link struct.
+// See either the previous Link or the [Root].
+//
+// An example, assuming objects "src" and "dst" exist:
+//
+// src{
+// a // ...
+// b // ...
+// ptr *someType
+// }
+//
+// dst{
+// c // ...
+// d // ...
+// someType // ...
+// }
+//
+// Then the Link representing the connection will be
+//
+// Link{
+// SrcOff: &src.ptr - &src,
+// Dst: &dst,
+// DstOff: &dst.someType - &dst,
+// }
+//
+// TODO(aktau): How are Roots that _are_ the object we're looking for themselves
+// materialized?
+type Link struct {
+ Dst Object // Object containing the pointed-to address.
+ SrcOff int64 // Offset in the source [Object] or [Root] where this pointer was found. The location of the pointer is p.Addr(source).Add(SrcOff).
+ DstOff int64 // Offset into Dst. The pointed-to address is p.Addr(Dst).Add(DstOff).
+}
+
+// Reachable returns an iterator that yields a path (slice of [Link]s) for every
+// [Root] that refers to obj.
+//
+// The order in which roots are returned is undefined but deterministic.
+// Similarly for multiple paths out of a given root: a shortest path is
+// returned. Which one is undefined but deterministic.
+//
+// The link slice always contains at least one element, representing the link
+// between the root and the object.
+//
+// The link slice (path) is invalidated on every iteration. The elements are
+// values, so (e.g.) [slices.Clone] can be used to retain the path.
+func (p *Process) Reachable(obj Object) iter.Seq2[*Root, []Link] {
+ return func(yield func(*Root, []Link) bool) {
+ depth := map[Object]int{}
+ depth[obj] = 0
+ var path []Link // Path slice is reused over all returned elements.
+ q := []Object{obj}
+ var stop bool
+ for len(q) > 0 && !stop {
+ // Follow the reverse pointers Depth-First (FIFO).
+ y := q[0]
+ q = q[1:]
+
+ p.ForEachReversePtr(y, func(x Object, r *Root, i, j int64) bool {
+ if r != nil {
+ // Found a root. Reconstruct a path.
+ path = path[:0] // Re-use memory.
+ path = append(path, Link{
+ SrcOff: i, // i: the offset in [r] where the pointer resides.
+ Dst: y,
+ DstOff: j, // j: the offset in [y] where the pointer points.
+ })
+
+ // Object hierarchy.
+ for z := y; z != obj; {
+ // Find an edge out of z which goes to an object closer to obj.
+ p.ForEachPtr(z, func(i int64, w Object, j int64) bool {
+ if d, ok := depth[w]; ok && d < depth[z] {
+ path = append(path, Link{
+ SrcOff: i, // the offset in [z] where the pointer resides.
+ Dst: w,
+ DstOff: j, // the offset in [w] where the pointer points.
+ })
+ z = w
+ return false
+ }
+ return true
+ })
+ }
+
+ if !yield(r, path) {
+ stop = true
+ return false
+ }
+ return true
+ }
+
+ // Reverse pointer to a non-root. Add this object to the work list
+ // (unless we've already seen it).
+ if _, ok := depth[x]; ok {
+ return true
+ }
+ depth[x] = depth[y] + 1
+ q = append(q, x)
+ return true
+ })
+ }
+ }
+}
diff --git a/internal/gocore/testdata/testprogs/reachable.go b/internal/gocore/testdata/testprogs/reachable.go
new file mode 100644
index 0000000..2496298
--- /dev/null
+++ b/internal/gocore/testdata/testprogs/reachable.go
@@ -0,0 +1,243 @@
+// Copyright 2025 The Go Authors. All rights reserved.
+// Use of this srcFile code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build ignore
+
+// Tests to make sure Reachable finds all paths.
+//
+// Note, the goroutine roots are all marked go:noinline. Without it, they look
+// like a single frame:
+//
+// gocore_test.go:498: [goroutine]
+// gocore_test.go:505: 0x55a67cc8e53b main.main.func1.gowrap3
+// gocore_test.go:533: unk (SP+0xb8) 0xc00004b7c8 (unsafe.Pointer) → 0xc000078000 (main.myObj)
+
+package main
+
+import (
+ "os"
+ "unsafe"
+
+ "golang.org/x/debug/internal/testenv"
+)
+
+type myObj struct {
+ x []byte
+ y complex128
+ z func()
+ w [12]int
+ v string
+}
+
+type anyWrapper struct {
+ container any
+}
+
+type compoundWrapper struct {
+ x int
+ container *myObj
+}
+
+// multiWrapper contains within it multiple paths to a [myObj].
+type multiWrapper struct {
+ mx int
+ container *myObj
+ my int
+ sub *compoundWrapper
+}
+
+var (
+ gPlainMyObj *myObj
+ // TODO: uncommenting and assigning this makes [gPlainMyObj] invisible
+ // in favour of [gCompoundWrapper]. That shouldn't happen
+ // gCompoundWrapper compoundWrapper
+)
+
+func main() {
+ testenv.RunThenCrash(os.Getenv("GO_DEBUG_TEST_COREDUMP_FILTER"), func() any {
+ obj := &myObj{v: "oh my"}
+ println("OBJPOINTER", obj) // Give the test driver a pointer value it can search for.
+
+ ready := make(chan struct{})
+ done := make(chan struct{})
+
+ // Goroutine roots.
+ {
+ go retain(obj, ready, done) // ref
+ go multiFrame1Retain(obj, ready, done) // ref
+
+ // Renaming.
+ go renameRetain(obj, ready, done) // otherRef (but keeps ref too, which is a bug)
+ go hardRenameRetain(obj, ready, done) // otherRef
+
+ // Aliasing.
+ go aliasRetain(obj, ready, done) // ref, otherRef
+ go hardAliasRetain(obj, ready, done) // ref, otherRef
+
+ // Compound objects.
+ go complicatedRetain(obj, ready, done) // ref, anyWrapperRef, compoundWrapperRef, multiWrapperRefReuse, multiWrapperRef (should have compoundWrapper too, but that's a bug)
+ }
+
+ // Global roots.
+ gPlainMyObj = obj
+ // gCompoundWrapper.container = obj // TODO: see TODO on global.
+
+ obj = nil // Nil out so we don't see a reference from main.
+ <-ready
+ <-ready
+ <-ready
+ <-ready
+ <-ready
+ <-ready
+ <-ready
+ return nil
+ })
+}
+
+//go:noinline
+func complicatedRetain(ref *myObj, ready, done chan struct{}) {
+ // TODO(aktau): fix the anyWrapper being unnamed (name: "unk").
+ anyWrapperRef := &anyWrapper{container: ref}
+ // TODO(aktau): fix the compoundWrapper being unnamed (name: "unk").
+ compoundWrapperRef := &compoundWrapper{container: ref}
+ // TODO(aktau): fix the multiWrapper being unnamed (name: "unk").
+ // TODO(aktau): fix only one path being printed for this object.
+ multiWrapperRefReuse := &multiWrapper{container: ref, sub: compoundWrapperRef}
+ // TODO(aktau): fix the multiWrapper being unnamed (name: "unk").
+ // TODO(aktau): fix only one path being printed for this object.
+ multiWrapperRef := &multiWrapper{container: ref, sub: &compoundWrapper{container: ref}}
+ // TODO(aktau): fix the reference not being found at all if we don't take the
+ // address of anyWrapper/compoundWrapper.
+ compoundWrapperVal := compoundWrapper{container: ref}
+
+ ready <- struct{}{}
+ // PC is here at crash time.
+ <-done
+
+ // Print the values so the GC doesn't consider them dead. It also makes it
+ // easier to get the association of which value is at which (stack) position
+ // by reading the runtime.print(pointer|string|eface) calls in the disasm.
+ println("I am retaining", ref.v, "which should be the same as",
+ anyWrapperRef,
+ anyWrapperRef.container,
+ compoundWrapperRef.container.v,
+ compoundWrapperRef,
+ compoundWrapperVal.container.v,
+ multiWrapperRefReuse,
+ multiWrapperRefReuse.container,
+ multiWrapperRefReuse.container.v,
+ multiWrapperRefReuse.sub,
+ multiWrapperRefReuse.sub.container.v,
+ multiWrapperRef,
+ multiWrapperRef.container,
+ multiWrapperRef.container.v,
+ multiWrapperRef.sub,
+ multiWrapperRef.sub.container.v,
+ )
+}
+
+// retain retains a reference to the incoming ref.
+//
+//go:noinline
+func retain(ref *myObj, ready, done chan struct{}) {
+ ready <- struct{}{}
+ <-done
+ println("I am retaining", ref.v)
+}
+
+// renameRetain is like [retain], but renames the incoming parameter [ref] to
+// [otherRef].
+//
+//go:noinline
+func renameRetain(ref *myObj, ready, done chan struct{}) {
+ otherRef := ref // After this, there are no more references to ref.
+
+ // TODO: viewcore sees ref as live. It shouldn't. This might be something
+ // subtle in DWARF, as the compiler has already "deduplicated" the memory
+ // location (essentially renaming).
+ //
+ // gocore_test.go:498: [goroutine]
+ // gocore_test.go:505: 0x5625676167a8 main.main.func1.gowrap2
+ // gocore_test.go:505: 0x562567616273 main.renameRetain
+ // gocore_test.go:533: otherRef (SP+0x30) 0xc00004afb8 (*main.myObj) → 0xc000078000 (main.myObj)
+ //
+ // gocore_test.go:498: [goroutine]
+ // gocore_test.go:505: 0x5625676167a8 main.main.func1.gowrap2
+ // gocore_test.go:505: 0x562567616273 main.renameRetain
+ // gocore_test.go:533: ref (SP+0x30) 0xc00004afb8 (*main.myObj) → 0xc000078000 (main.myObj)
+ //
+ // See the equivalent [hardRenameParamRetain], which is effectively the same,
+ // but viewcore no longer sees ref as live, further implying DWARF.
+ ref = nil // This shouldn't even be necessary, as Go knows the liveness range. But it's not working for viewcore any way.
+
+ ready <- struct{}{}
+ // PC is here at crash time.
+ <-done
+
+ println("I am retaining", otherRef.v) // Print the values so the GC doesn't consider them dead
+}
+
+// hardRenameRetain is like [renameRetain], but obscures the renaming to the
+// compiler.
+//
+//go:noinline
+func hardRenameRetain(ref *myObj, ready, done chan struct{}) {
+ otherRef := unalias(ref)
+
+ ready <- struct{}{}
+ // PC is here at crash time.
+ <-done
+
+ println("I am retaining", otherRef.v) // Print the values so the GC doesn't consider them dead
+}
+
+// multiFrame1Retain is just an empty frame to "complexify" the program and make
+// stack traces stand out more.
+//
+//go:noinline
+func multiFrame1Retain(ref *myObj, ready, done chan struct{}) { multiFrame2Retain(ref, ready, done) }
+
+//go:noinline
+func multiFrame2Retain(ref *myObj, ready, done chan struct{}) { multiFrame3Retain(ref, ready, done) }
+
+//go:noinline
+func multiFrame3Retain(ref *myObj, ready, done chan struct{}) { multiFrame4Retain(ref, ready, done) }
+
+// NOTE: no go:noinline marker to test this variant
+func multiFrame4Retain(ref *myObj, ready, done chan struct{}) { retain(ref, ready, done) }
+
+// aliasRetain is like [renameRetain], but keeps the original name
+// (ref) intact.
+//
+//go:noinline
+func aliasRetain(ref *myObj, ready, done chan struct{}) {
+ otherRef := ref
+
+ ready <- struct{}{}
+ // PC is here at crash time.
+ <-done
+
+ println("I am retaining", ref.v, "which should be the same as", otherRef.v) // Print the values so the GC doesn't consider them dead.
+}
+
+// hardAliasRetain is like [aliasRetain], but tries harder to avoid
+// the compiler aliasing the roots to the same memory location.
+//
+//go:noinline
+func hardAliasRetain(ref *myObj, ready, done chan struct{}) {
+ otherRef := unalias(ref)
+
+ ready <- struct{}{}
+ // PC is here at crash time.
+ <-done
+
+ println("I am retaining", ref.v, "which should be the same as", otherRef.v) // Print the values so the GC doesn't consider them dead.
+}
+
+var off uintptr = 0x0 // Throw the compiler off.
+
+//go:noinline
+func unalias(in *myObj) *myObj {
+ return (*myObj)(unsafe.Add(unsafe.Pointer(in), off))
+}