gocore: update for Go 1.24

This change drops support for old versions of Go and adds numerous
updates to support Go 1.24.

- Correct DWARF variable parsing via location lists and complete
  interpretation of DWARF expressions (with the help of a couple
  Delve packages).
- Fixed handling of sigtrampgo and unwinding out of the signal handler.
- Improved handling of frames that aren't in safe points by
  conservatively finding live pointers through DWARF types of live
  local variables.
- Partial support for values live in registers (much more to do here,
  and composites are not supported).
- Completely overhauled Process initialization to better track
  dependencies.
- Clean up of "page table" data structure to be better encapsulated and
  more explicitly about the heap.
- Fix for a field in a runtime.special moving from a uint16 to a
  uintptr.
- Overhaul of gocore.Stats -> gocore.Statistic.
- Update go.mod to go1.23 for iterators.
- Update some dependencies.
- Adds TestObject from go.dev/cl/634756 (thanks aktau@!).

The main milestone this CL reaches is that the main.Large value in the
core test is consistently found and correctly typed.

There is still a lot left to do.

- Anything interpreting map structure is completely wrong with
  the change to Swiss Tables.
- Full support for roots that are in registers. (This matters, because
  otherwise values that are only in a register in the crash context are
  completely missed.)
- Full support for values that are partially in registers (composites).
- Tons of cleanup.
- A better abstraction over arches (so much stuff is hard-coded for
  amd64).

Change-Id: I9c671c15128b72ee118b3623aa403477b91f5ce6
Reviewed-on: https://go-review.googlesource.com/c/debug/+/635227
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Nicolas Hillegeer <aktau@google.com>
Auto-Submit: Nicolas Hillegeer <aktau@google.com>
Commit-Queue: Nicolas Hillegeer <aktau@google.com>
diff --git a/cmd/viewcore/html.go b/cmd/viewcore/html.go
index 58c7824..1b16c14 100644
--- a/cmd/viewcore/html.go
+++ b/cmd/viewcore/html.go
@@ -194,11 +194,11 @@
 		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 {
+		all := c.Stats().Value
+		var p func(*gocore.Statistic, string)
+		p = func(s *gocore.Statistic, 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.Value, float64(s.Value)/float64(all)*100)
+			for c := range s.Children() {
 				p(c, prefix+"..")
 			}
 		}
diff --git a/cmd/viewcore/main.go b/cmd/viewcore/main.go
index 9f0d999..f1106de 100644
--- a/cmd/viewcore/main.go
+++ b/cmd/viewcore/main.go
@@ -512,9 +512,9 @@
 		exitf("%v\n", err)
 	}
 	t := tabwriter.NewWriter(os.Stdout, 0, 8, 1, ' ', tabwriter.AlignRight)
-	all := c.Stats().Size
-	var printStat func(*gocore.Stats, string)
-	printStat = func(s *gocore.Stats, indent string) {
+	all := c.Stats().Value
+	var printStat func(*gocore.Statistic, string)
+	printStat = func(s *gocore.Statistic, indent string) {
 		comment := ""
 		switch s.Name {
 		case "bss":
@@ -526,8 +526,8 @@
 		case "released":
 			comment = "(given back to the OS)"
 		}
-		fmt.Fprintf(t, "%s\t%d\t%6.2f%%\t %s\n", fmt.Sprintf("%-20s", indent+s.Name), s.Size, float64(s.Size)*100/float64(all), comment)
-		for _, c := range s.Children {
+		fmt.Fprintf(t, "%s\t%d\t%6.2f%%\t %s\n", fmt.Sprintf("%-20s", indent+s.Name), s.Value, float64(s.Value)*100/float64(all), comment)
+		for c := range s.Children() {
 			printStat(c, indent+"  ")
 		}
 	}
diff --git a/go.mod b/go.mod
index 2af987b..9e03bc5 100644
--- a/go.mod
+++ b/go.mod
@@ -1,16 +1,17 @@
 module golang.org/x/debug
 
-go 1.18
+go 1.23
 
 require (
 	github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
-	github.com/spf13/cobra v0.0.3
-	github.com/spf13/pflag v1.0.3
+	github.com/go-delve/delve v1.23.1
+	github.com/spf13/cobra v1.7.0
+	github.com/spf13/pflag v1.0.5
 	golang.org/x/sys v0.28.0
 )
 
 require (
 	github.com/chzyer/logex v1.1.10 // indirect
 	github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
-	github.com/inconshreveable/mousetrap v1.0.0 // indirect
+	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 )
diff --git a/go.sum b/go.sum
index 5ab7155..11c30c1 100644
--- a/go.sum
+++ b/go.sum
@@ -4,11 +4,17 @@
 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
-github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
-github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
-github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8=
-github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
-github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
-github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/go-delve/delve v1.23.1 h1:MtZ13ppptttkqSuvVnwJ5CPhIAzDiOwRrYuCk3ES7fU=
+github.com/go-delve/delve v1.23.1/go.mod h1:S3SLuEE2mn7wipKilTvk1p9HdTMnXXElcEpiZ+VcuqU=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
+github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
 golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/internal/core/process.go b/internal/core/process.go
index 74400e4..22bb8b5 100644
--- a/internal/core/process.go
+++ b/internal/core/process.go
@@ -51,6 +51,7 @@
 	symErr   error              // an error encountered while reading symbols
 	dwarf    *dwarf.Data        // debugging info (could be nil)
 	dwarfErr error              // an error encountered while reading DWARF
+	dwarfLoc []byte             // .debug_loc section
 
 	warnings []string // warnings generated during loading
 }
@@ -176,6 +177,10 @@
 	return p.dwarf, p.dwarfErr
 }
 
+func (p *Process) DWARFLoc() ([]byte, error) {
+	return p.dwarfLoc, p.dwarfErr
+}
+
 // Symbols returns a mapping from name to inferior address, along with
 // any error encountered during reading the symbol information.
 // (There may be both an error and some returned symbols.)
@@ -274,6 +279,14 @@
 	if dwarfErr != nil {
 		dwarfErr = fmt.Errorf("error reading DWARF info from %s: %v", exeFile.Name(), dwarfErr)
 	}
+	var dwarfLoc []byte
+	if locSection := exeElf.Section(".debug_loc"); locSection != nil {
+		var err error
+		dwarfLoc, err = locSection.Data()
+		if err != nil && dwarfErr == nil {
+			dwarfErr = fmt.Errorf("error reading DWARF location list section from %s: %v", exeFile.Name(), err)
+		}
+	}
 
 	// Sort then merge mappings, just to clean up a bit.
 	mappings := mem.mappings
@@ -363,6 +376,7 @@
 		symErr:     symErr,
 		dwarf:      dwarf,
 		dwarfErr:   dwarfErr,
+		dwarfLoc:   dwarfLoc,
 		warnings:   warnings,
 	}
 
@@ -673,39 +687,40 @@
 			t.pid = uint64(meta.byteOrder.Uint32(desc[32 : 32+4]))
 			// 112 = offsetof(prstatus_t, pr_reg), 216 = sizeof(elf_gregset_t)
 			reg := desc[112 : 112+216]
-			for i := 0; i < len(reg); i += 8 {
-				t.regs = append(t.regs, meta.byteOrder.Uint64(reg[i:]))
+			i := 0
+			readReg := func(name string) uint64 {
+				value := meta.byteOrder.Uint64(reg[i:])
+				t.regs = append(t.regs, Register{Name: name, Value: value})
+				i += 8
+				return value
 			}
-			// Registers are:
-			//  0: r15
-			//  1: r14
-			//  2: r13
-			//  3: r12
-			//  4: rbp
-			//  5: rbx
-			//  6: r11
-			//  7: r10
-			//  8: r9
-			//  9: r8
-			// 10: rax
-			// 11: rcx
-			// 12: rdx
-			// 13: rsi
-			// 14: rdi
-			// 15: orig_rax
-			// 16: rip
-			// 17: cs
-			// 18: eflags
-			// 19: rsp
-			// 20: ss
-			// 21: fs_base
-			// 22: gs_base
-			// 23: ds
-			// 24: es
-			// 25: fs
-			// 26: gs
-			t.pc = Address(t.regs[16])
-			t.sp = Address(t.regs[19])
+			readReg("r15")
+			readReg("r14")
+			readReg("r13")
+			readReg("r12")
+			readReg("rbp")
+			readReg("rbx")
+			readReg("r11")
+			readReg("r10")
+			readReg("r9")
+			readReg("r8")
+			readReg("rax")
+			readReg("rcx")
+			readReg("rdx")
+			readReg("rsi")
+			readReg("rdi")
+			readReg("orig_rax")
+			t.pc = Address(readReg("rip"))
+			readReg("cs")
+			readReg("eflags")
+			t.sp = Address(readReg("rsp"))
+			readReg("ss")
+			readReg("fs_base")
+			readReg("gs_base")
+			readReg("ds")
+			readReg("es")
+			readReg("fs")
+			readReg("gs")
 
 			// TODO: NT_FPREGSET for floating-point registers.
 			//
diff --git a/internal/core/thread.go b/internal/core/thread.go
index 8c7517e..61c1af1 100644
--- a/internal/core/thread.go
+++ b/internal/core/thread.go
@@ -6,10 +6,15 @@
 
 // A Thread represents an operating system thread.
 type Thread struct {
-	pid  uint64   // thread/process ID
-	regs []uint64 // set depends on arch
-	pc   Address  // program counter
-	sp   Address  // stack pointer
+	pid  uint64     // thread/process ID
+	regs []Register // set depends on arch
+	pc   Address    // program counter
+	sp   Address    // stack pointer
+}
+
+type Register struct {
+	Name  string
+	Value uint64
 }
 
 func (t *Thread) Pid() uint64 {
@@ -18,9 +23,7 @@
 
 // Regs returns the set of register values for the thread.
 // What registers go where is architecture-dependent.
-// TODO: document for each architecture.
-// TODO: do this in some arch-independent way?
-func (t *Thread) Regs() []uint64 {
+func (t *Thread) Regs() []Register {
 	return t.regs
 }
 
diff --git a/internal/gocore/dominator_test.go b/internal/gocore/dominator_test.go
index 699169e..2f9c85d 100644
--- a/internal/gocore/dominator_test.go
+++ b/internal/gocore/dominator_test.go
@@ -8,27 +8,9 @@
 
 import (
 	"fmt"
-	"os"
 	"testing"
 )
 
-func TestLT(t *testing.T) {
-	p := loadExample(t)
-	lt := runLT(p)
-	checkDominator(t, lt)
-	if false {
-		lt.dot(os.Stdout)
-	}
-}
-
-func TestDominators(t *testing.T) {
-	p := loadExample(t)
-	d := p.calculateDominators()
-	if size := d.size[pseudoRoot]; size < 100<<10 {
-		t.Errorf("total size of objects is only %v bytes, should be >100KiB", size)
-	}
-}
-
 func checkDominator(t *testing.T, d ltDom) bool {
 	t.Helper()
 	// Build pointer-y graph.
diff --git a/internal/gocore/dwarf.go b/internal/gocore/dwarf.go
index e8f9497..53e711a 100644
--- a/internal/gocore/dwarf.go
+++ b/internal/gocore/dwarf.go
@@ -6,13 +6,16 @@
 
 import (
 	"debug/dwarf"
+	"errors"
 	"fmt"
 	"reflect"
-	"regexp"
-	"sort"
 	"strings"
 
 	"golang.org/x/debug/internal/core"
+
+	"github.com/go-delve/delve/pkg/dwarf/loclist"
+	"github.com/go-delve/delve/pkg/dwarf/op"
+	"github.com/go-delve/delve/pkg/dwarf/regnum"
 )
 
 const (
@@ -20,8 +23,12 @@
 )
 
 // read DWARF types from core dump.
-func (p *Process) readDWARFTypes() {
-	d, _ := p.proc.DWARF()
+func readDWARFTypes(p *core.Process) (map[dwarf.Type]*Type, error) {
+	d, err := p.DWARF()
+	if err != nil {
+		return nil, fmt.Errorf("failed to read DWARF: %v", err)
+	}
+	dwarfMap := make(map[dwarf.Type]*Type)
 
 	// Make one of our own Types for each dwarf type.
 	r := d.Reader()
@@ -37,35 +44,33 @@
 			if err != nil {
 				continue
 			}
-			t := &Type{Name: gocoreName(dt), Size: dwarfSize(dt, p.proc.PtrSize())}
+			t := &Type{Name: gocoreName(dt), Size: dwarfSize(dt, p.PtrSize())}
 			if goKind, ok := e.Val(AttrGoKind).(int64); ok {
 				t.goKind = reflect.Kind(goKind)
 			}
-			p.dwarfMap[dt] = t
+			dwarfMap[dt] = t
 			types = append(types, t)
 		}
 	}
 
-	p.runtimeNameMap = map[string][]*Type{}
-
 	// Fill in fields of types. Postponed until now so we're sure
 	// we have all the Types allocated and available.
-	for dt, t := range p.dwarfMap {
+	for dt, t := range dwarfMap {
 		switch x := dt.(type) {
 		case *dwarf.ArrayType:
 			t.Kind = KindArray
-			t.Elem = p.dwarfMap[x.Type]
+			t.Elem = dwarfMap[x.Type]
 			t.Count = x.Count
 		case *dwarf.PtrType:
 			t.Kind = KindPtr
 			// unsafe.Pointer has a void base type.
 			if _, ok := x.Type.(*dwarf.VoidType); !ok {
-				t.Elem = p.dwarfMap[x.Type]
+				t.Elem = dwarfMap[x.Type]
 			}
 		case *dwarf.StructType:
 			t.Kind = KindStruct
 			for _, f := range x.Field {
-				fType := p.dwarfMap[f.Type]
+				fType := dwarfMap[f.Type]
 				if fType == nil {
 					// Weird case: arrays of size 0 in structs, like
 					// Sysinfo_t.X_f. Synthesize a type so things later don't
@@ -75,24 +80,14 @@
 							Name:  f.Type.String(),
 							Kind:  KindArray,
 							Count: arr.Count,
-							Elem:  p.dwarfMap[arr.Type],
+							Elem:  dwarfMap[arr.Type],
 						}
 					} else {
-						panic(fmt.Sprintf(
+						return nil, fmt.Errorf(
 							"found a nil ftype for field %s.%s, type %s (%s) on ",
-							x.StructName, f.Name, f.Type, reflect.TypeOf(f.Type)))
+							x.StructName, f.Name, f.Type, reflect.TypeOf(f.Type))
 					}
 				}
-
-				// Work around issue 21094. There's no guarantee that the
-				// pointer type is in the DWARF, so just invent a Type.
-				if strings.HasPrefix(t.Name, "sudog<") && f.Name == "elem" &&
-					strings.Count(t.Name, "*")+1 != strings.Count(gocoreName(f.Type), "*") {
-					ptrName := "*" + gocoreName(f.Type)
-					fType = &Type{Name: ptrName, Kind: KindPtr, Size: p.proc.PtrSize(), Elem: fType}
-					p.runtimeNameMap[ptrName] = []*Type{fType}
-				}
-
 				t.Fields = append(t.Fields, Field{Name: f.Name, Type: fType, Off: f.ByteOffset})
 			}
 		case *dwarf.BoolType:
@@ -110,7 +105,7 @@
 		case *dwarf.TypedefType:
 			// handle these types in the loop below
 		default:
-			panic(fmt.Sprintf("unknown type %s %T", dt, dt))
+			return nil, fmt.Errorf("unknown type %s %T", dt, dt)
 		}
 	}
 
@@ -132,7 +127,7 @@
 	}
 
 	// Copy info from base types into typedefs.
-	for dt, t := range p.dwarfMap {
+	for dt, t := range dwarfMap {
 		tt, ok := dt.(*dwarf.TypedefType)
 		if !ok {
 			continue
@@ -146,7 +141,7 @@
 			}
 			break
 		}
-		bt := p.dwarfMap[base]
+		bt := dwarfMap[base]
 
 		// Copy type info from base. Everything except the name.
 		name := t.Name
@@ -169,59 +164,7 @@
 			t.Fields = nil
 		}
 	}
-
-	// Make a runtime name -> Type map for existing DWARF types.
-	for dt, t := range p.dwarfMap {
-		name := runtimeName(dt)
-		p.runtimeNameMap[name] = append(p.runtimeNameMap[name], t)
-	}
-
-	// Construct the runtime.specialfinalizer type.  It won't be found
-	// in DWARF before 1.10 because it does not appear in the type of any variable.
-	// type specialfinalizer struct {
-	//      special special
-	//      fn      *funcval
-	//      nret    uintptr
-	//      fint    *_type
-	//      ot      *ptrtype
-	// }
-	if p.runtimeNameMap["runtime.specialfinalizer"] == nil {
-		special := p.findType("runtime.special")
-		p.runtimeNameMap["runtime.specialfinalizer"] = []*Type{
-			&Type{
-				Name: "runtime.specialfinalizer",
-				Size: special.Size + 4*p.proc.PtrSize(),
-				Kind: KindStruct,
-				Fields: []Field{
-					Field{
-						Name: "special",
-						Off:  0,
-						Type: special,
-					},
-					Field{
-						Name: "fn",
-						Off:  special.Size,
-						Type: p.findType("*runtime.funcval"),
-					},
-					Field{
-						Name: "nret",
-						Off:  special.Size + p.proc.PtrSize(),
-						Type: p.findType("uintptr"),
-					},
-					Field{
-						Name: "fint",
-						Off:  special.Size + 2*p.proc.PtrSize(),
-						Type: p.findType("*runtime._type"),
-					},
-					Field{
-						Name: "fn",
-						Off:  special.Size + 3*p.proc.PtrSize(),
-						Type: p.findType("*runtime.ptrtype"),
-					},
-				},
-			},
-		}
-	}
+	return dwarfMap, nil
 }
 
 func isNonGoCU(e *dwarf.Entry) bool {
@@ -298,104 +241,14 @@
 	}
 }
 
-// Generate the name the runtime uses for a dwarf type. The DWARF generator
-// and the runtime use slightly different names for the same underlying type.
-func runtimeName(dt dwarf.Type) string {
-	switch x := dt.(type) {
-	case *dwarf.PtrType:
-		if _, ok := x.Type.(*dwarf.VoidType); ok {
-			return "unsafe.Pointer"
-		}
-		return "*" + runtimeName(x.Type)
-	case *dwarf.ArrayType:
-		return fmt.Sprintf("[%d]%s", x.Count, runtimeName(x.Type))
-	case *dwarf.StructType:
-		if !strings.HasPrefix(x.StructName, "struct {") {
-			// This is a named type, return that name.
-			return stripPackagePath(x.StructName)
-		}
-		// Figure out which fields have anonymous names.
-		var anon []bool
-		for _, f := range strings.Split(x.StructName[8:len(x.StructName)-1], ";") {
-			f = strings.TrimSpace(f)
-			anon = append(anon, !strings.Contains(f, " "))
-			// TODO: this isn't perfect. If the field type has a space in it,
-			// then this logic doesn't work. Need to search for keyword for
-			// field type, like "interface", "struct", ...
-		}
-		// Make sure anon is long enough. This probably never triggers.
-		for len(anon) < len(x.Field) {
-			anon = append(anon, false)
-		}
-
-		// Build runtime name from the DWARF fields.
-		s := "struct {"
-		first := true
-		for _, f := range x.Field {
-			if !first {
-				s += ";"
-			}
-			name := f.Name
-			if i := strings.Index(name, "."); i >= 0 {
-				name = name[i+1:]
-			}
-			if anon[0] {
-				s += fmt.Sprintf(" %s", runtimeName(f.Type))
-			} else {
-				s += fmt.Sprintf(" %s %s", name, runtimeName(f.Type))
-			}
-			first = false
-			anon = anon[1:]
-		}
-		s += " }"
-		return s
-	default:
-		return stripPackagePath(dt.String())
+func readRuntimeConstants(p *core.Process) (map[string]int64, error) {
+	d, err := p.DWARF()
+	if err != nil {
+		return nil, fmt.Errorf("failed to read DWARF: %v", err)
 	}
-}
-
-var pathRegexp = regexp.MustCompile(`([\w.-]+/)+\w+`)
-
-func stripPackagePath(name string) string {
-	// The runtime uses just package names. DWARF uses whole package paths.
-	// To convert from the latter to the former, get rid of the package paths.
-	// Examples:
-	//   text/template.Template -> template.Template
-	//   map[string]compress/gzip.Writer -> map[string]gzip.Writer
-	return pathRegexp.ReplaceAllStringFunc(name, func(path string) string {
-		return path[strings.LastIndex(path, "/")+1:]
-	})
-}
-
-// readRuntimeConstants populates the p.rtConstants map.
-func (p *Process) readRuntimeConstants() {
-	p.rtConstants = map[string]int64{}
-
-	// Hardcoded values for Go 1.9.
-	// (Go did not have constants in DWARF before 1.10.)
-	m := p.rtConstants
-	m["_MSpanDead"] = 0
-	m["_MSpanInUse"] = 1
-	m["_MSpanManual"] = 2
-	m["_MSpanFree"] = 3
-	m["_Gidle"] = 0
-	m["_Grunnable"] = 1
-	m["_Grunning"] = 2
-	m["_Gsyscall"] = 3
-	m["_Gwaiting"] = 4
-	m["_Gdead"] = 6
-	m["_Gscan"] = 0x1000
-	m["_PCDATA_StackMapIndex"] = 0
-	m["_FUNCDATA_LocalsPointerMaps"] = 1
-	m["_FUNCDATA_ArgsPointerMaps"] = 0
-	m["tflagExtraStar"] = 1 << 1
-	m["kindGCProg"] = 1 << 6
-	m["kindDirectIface"] = 1 << 5
-	m["_PageSize"] = 1 << 13
-	m["_KindSpecialFinalizer"] = 1
+	consts := map[string]int64{}
 
 	// From 1.10, these constants are recorded in DWARF records.
-	d, _ := p.proc.DWARF()
 	r := d.Reader()
 	for e, err := r.Next(); e != nil && err == nil; e, err = r.Next() {
 		if e.Tag != dwarf.TagConstant {
@@ -414,8 +267,9 @@
 		if c == nil {
 			continue
 		}
-		p.rtConstants[name] = c.Val.(int64)
+		consts[name] = c.Val.(int64)
 	}
+	return consts, nil
 }
 
 const (
@@ -425,8 +279,12 @@
 	_DW_OP_consts         = 0x11
 )
 
-func (p *Process) readGlobals() {
-	d, _ := p.proc.DWARF()
+func readGlobals(p *core.Process, dwarfTypeMap map[dwarf.Type]*Type) ([]*Root, error) {
+	d, err := p.DWARF()
+	if err != nil {
+		return nil, fmt.Errorf("failed to read DWARF: %v", err)
+	}
+	var roots []*Root
 	r := d.Reader()
 	for e, err := r.Next(); e != nil && err == nil; e, err = r.Next() {
 		if isNonGoCU(e) {
@@ -450,13 +308,13 @@
 			continue
 		}
 		var a core.Address
-		if p.proc.PtrSize() == 8 {
-			a = core.Address(p.proc.ByteOrder().Uint64(loc[1:]))
+		if p.PtrSize() == 8 {
+			a = core.Address(p.ByteOrder().Uint64(loc[1:]))
 		} else {
-			a = core.Address(p.proc.ByteOrder().Uint32(loc[1:]))
+			a = core.Address(p.ByteOrder().Uint32(loc[1:]))
 		}
-		a = a.Add(int64(p.proc.StaticBase()))
-		if !p.proc.Writeable(a) {
+		a = a.Add(int64(p.StaticBase()))
+		if !p.Writeable(a) {
 			// Read-only globals can't have heap pointers.
 			// TODO: keep roots around anyway?
 			continue
@@ -467,7 +325,7 @@
 		}
 		dt, err := d.Type(f.Val.(dwarf.Offset))
 		if err != nil {
-			panic(err)
+			return nil, err
 		}
 		if _, ok := dt.(*dwarf.UnspecifiedType); ok {
 			continue // Ignore markers like data/edata.
@@ -476,24 +334,34 @@
 		if nf == nil {
 			continue
 		}
-		p.globals = append(p.globals, &Root{
+		roots = append(roots, &Root{
 			Name:  nf.Val.(string),
 			Addr:  a,
-			Type:  p.dwarfMap[dt],
+			Type:  dwarfTypeMap[dt],
 			Frame: nil,
 		})
 	}
+	return roots, nil
 }
 
-func (p *Process) readStackVars() {
-	type Var struct {
-		name string
-		off  int64
-		typ  *Type
+type dwarfVar struct {
+	lowPC, highPC core.Address
+	name          string
+	instr         []byte
+	typ           *Type
+}
+
+func readDWARFVars(p *core.Process, fns *funcTab, dwarfTypeMap map[dwarf.Type]*Type) (map[*Func][]dwarfVar, error) {
+	d, err := p.DWARF()
+	if err != nil {
+		return nil, fmt.Errorf("failed to read DWARF: %v", err)
 	}
-	vars := map[*Func][]Var{}
+	dLocSec, err := p.DWARFLoc()
+	if err != nil {
+		return nil, fmt.Errorf("failed to read DWARF: %v", err)
+	}
+	vars := make(map[*Func][]dwarfVar)
 	var curfn *Func
-	d, _ := p.proc.DWARF()
 	r := d.Reader()
 	for e, err := r.Next(); e != nil && err == nil; e, err = r.Next() {
 		if isNonGoCU(e) {
@@ -507,18 +375,18 @@
 			if lowpc == nil || highpc == nil {
 				continue
 			}
-			min := core.Address(lowpc.Val.(uint64))
-			max := core.Address(highpc.Val.(uint64))
-			f := p.funcTab.find(min)
+			min := core.Address(lowpc.Val.(uint64) + p.StaticBase())
+			max := core.Address(highpc.Val.(uint64) + p.StaticBase())
+			f := fns.find(min)
 			if f == nil {
 				// some func Go doesn't know about. C?
 				curfn = nil
 			} else {
 				if f.entry != min {
-					panic("dwarf and runtime don't agree about start of " + f.name)
+					return nil, errors.New("dwarf and runtime don't agree about start of " + f.name)
 				}
-				if p.funcTab.find(max-1) != f {
-					panic("function ranges don't match for " + f.name)
+				if fns.find(max-1) != f {
+					return nil, errors.New("function ranges don't match for " + f.name)
 				}
 				curfn = f
 			}
@@ -531,102 +399,61 @@
 		if aloc == nil {
 			continue
 		}
-		if aloc.Class != dwarf.ClassExprLoc {
-			// TODO: handle ClassLocListPtr here.
-			// As of go 1.11, locals are encoded this way.
-			// Until we fix this TODO, viewcore will not be able to
-			// show local variables.
+		if aloc.Class != dwarf.ClassLocListPtr {
 			continue
 		}
-		// Interpret locations of the form
-		//    DW_OP_call_frame_cfa
-		//    DW_OP_consts <off>
-		//    DW_OP_plus
-		// (with possibly missing DW_OP_consts & DW_OP_plus for the zero offset.)
-		// TODO: handle other possible locations (e.g. register locations).
-		loc := aloc.Val.([]byte)
-		if len(loc) == 0 || loc[0] != _DW_OP_call_frame_cfa {
-			continue
-		}
-		loc = loc[1:]
-		var off int64
-		if len(loc) != 0 && loc[0] == _DW_OP_consts {
-			loc = loc[1:]
-			var s uint
-			for len(loc) > 0 {
-				b := loc[0]
-				loc = loc[1:]
-				off += int64(b&0x7f) << s
-				s += 7
-				if b&0x80 == 0 {
-					break
-				}
-			}
-			off = off << (64 - s) >> (64 - s)
-			if len(loc) == 0 || loc[0] != _DW_OP_plus {
-				continue
-			}
-			loc = loc[1:]
-		}
-		if len(loc) != 0 {
-			continue // more stuff we don't recognize
-		}
+
+		// Read attributes for some high-level information.
 		f := e.AttrField(dwarf.AttrType)
 		if f == nil {
 			continue
 		}
 		dt, err := d.Type(f.Val.(dwarf.Offset))
 		if err != nil {
-			panic(err)
+			return nil, err
 		}
 		nf := e.AttrField(dwarf.AttrName)
 		if nf == nil {
 			continue
 		}
 		name := nf.Val.(string)
-		vars[curfn] = append(vars[curfn], Var{name: name, off: off, typ: p.dwarfMap[dt]})
-	}
 
-	// Get roots from goroutine stacks.
-	for _, g := range p.goroutines {
-		for _, f := range g.frames {
-			// Start with all pointer slots as unnamed.
-			unnamed := map[core.Address]bool{}
-			for a := range f.Live {
-				unnamed[a] = true
+		// Read the location list.
+		locListOff := aloc.Val.(int64)
+		dr := loclist.NewDwarf2Reader(dLocSec, int(p.PtrSize()))
+		dr.Seek(int(locListOff))
+		var base uint64
+		var e loclist.Entry
+		for dr.Next(&e) {
+			if e.BaseAddressSelection() {
+				base = e.HighPC + p.StaticBase()
+				continue
 			}
-			// Emit roots for DWARF entries.
-			for _, v := range vars[f.f] {
-				r := &Root{
-					Name:  v.name,
-					Addr:  f.max.Add(v.off),
-					Type:  v.typ,
-					Frame: f,
-				}
-				f.roots = append(f.roots, r)
-				// Remove this variable from the set of unnamed pointers.
-				for a := r.Addr; a < r.Addr.Add(r.Type.Size); a = a.Add(p.proc.PtrSize()) {
-					delete(unnamed, a)
-				}
-			}
-			// Emit roots for unnamed pointer slots in the frame.
-			// Make deterministic by sorting first.
-			s := make([]core.Address, 0, len(unnamed))
-			for a := range unnamed {
-				s = append(s, a)
-			}
-			sort.Slice(s, func(i, j int) bool { return s[i] < s[j] })
-			for _, a := range s {
-				r := &Root{
-					Name:  "unk",
-					Addr:  a,
-					Type:  p.findType("unsafe.Pointer"),
-					Frame: f,
-				}
-				f.roots = append(f.roots, r)
-			}
+			vars[curfn] = append(vars[curfn], dwarfVar{
+				lowPC:  core.Address(e.LowPC + base),
+				highPC: core.Address(e.HighPC + base),
+				instr:  e.Instr,
+				name:   name,
+				typ:    dwarfTypeMap[dt],
+			})
 		}
 	}
+	return vars, nil
+}
+
+func hardwareRegs2DWARF(hregs []core.Register) []*op.DwarfRegister {
+	n := regnum.AMD64MaxRegNum()
+	dregs := make([]*op.DwarfRegister, n)
+	for _, hreg := range hregs {
+		dwn, ok := regnum.AMD64NameToDwarf[hreg.Name]
+		if !ok {
+			continue
+		}
+		dreg := op.DwarfRegisterFromUint64(hreg.Value)
+		dreg.FillBytes()
+		dregs[dwn] = dreg
+	}
+	return dregs
 }
 
 /* Dwarf encoding notes
diff --git a/internal/gocore/gocore_test.go b/internal/gocore/gocore_test.go
index ec60ad9..a8bd3e4 100644
--- a/internal/gocore/gocore_test.go
+++ b/internal/gocore/gocore_test.go
@@ -7,16 +7,12 @@
 package gocore
 
 import (
-	"archive/zip"
 	"bytes"
 	"errors"
 	"fmt"
-	"io"
 	"os"
 	"os/exec"
-	"path"
 	"path/filepath"
-	"reflect"
 	"runtime"
 	"strings"
 	"testing"
@@ -39,52 +35,6 @@
 	return p
 }
 
-// loadExample loads a simple core file which resulted from running the
-// following program on linux/amd64 with go 1.9.0 (the earliest supported runtime):
-//
-//	package main
-//
-//	func main() {
-//		_ = *(*int)(nil)
-//	}
-func loadExample(t *testing.T) *Process {
-	t.Helper()
-	if runtime.GOOS == "android" {
-		t.Skip("skipping test on android")
-	}
-	return loadCore(t, "testdata/core", "testdata", "")
-}
-
-func loadExampleVersion(t *testing.T, version string) *Process {
-	t.Helper()
-	if runtime.GOOS == "android" {
-		t.Skip("skipping test on android")
-	}
-	if version == "1.9" {
-		version = ""
-	}
-	var file string
-	var base string
-	if strings.HasSuffix(version, ".zip") {
-		// Make temporary directory.
-		dir, err := os.MkdirTemp("", strings.TrimSuffix(version, ".zip")+"_")
-		if err != nil {
-			t.Fatalf("can't make temp directory: %s", err)
-		}
-		defer os.RemoveAll(dir)
-
-		// Unpack test into directory.
-		unzip(t, filepath.Join("testdata", version), dir)
-
-		file = filepath.Join(dir, "tmp", "coretest", "core")
-		base = dir
-	} else {
-		file = fmt.Sprintf("testdata/core%s", version)
-		base = "testdata"
-	}
-	return loadCore(t, file, base, "")
-}
-
 // loadExampleGenerated generates a core from a binary built with
 // runtime.GOROOT().
 func loadExampleGenerated(t *testing.T, buildFlags ...string) *Process {
@@ -264,202 +214,6 @@
 	return "", b, fmt.Errorf("did not find core file in %+v", names)
 }
 
-// unzip unpacks the zip file name into the directory dir.
-func unzip(t *testing.T, name, dir string) {
-	t.Helper()
-	r, err := zip.OpenReader(name)
-	if err != nil {
-		t.Fatalf("can't read zip file %s: %s", name, err)
-	}
-	for _, f := range r.File {
-		rf, err := f.Open()
-		if err != nil {
-			t.Fatalf("can't read entry %s: %s", f.Name, err)
-		}
-		err = os.MkdirAll(path.Dir(filepath.Join(dir, f.Name)), 0777)
-		if err != nil {
-			t.Fatalf("can't make directory: %s", err)
-		}
-		wf, err := os.Create(filepath.Join(dir, f.Name))
-		if err != nil {
-			t.Fatalf("can't write entry %s: %s", f.Name, err)
-		}
-		_, err = io.Copy(wf, rf)
-		if err != nil {
-			t.Fatalf("can't copy %s: %s", f.Name, err)
-		}
-		err = rf.Close()
-		if err != nil {
-			t.Fatalf("can't close reader %s: %s", f.Name, err)
-		}
-		err = wf.Close()
-		if err != nil {
-			t.Fatalf("can't close writer %s: %s", f.Name, err)
-		}
-	}
-}
-
-func TestObjects(t *testing.T) {
-	p := loadExample(t)
-	n := 0
-	p.ForEachObject(func(x Object) bool {
-		n++
-		return true
-	})
-	if n != 104 {
-		t.Errorf("#objects = %d, want 104", n)
-	}
-}
-
-func TestRoots(t *testing.T) {
-	p := loadExample(t)
-	n := 0
-	p.ForEachRoot(func(r *Root) bool {
-		n++
-		return true
-	})
-	if n != 257 {
-		t.Errorf("#roots = %d, want 257", n)
-	}
-}
-
-// TestConfig checks the configuration accessors.
-func TestConfig(t *testing.T) {
-	p := loadExample(t)
-	if v := p.BuildVersion(); v != "go1.9" {
-		t.Errorf("version=%s, wanted go1.9", v)
-	}
-	if n := p.Stats().Size; n != 2732032 {
-		t.Errorf("all stats=%d, want 2732032", n)
-	}
-}
-
-func TestFindFunc(t *testing.T) {
-	p := loadExample(t)
-	a := core.Address(0x404000)
-	f := p.FindFunc(a)
-	if f == nil {
-		t.Errorf("can't find function at %x", a)
-		return
-	}
-	if n := f.Name(); n != "runtime.recvDirect" {
-		t.Errorf("funcname(%x)=%s, want runtime.recvDirect", a, n)
-	}
-}
-
-func TestTypes(t *testing.T) {
-	p := loadExample(t)
-	// Check the type of a few objects.
-	for _, s := range [...]struct {
-		addr   core.Address
-		size   int64
-		kind   Kind
-		name   string
-		repeat int64
-	}{
-		{0xc420000480, 384, KindStruct, "runtime.g", 1},
-		{0xc42000a020, 32, KindPtr, "*runtime.g", 4},
-		{0xc420082000, 96, KindStruct, "hchan<bool>", 1},
-		{0xc420062000, 64, KindStruct, "runtime._defer", 1},
-	} {
-		x, i := p.FindObject(s.addr)
-		if x == 0 {
-			t.Errorf("can't find object at %x", s.addr)
-			continue
-		}
-		if i != 0 {
-			t.Errorf("offset(%x)=%d, want 0", s.addr, i)
-		}
-		if p.Size(x) != s.size {
-			t.Errorf("size(%x)=%d, want %d", s.addr, p.Size(x), s.size)
-		}
-		typ, repeat := p.Type(x)
-		if typ.Kind != s.kind {
-			t.Errorf("kind(%x)=%s, want %s", s.addr, typ.Kind, s.kind)
-		}
-		if typ.Name != s.name {
-			t.Errorf("name(%x)=%s, want %s", s.addr, typ.Name, s.name)
-		}
-		if repeat != s.repeat {
-			t.Errorf("repeat(%x)=%d, want %d", s.addr, repeat, s.repeat)
-		}
-
-		y, i := p.FindObject(s.addr + 1)
-		if y != x {
-			t.Errorf("can't find object at %x", s.addr+1)
-		}
-		if i != 1 {
-			t.Errorf("offset(%x)=%d, want i", s.addr, i)
-		}
-	}
-}
-
-func TestReverse(t *testing.T) {
-	p := loadExample(t)
-
-	// Build the pointer map.
-	// m[x]=y means address x has a pointer to address y.
-	m1 := map[core.Address]core.Address{}
-	p.ForEachObject(func(x Object) bool {
-		p.ForEachPtr(x, func(i int64, y Object, j int64) bool {
-			m1[p.Addr(x).Add(i)] = p.Addr(y).Add(j)
-			return true
-		})
-		return true
-	})
-	p.ForEachRoot(func(r *Root) bool {
-		p.ForEachRootPtr(r, func(i int64, y Object, j int64) bool {
-			m1[r.Addr.Add(i)] = p.Addr(y).Add(j)
-			return true
-		})
-		return true
-	})
-
-	// Build the same, with reverse entries.
-	m2 := map[core.Address]core.Address{}
-	p.ForEachObject(func(y Object) bool {
-		p.ForEachReversePtr(y, func(x Object, r *Root, i, j int64) bool {
-			if r != nil {
-				m2[r.Addr.Add(i)] = p.Addr(y).Add(j)
-			} else {
-				m2[p.Addr(x).Add(i)] = p.Addr(y).Add(j)
-			}
-			return true
-		})
-		return true
-	})
-
-	if !reflect.DeepEqual(m1, m2) {
-		t.Errorf("forward and reverse edges don't match")
-	}
-}
-
-func TestDynamicType(t *testing.T) {
-	p := loadExample(t)
-	for _, g := range p.Globals() {
-		if g.Name == "runtime.indexError" {
-			d := p.DynamicType(g.Type, g.Addr)
-			if d.Name != "runtime.errorString" {
-				t.Errorf("dynamic type wrong: got %s want runtime.errorString", d.Name)
-			}
-		}
-	}
-}
-
-// getStat returns the first (depth first) stat in the hierarchy which matches
-// name, nil otherwise.
-func getStat(stat *Stats, name string) *Stats {
-	if stat.Name == name {
-		return stat
-	}
-	for _, child := range stat.Children {
-		if found := getStat(child, name); found != nil {
-			return found
-		}
-	}
-	return nil
-}
-
 func checkProcess(t *testing.T, p *Process) {
 	t.Helper()
 	if gs := p.Goroutines(); len(gs) == 0 {
@@ -467,8 +221,8 @@
 	}
 
 	const heapName = "heap"
-	heapStat := getStat(p.Stats(), heapName)
-	if heapStat == nil || heapStat.Size == 0 {
+	heapStat := p.Stats().Sub(heapName)
+	if heapStat == nil || heapStat.Value == 0 {
 		t.Errorf("stat[%q].Size == 0, want >0", heapName)
 	}
 
@@ -479,25 +233,6 @@
 }
 
 func TestVersions(t *testing.T) {
-	versions := []string{
-		"1.10",
-		"1.11",
-		"1.12.zip",
-		"1.13.zip",
-		"1.13.3.zip",
-		"1.14.zip",
-		"1.16.zip",
-		"1.17.zip",
-		"1.18.zip",
-		"1.19.zip",
-	}
-	for _, ver := range versions {
-		t.Run(ver, func(t *testing.T) {
-			p := loadExampleVersion(t, ver)
-			checkProcess(t, p)
-		})
-	}
-
 	t.Run("goroot", func(t *testing.T) {
 		for _, buildFlags := range [][]string{
 			nil,
@@ -511,69 +246,52 @@
 	})
 }
 
-func loadZipCore(t *testing.T, name string) *Process {
-	t.Helper()
-	if runtime.GOOS == "android" {
-		t.Skip("skipping test on android")
-	}
-	// Make temporary directory.
-	dir, err := os.MkdirTemp("", name+"_")
-	if err != nil {
-		t.Fatalf("can't make temp directory: %s", err)
-	}
-	defer os.RemoveAll(dir)
-
-	// Unpack bin file and core file into directory.
-	unzip(t, filepath.Join("testdata", name+".zip"), dir)
-
-	exe := filepath.Join(dir, name)
-	file := filepath.Join(dir, "core")
-	return loadCore(t, file, dir, exe)
+func TestObjects(t *testing.T) {
+	t.Run("goroot", func(t *testing.T) {
+		for _, buildFlags := range [][]string{
+			nil,
+			{"-buildmode=pie"},
+		} {
+			t.Run(strings.Join(buildFlags, ","), func(t *testing.T) {
+				p := loadExampleGenerated(t, buildFlags...)
+				n := 0
+				const largeObjectThreshold = 32768
+				large := 0 // Number of objects larger than (or equal to largeObjectThreshold)
+				p.ForEachObject(func(x Object) bool {
+					siz := p.Size(x)
+					t.Logf("%s size=%d", typeName(p, x), p.Size(x))
+					if siz >= largeObjectThreshold {
+						large++
+					}
+					n++
+					return true
+				})
+				if n < 10 {
+					t.Errorf("#objects = %d, want >10", n)
+				}
+				if large != 1 {
+					t.Errorf("expected exactly one object larger than %d, found %d", largeObjectThreshold, large)
+				}
+			})
+		}
+	})
 }
 
-func TestRuntimeTypes(t *testing.T) {
-	p := loadZipCore(t, "runtimetype")
-	// Check the type of a few objects.
-	for _, s := range [...]struct {
-		addr   core.Address
-		size   int64
-		kind   Kind
-		name   string
-		repeat int64
-	}{
-		{0xc00018e000, 16, KindStruct, "example.com/m/path-a/pkg.T1", 1},
-		{0xc00018e010, 16, KindStruct, "example.com/m/path-a/pkg.T2", 1},
-		{0xc000190000, 32, KindStruct, "example.com/m/path-b/pkg.T1", 1},
-		{0xc000190020, 32, KindStruct, "example.com/m/path-b/pkg.T2", 1},
-	} {
-		x, i := p.FindObject(s.addr)
-		if x == 0 {
-			t.Errorf("can't find object at %x", s.addr)
-			continue
-		}
-		if i != 0 {
-			t.Errorf("offset(%x)=%d, want 0", s.addr, i)
-		}
-		if p.Size(x) != s.size {
-			t.Errorf("size(%x)=%d, want %d", s.addr, p.Size(x), s.size)
-		}
-		typ, repeat := p.Type(x)
-		if typ.Kind != s.kind {
-			t.Errorf("kind(%x)=%s, want %s", s.addr, typ.Kind, s.kind)
-		}
-		if typ.Name != s.name {
-			t.Errorf("name(%x)=%s, want %s", s.addr, typ.Name, s.name)
-		}
-		if repeat != s.repeat {
-			t.Errorf("repeat(%x)=%d, want %d", s.addr, repeat, s.repeat)
-		}
-
-		y, i := p.FindObject(s.addr + 1)
-		if y != x {
-			t.Errorf("can't find object at %x", s.addr+1)
-		}
-		if i != 1 {
-			t.Errorf("offset(%x)=%d, want i", s.addr, i)
+// typeName returns a string representing the type of this object.
+func typeName(c *Process, x Object) string {
+	size := c.Size(x)
+	typ, repeat := c.Type(x)
+	if typ == nil {
+		return fmt.Sprintf("unk%d", size)
+	}
+	name := typ.String()
+	n := size / typ.Size
+	if n > 1 {
+		if repeat < n {
+			name = fmt.Sprintf("[%d+%d?]%s", repeat, n-repeat, name)
+		} else {
+			name = fmt.Sprintf("[%d]%s", repeat, name)
 		}
 	}
+	return name
 }
diff --git a/internal/gocore/goroutine.go b/internal/gocore/goroutine.go
index 2c6dbdd..053e274 100644
--- a/internal/gocore/goroutine.go
+++ b/internal/gocore/goroutine.go
@@ -13,6 +13,9 @@
 	stackSize int64  // current stack allocation
 	frames    []*Frame
 
+	// Registers containing live pointers.
+	regRoots []*Root
+
 	// TODO: defers, in-progress panics
 }
 
diff --git a/internal/gocore/module.go b/internal/gocore/module.go
index 9429a8d..3d09d29 100644
--- a/internal/gocore/module.go
+++ b/internal/gocore/module.go
@@ -15,23 +15,23 @@
 type module struct {
 	r             region       // inferior region holding a runtime.moduledata
 	types, etypes core.Address // range that holds all the runtime._type data in this module
-	p             *Process     // The parent process of this module.
 }
 
-func (p *Process) readModules() {
-	// Note: the cast is necessary for cores generated by Go 1.9, where
-	// runtime.moduledata is just an unsafe.Pointer.
-	ms := p.rtGlobals["modulesSlice"].Cast("*[]*runtime.moduledata").Deref()
+func readModules(rtTypeByName map[string]*Type, rtConsts map[string]int64, rtGlobals map[string]region) ([]*module, *funcTab, error) {
+	ms := rtGlobals["modulesSlice"].Deref()
 	n := ms.SliceLen()
+	var modules []*module
+	var fnTab funcTab
 	for i := int64(0); i < n; i++ {
 		md := ms.SliceIndex(i).Deref()
-		p.modules = append(p.modules, p.readModule(md))
+		modules = append(modules, readModule(md, &fnTab, rtTypeByName, rtConsts))
 	}
-	p.funcTab.sort()
+	fnTab.sort()
+	return modules, &fnTab, nil
 }
 
-func (p *Process) readModule(r region) *module {
-	m := &module{p: p, r: r}
+func readModule(r region, fns *funcTab, rtTypeByName map[string]*Type, rtConsts map[string]int64) *module {
+	m := &module{r: r}
 	m.types = core.Address(r.Field("types").Uintptr())
 	m.etypes = core.Address(r.Field("etypes").Uintptr())
 
@@ -62,17 +62,17 @@
 			// funcoff changed type, but had the same meaning.
 			funcoff = int64(ft.Field("funcoff").Uintptr())
 		}
-		fr := pcln.SliceIndex(funcoff).Cast("runtime._func")
+		fr := pcln.SliceIndex(funcoff).Cast(rtTypeByName["runtime._func"])
 		var f *Func
 		if havePCtab {
-			f = m.readFunc(fr, pctab, funcnametab)
+			f = m.readFunc(fr, pctab, funcnametab, rtConsts)
 		} else {
-			f = m.readFunc(fr, pcln, pcln)
+			f = m.readFunc(fr, pcln, pcln, rtConsts)
 		}
 		if f.entry != min {
 			panic(fmt.Errorf("entry %x and min %x don't match for %s", f.entry, min, f.name))
 		}
-		p.funcTab.add(min, max, f)
+		fns.add(min, max, f)
 	}
 
 	return m
@@ -81,7 +81,7 @@
 // readFunc parses a runtime._func and returns a *Func.
 // r must have type runtime._func.
 // pcln must have type []byte and represent the module's pcln table region.
-func (m *module) readFunc(r region, pctab region, funcnametab region) *Func {
+func (m *module) readFunc(r region, pctab region, funcnametab region, rtConsts map[string]int64) *Func {
 	f := &Func{module: m, r: r}
 	var nameOff int32
 	switch {
@@ -98,7 +98,7 @@
 		f.entry = core.Address(r.Field("entry").Uintptr())
 		nameOff = r.Field("nameoff").Int32()
 	}
-	f.name = r.p.proc.ReadCString(funcnametab.SliceIndex(int64(nameOff)).a)
+	f.name = r.p.ReadCString(funcnametab.SliceIndex(int64(nameOff)).a)
 	pcsp := r.Field("pcsp")
 	var pcspIdx int64
 	if pcsp.typ.Kind == KindUint {
@@ -107,7 +107,7 @@
 	} else {
 		pcspIdx = int64(pcsp.Int32())
 	}
-	f.frameSize.read(r.p.proc, pctab.SliceIndex(pcspIdx).a)
+	f.frameSize.read(r.p, pctab.SliceIndex(pcspIdx).a)
 
 	// Parse pcdata and funcdata, which are laid out beyond the end of the _func.
 	// In 1.16, npcdata changed to be a uint32 from an int32.
@@ -123,43 +123,26 @@
 	a := nfd.a.Add(nfd.typ.Size)
 
 	for i := uint32(0); i < n; i++ {
-		f.pcdata = append(f.pcdata, r.p.proc.ReadInt32(a))
+		f.pcdata = append(f.pcdata, r.p.ReadInt32(a))
 		a = a.Add(4)
 	}
 
-	is118OrGreater := m.r.HasField("gofunc")
-	if !is118OrGreater {
-		// Since 1.18, funcdata no longer needs to be aligned.
-		a = a.Align(r.p.proc.PtrSize())
-	}
-
-	if nfd.typ.Size == 1 { // go 1.12 and beyond, this is a uint8
-		n = uint32(nfd.Uint8())
-	} else { // go 1.11 and earlier, this is an int32
-		n = uint32(nfd.Int32())
-	}
+	n = uint32(nfd.Uint8())
 	for i := uint32(0); i < n; i++ {
-		if is118OrGreater {
-			// Since 1.18, funcdata contains offsets from go.func.*.
-			off := r.p.proc.ReadUint32(a)
-			if off == ^uint32(0) {
-				// No entry.
-				f.funcdata = append(f.funcdata, 0)
-			} else {
-				f.funcdata = append(f.funcdata, core.Address(m.r.Field("gofunc").Uintptr()+uint64(off)))
-			}
-			a = a.Add(4)
+		// Since 1.18, funcdata contains offsets from go.func.*.
+		off := r.p.ReadUint32(a)
+		if off == ^uint32(0) {
+			// No entry.
+			f.funcdata = append(f.funcdata, 0)
 		} else {
-			// Prior to 1.18, funcdata contains pointers directly
-			// to the data.
-			f.funcdata = append(f.funcdata, r.p.proc.ReadPtr(a))
-			a = a.Add(r.p.proc.PtrSize())
+			f.funcdata = append(f.funcdata, core.Address(m.r.Field("gofunc").Uintptr()+uint64(off)))
 		}
+		a = a.Add(4)
 	}
 
 	// Read pcln tables we need.
-	if stackmap := int(r.p.rtConstants["_PCDATA_StackMapIndex"]); stackmap < len(f.pcdata) {
-		f.stackMap.read(r.p.proc, pctab.SliceIndex(int64(f.pcdata[stackmap])).a)
+	if stackmap := int(rtConsts["_PCDATA_StackMapIndex"]); stackmap < len(f.pcdata) {
+		f.stackMap.read(r.p, pctab.SliceIndex(int64(f.pcdata[stackmap])).a)
 	} else {
 		f.stackMap.setEmpty()
 	}
diff --git a/internal/gocore/object.go b/internal/gocore/object.go
index 481a04d..b0e2704 100644
--- a/internal/gocore/object.go
+++ b/internal/gocore/object.go
@@ -5,6 +5,7 @@
 package gocore
 
 import (
+	"iter"
 	"math/bits"
 	"strings"
 
@@ -29,7 +30,7 @@
 
 	// Function to call when we find a new pointer.
 	add := func(x core.Address) {
-		h := p.findHeapInfo(x)
+		h := p.heap.get(x)
 		if h == nil { // not in heap or not in a valid span
 			// Invalid spans can happen with intra-stack pointers.
 			return
@@ -37,7 +38,7 @@
 		// Round down to object start.
 		x = h.base.Add(x.Sub(h.base) / h.size * h.size)
 		// Object start may map to a different info. Reload heap info.
-		h = p.findHeapInfo(x)
+		h = p.heap.get(x)
 		// Find mark bit
 		b := uint64(x) % heapInfoSize / 8
 		if h.mark&(uint64(1)<<b) != 0 { // already found
@@ -61,6 +62,9 @@
 	// Not a huge deal given that we'll just ignore outright bad pointers, but
 	// we may accidentally mark some objects as live erroneously.
 	for _, g := range p.goroutines {
+		for _, r := range g.regRoots {
+			add(core.Address(r.RegValue))
+		}
 		for _, f := range g.frames {
 			for a := range f.Live {
 				add(p.proc.ReadPtr(a))
@@ -115,7 +119,7 @@
 	// Initialize firstIdx fields in the heapInfo, for fast object index lookups.
 	n = 0
 	p.ForEachObject(func(x Object) bool {
-		h := p.findHeapInfo(p.Addr(x))
+		h := p.heap.get(p.Addr(x))
 		if h.firstIdx == -1 {
 			h.firstIdx = n
 		}
@@ -127,24 +131,26 @@
 	}
 
 	// Update stats to include the live/garbage distinction.
-	alloc := p.Stats().Child("heap").Child("in use spans").Child("alloc")
-	alloc.Children = []*Stats{
-		&Stats{"live", live, nil},
-		&Stats{"garbage", alloc.Size - live, nil},
-	}
+	allocSize := p.Stats().Sub("heap", "in use spans", "alloc").Value
+	p.Stats().Sub("heap", "in use spans").setChild(
+		groupStat("alloc",
+			leafStat("live", live),
+			leafStat("garbage", allocSize-live),
+		),
+	)
 }
 
 // isPtrFromHeap reports whether the inferior at address a contains a pointer.
 // a must be somewhere in the heap.
 func (p *Process) isPtrFromHeap(a core.Address) bool {
-	return p.findHeapInfo(a).IsPtr(a, p.proc.PtrSize())
+	return p.heap.get(a).isPtr(a, p.proc.PtrSize())
 }
 
 // IsPtr reports whether the inferior at address a contains a pointer.
 func (p *Process) IsPtr(a core.Address) bool {
-	h := p.findHeapInfo(a)
+	h := p.heap.get(a)
 	if h != nil {
-		return h.IsPtr(a, p.proc.PtrSize())
+		return h.isPtr(a, p.proc.PtrSize())
 	}
 	for _, m := range p.modules {
 		for _, s := range [2]string{"data", "bss"} {
@@ -169,7 +175,7 @@
 // Returns 0,0 if a doesn't point to a live heap object.
 func (p *Process) FindObject(a core.Address) (Object, int64) {
 	// Round down to the start of an object.
-	h := p.findHeapInfo(a)
+	h := p.heap.get(a)
 	if h == nil {
 		// Not in Go heap, or in a span
 		// that doesn't hold Go objects (freed, stacks, ...)
@@ -177,7 +183,7 @@
 	}
 	x := h.base.Add(a.Sub(h.base) / h.size * h.size)
 	// Check if object is marked.
-	h = p.findHeapInfo(x)
+	h = p.heap.get(x)
 	if h.mark>>(uint64(x)%heapInfoSize/8)&1 == 0 { // free or garbage
 		return 0, 0
 	}
@@ -189,25 +195,21 @@
 	if x == 0 {
 		return -1, 0
 	}
-	h := p.findHeapInfo(core.Address(x))
+	h := p.heap.get(core.Address(x))
 	return h.firstIdx + bits.OnesCount64(h.mark&(uint64(1)<<(uint64(x)%heapInfoSize/8)-1)), off
 }
 
 // ForEachObject calls fn with each object in the Go heap.
 // If fn returns false, ForEachObject returns immediately.
 func (p *Process) ForEachObject(fn func(x Object) bool) {
-	for _, k := range p.pages {
-		pt := p.pageTable[k]
-		for i := range pt {
-			h := &pt[i]
-			m := h.mark
-			for m != 0 {
-				j := bits.TrailingZeros64(m)
-				m &= m - 1
-				x := Object(k)*pageTableSize*heapInfoSize + Object(i)*heapInfoSize + Object(j)*8
-				if !fn(x) {
-					return
-				}
+	for a, h := range p.heap.all() {
+		m := h.mark
+		for m != 0 {
+			j := bits.TrailingZeros64(m)
+			m &= m - 1
+			x := Object(a + core.Address(j*8))
+			if !fn(x) {
+				return
 			}
 		}
 	}
@@ -222,6 +224,11 @@
 		}
 	}
 	for _, g := range p.goroutines {
+		for _, r := range g.regRoots {
+			if !fn(r) {
+				return
+			}
+		}
 		for _, f := range g.frames {
 			for _, r := range f.roots {
 				if !fn(r) {
@@ -239,7 +246,7 @@
 
 // Size returns the size of x in bytes.
 func (p *Process) Size(x Object) int64 {
-	return p.findHeapInfo(core.Address(x)).size
+	return p.heap.get(core.Address(x)).size
 }
 
 // Type returns the type and repeat count for the object x.
@@ -306,6 +313,15 @@
 		off += p.proc.PtrSize()
 		fallthrough
 	case KindPtr, KindString, KindSlice, KindFunc:
+		if t.Kind == KindPtr && r.Addr == 0 {
+			dst, off2 := p.FindObject(core.Address(r.RegValue))
+			if dst != 0 {
+				if !fn(off, dst, off2) {
+					return false
+				}
+			}
+			break
+		}
 		a := r.Addr.Add(off)
 		if r.Frame == nil || r.Frame.Live[a] {
 			dst, off2 := p.FindObject(p.proc.ReadPtr(a))
@@ -346,7 +362,7 @@
 	ptr [2]uint64
 }
 
-func (h *heapInfo) IsPtr(a core.Address, ptrSize int64) bool {
+func (h *heapInfo) isPtr(a core.Address, ptrSize int64) bool {
 	if ptrSize == 8 {
 		i := uint(a%heapInfoSize) / 8
 		return h.ptr[0]>>i&1 != 0
@@ -355,30 +371,45 @@
 	return h.ptr[i/64]>>(i%64)&1 != 0
 }
 
-// setHeapPtr records that the memory at heap address a contains a pointer.
-func (p *Process) setHeapPtr(a core.Address) {
-	h := p.allocHeapInfo(a)
-	if p.proc.PtrSize() == 8 {
-		i := uint(a%heapInfoSize) / 8
-		h.ptr[0] |= uint64(1) << i
-		return
-	}
-	i := a % heapInfoSize / 4
-	h.ptr[i/64] |= uint64(1) << (i % 64)
+type heapTableID uint64
+
+func (id heapTableID) addr() core.Address {
+	return core.Address(id * heapInfoSize * heapTableSize)
+}
+
+type heapTable struct {
+	table   map[heapTableID]*heapTableEntry
+	entries []heapTableID
+	ptrSize uint64
+}
+
+func heapTableIndex(a core.Address) (heapTableID, uint64) {
+	return heapTableID(a / heapInfoSize / heapTableSize), uint64(a / heapInfoSize % heapTableSize)
 }
 
 // Heap info structures cover 9 bits of address.
 // A page table entry covers 20 bits of address (1MB).
-const pageTableSize = 1 << 11
+const heapTableSize = 1 << 11
 
-type pageTableEntry [pageTableSize]heapInfo
+type heapTableEntry [heapTableSize]heapInfo
 
-// findHeapInfo finds the heapInfo structure for a.
-// Returns nil if a is not a heap address.
-func (p *Process) findHeapInfo(a core.Address) *heapInfo {
-	k := a / heapInfoSize / pageTableSize
-	i := a / heapInfoSize % pageTableSize
-	t := p.pageTable[k]
+func (ht *heapTable) setIsPointer(a core.Address) {
+	h := ht.getOrCreate(a)
+	ptrSize := ht.ptrSize
+	if ptrSize == 8 {
+		i := uint64(a%heapInfoSize) / 8
+		h.ptr[0] |= uint64(1) << i
+		return
+	}
+	i := uint64(a%heapInfoSize) / 4
+	h.ptr[i/64] |= uint64(1) << (i % 64)
+}
+
+// get returns the heap info for an address. Returns nil
+// if a is not a heap address.
+func (ht *heapTable) get(a core.Address) *heapInfo {
+	k, i := heapTableIndex(a)
+	t := ht.table[k]
 	if t == nil {
 		return nil
 	}
@@ -389,19 +420,33 @@
 	return h
 }
 
-// Same as findHeapInfo, but allocates the heapInfo if it
-// hasn't been allocated yet.
-func (p *Process) allocHeapInfo(a core.Address) *heapInfo {
-	k := a / heapInfoSize / pageTableSize
-	i := a / heapInfoSize % pageTableSize
-	t := p.pageTable[k]
+// getOrCreate returns the heap info for an address, or if nil,
+// creates it, marking it as a heap address.
+func (ht *heapTable) getOrCreate(a core.Address) *heapInfo {
+	k, i := heapTableIndex(a)
+	t := ht.table[k]
 	if t == nil {
-		t = new(pageTableEntry)
-		for j := 0; j < pageTableSize; j++ {
+		t = new(heapTableEntry)
+		for j := 0; j < heapTableSize; j++ {
 			t[j].firstIdx = -1
 		}
-		p.pageTable[k] = t
-		p.pages = append(p.pages, k)
+		ht.table[k] = t
+		ht.entries = append(ht.entries, k)
 	}
 	return &t[i]
 }
+
+func (ht *heapTable) all() iter.Seq2[core.Address, *heapInfo] {
+	return func(yield func(core.Address, *heapInfo) bool) {
+		for _, k := range ht.entries {
+			e := ht.table[k]
+			for i := range e {
+				h := &e[i]
+				a := k.addr() + core.Address(uint64(i)*heapInfoSize)
+				if !yield(a, h) {
+					return
+				}
+			}
+		}
+	}
+}
diff --git a/internal/gocore/process.go b/internal/gocore/process.go
index e934358..7b2777c 100644
--- a/internal/gocore/process.go
+++ b/internal/gocore/process.go
@@ -6,60 +6,56 @@
 
 import (
 	"debug/dwarf"
+	"encoding/binary"
+	"errors"
 	"fmt"
+	"iter"
 	"math/bits"
+	"sort"
 	"strings"
 	"sync"
 
 	"golang.org/x/debug/internal/core"
+
+	"github.com/go-delve/delve/pkg/dwarf/op"
+	"github.com/go-delve/delve/pkg/dwarf/regnum"
 )
 
 // A Process represents the state of a Go process that core dumped.
 type Process struct {
-	proc *core.Process
+	proc         *core.Process
+	buildVersion string
 
-	// data structure for fast object finding
-	// The key to these maps is the object address divided by
-	// pageTableSize * heapInfoSize.
-	pageTable map[core.Address]*pageTableEntry
-	pages     []core.Address // deterministic ordering of keys of pageTable
+	// Index of heap objects and pointers.
+	heap *heapTable
 
 	// number of live objects
 	nObj int
 
 	goroutines []*Goroutine
 
-	// runtime info
-	rtGlobals   map[string]region
-	rtConstants map[string]int64
+	// Runtime info for easier lookup.
+	rtGlobals map[string]region
+	rtConsts  map[string]int64
 
 	// A module is a loadable unit. Most Go programs have 1, programs
 	// which load plugins will have more.
 	modules []*module
 
-	// address -> function mapping
-	funcTab funcTab
+	// Maps core.Address to functions.
+	funcTab *funcTab
 
-	// map from dwarf type to *Type
-	dwarfMap map[dwarf.Type]*Type
+	// Fundamental type mappings extracted from the core.
+	dwarfTypeMap map[dwarf.Type]*Type
+	rtTypeByName map[string]*Type
 
-	// map from address of runtime._type to *Type
-	runtimeMap map[core.Address]*Type
+	// rtTypeMap maps a core.Address to a *Type. Constructed incrementally, on-demand.
+	rtTypeMap map[core.Address]*Type
 
-	// map from runtime type name to the set of *Type with that name
-	// Used to find candidates to put in the runtimeMap map.
-	runtimeNameMap map[string][]*Type
+	// Memory usage breakdown.
+	stats *Statistic
 
-	// memory usage by category
-	stats *Stats
-
-	buildVersion string
-
-	// This is a Go 1.17 process, or higher. This field is used for
-	// differences in behavior that otherwise can't be detected via the
-	// type system.
-	is117OrGreater bool
-
+	// Global roots.
 	globals []*Root
 
 	// Types of each object, indexed by object index.
@@ -78,6 +74,78 @@
 	rootIdx []*Root
 }
 
+// Core takes a loaded core file and extracts Go information from it.
+func Core(proc *core.Process) (p *Process, err error) {
+	p = &Process{proc: proc}
+
+	// Initialize everything that just depends on DWARF.
+	p.dwarfTypeMap, err = readDWARFTypes(proc)
+	if err != nil {
+		return nil, err
+	}
+	p.rtTypeByName = make(map[string]*Type)
+	for dt, t := range p.dwarfTypeMap {
+		name := gocoreName(dt)
+		if _, ok := p.rtTypeByName[name]; ok {
+			// If a runtime type matches more than one DWARF type,
+			// pick one arbitrarily.
+			//
+			// This looks mostly harmless. DWARF has some redundant entries.
+			// For example, [32]uint8 appears twice.
+			//
+			// TODO(mknyszek): Investigate the reason for this duplication.
+			continue
+		}
+		p.rtTypeByName[name] = t
+	}
+	p.rtConsts, err = readRuntimeConstants(proc)
+	if err != nil {
+		return nil, err
+	}
+	p.globals, err = readGlobals(proc, p.dwarfTypeMap)
+	if err != nil {
+		return nil, err
+	}
+
+	// Find runtime globals we care about. Initialize regions for them.
+	p.rtGlobals = make(map[string]region)
+	for _, g := range p.globals {
+		if strings.HasPrefix(g.Name, "runtime.") {
+			p.rtGlobals[g.Name[8:]] = region{p: proc, a: g.Addr, typ: g.Type}
+		}
+	}
+
+	// Read all the data that depend on runtime globals.
+	p.buildVersion = p.rtGlobals["buildVersion"].String()
+
+	// Read modules and function data.
+	p.modules, p.funcTab, err = readModules(p.rtTypeByName, p.rtConsts, p.rtGlobals)
+	if err != nil {
+		return nil, err
+	}
+
+	// Initialize the heap data structures.
+	p.heap, p.stats, err = readHeap(p)
+	if err != nil {
+		return nil, err
+	}
+
+	// Read stack and register variables from DWARF.
+	dwarfVars, err := readDWARFVars(proc, p.funcTab, p.dwarfTypeMap)
+	if err != nil {
+		return nil, err
+	}
+
+	// Read goroutines.
+	p.goroutines, err = readGoroutines(p, dwarfVars)
+	if err != nil {
+		return nil, err
+	}
+
+	p.markObjects() // needs to be after readGlobals, readGs.
+	return p, nil
+}
+
 // Process returns the core.Process used to construct this Process.
 func (p *Process) Process() *core.Process {
 	return p.proc
@@ -88,7 +156,7 @@
 }
 
 // Stats returns a breakdown of the program's memory use by category.
-func (p *Process) Stats() *Stats {
+func (p *Process) Stats() *Statistic {
 	return p.stats
 }
 
@@ -115,197 +183,59 @@
 }
 
 func (p *Process) tryFindType(name string) *Type {
-	s := p.runtimeNameMap[name]
-	if len(s) == 0 {
-		return nil
-	}
-	return s[0]
-}
-
-// Core takes a loaded core file and extracts Go information from it.
-func Core(proc *core.Process) (p *Process, err error) {
-	// Make sure we have DWARF info.
-	if _, err := proc.DWARF(); err != nil {
-		return nil, fmt.Errorf("error reading dwarf: %w", err)
-	}
-
-	// Guard against failures of proc.Read* routines.
-	/*
-		defer func() {
-			e := recover()
-			if e == nil {
-				return
-			}
-			p = nil
-			if x, ok := e.(error); ok {
-				err = x
-				return
-			}
-			panic(e) // Not an error, re-panic it.
-		}()
-	*/
-
-	p = &Process{
-		proc:       proc,
-		runtimeMap: map[core.Address]*Type{},
-		dwarfMap:   map[dwarf.Type]*Type{},
-	}
-
-	// Initialize everything that just depends on DWARF.
-	p.readDWARFTypes()
-	p.readRuntimeConstants()
-	p.readGlobals()
-
-	// Find runtime globals we care about. Initialize regions for them.
-	p.rtGlobals = map[string]region{}
-	for _, g := range p.globals {
-		if strings.HasPrefix(g.Name, "runtime.") {
-			p.rtGlobals[g.Name[8:]] = region{p: p, a: g.Addr, typ: g.Type}
-		}
-	}
-
-	// Read all the data that depend on runtime globals.
-	p.buildVersion = p.rtGlobals["buildVersion"].String()
-
-	// runtime._type varint name length encoding, and mheap curArena
-	// counting changed behavior in 1.17 without explicitly related type
-	// changes, making the difference difficult to detect. As a workaround,
-	// we check on the version explicitly.
-	//
-	// Go 1.17 added runtime._func.flag, so use that as a sentinel for this
-	// version.
-	p.is117OrGreater = p.findType("runtime._func").HasField("flag")
-
-	p.readModules()
-	p.readHeap()
-	p.readGs()
-	p.readStackVars() // needs to be after readGs.
-	p.markObjects()   // needs to be after readGlobals, readStackVars.
-
-	return p, nil
+	return p.rtTypeByName[name]
 }
 
 // arena is a summary of the size of components of a heapArena.
 type arena struct {
-	heapMin core.Address
-	heapMax core.Address
-
-	// Optional.
-	bitmapMin core.Address
-	bitmapMax core.Address
-
+	heapMin      core.Address
+	heapMax      core.Address
 	spanTableMin core.Address
 	spanTableMax core.Address
 }
 
-func (p *Process) getArenaBaseOffset() int64 {
-	if x, ok := p.rtConstants["arenaBaseOffsetUintptr"]; ok { // go1.15+
-		// arenaBaseOffset changed sign in 1.15. Callers treat this
-		// value as it was specified in 1.14, so we negate it here.
-		return -x
-	}
-	return p.rtConstants["arenaBaseOffset"]
-}
-
-func (p *Process) readHeap() {
-	ptrSize := p.proc.PtrSize()
-	p.pageTable = map[core.Address]*pageTableEntry{}
+func readHeap(p *Process) (*heapTable, *Statistic, error) {
 	mheap := p.rtGlobals["mheap_"]
+
 	var arenas []arena
+	arenaSize := p.rtConsts["heapArenaBytes"]
+	if arenaSize%heapInfoSize != 0 {
+		panic("arenaSize not a multiple of heapInfoSize")
+	}
+	arenaBaseOffset := -p.rtConsts["arenaBaseOffsetUintptr"]
+	if p.proc.PtrSize() == 4 && arenaBaseOffset != 0 {
+		panic("arenaBaseOffset must be 0 for 32-bit inferior")
+	}
 
-	if mheap.HasField("spans") {
-		// go 1.9 or 1.10. There is a single arena.
-		arenas = append(arenas, p.readArena19(mheap))
-	} else {
-		// go 1.11+. Has multiple arenas.
-		arenaSize := p.rtConstants["heapArenaBytes"]
-		if arenaSize%heapInfoSize != 0 {
-			panic("arenaSize not a multiple of heapInfoSize")
+	level1Table := mheap.Field("arenas")
+	level1size := level1Table.ArrayLen()
+	for level1 := int64(0); level1 < level1size; level1++ {
+		ptr := level1Table.ArrayIndex(level1)
+		if ptr.Address() == 0 {
+			continue
 		}
-		arenaBaseOffset := p.getArenaBaseOffset()
-		if ptrSize == 4 && arenaBaseOffset != 0 {
-			panic("arenaBaseOffset must be 0 for 32-bit inferior")
-		}
-
-		level1Table := mheap.Field("arenas")
-		level1size := level1Table.ArrayLen()
-		for level1 := int64(0); level1 < level1size; level1++ {
-			ptr := level1Table.ArrayIndex(level1)
+		level2table := ptr.Deref()
+		level2size := level2table.ArrayLen()
+		for level2 := int64(0); level2 < level2size; level2++ {
+			ptr = level2table.ArrayIndex(level2)
 			if ptr.Address() == 0 {
 				continue
 			}
-			level2table := ptr.Deref()
-			level2size := level2table.ArrayLen()
-			for level2 := int64(0); level2 < level2size; level2++ {
-				ptr = level2table.ArrayIndex(level2)
-				if ptr.Address() == 0 {
-					continue
-				}
-				a := ptr.Deref()
+			a := ptr.Deref()
 
-				min := core.Address(arenaSize*(level2+level1*level2size) - arenaBaseOffset)
-				max := min.Add(arenaSize)
+			min := core.Address(arenaSize*(level2+level1*level2size) - arenaBaseOffset)
+			max := min.Add(arenaSize)
 
-				arenas = append(arenas, p.readArena(a, min, max))
-			}
+			arenas = append(arenas, readArena(a, min, max))
 		}
 	}
-
-	p.readSpans(mheap, arenas)
-}
-
-// Read the global arena. Go 1.9 or 1.10 only, which has a single arena. Record
-// heap pointers and return the arena size summary.
-func (p *Process) readArena19(mheap region) arena {
-	ptrSize := p.proc.PtrSize()
-	logPtrSize := p.proc.LogPtrSize()
-
-	arenaStart := core.Address(mheap.Field("arena_start").Uintptr())
-	arenaUsed := core.Address(mheap.Field("arena_used").Uintptr())
-	arenaEnd := core.Address(mheap.Field("arena_end").Uintptr())
-	bitmapEnd := core.Address(mheap.Field("bitmap").Uintptr())
-	bitmapStart := bitmapEnd.Add(-int64(mheap.Field("bitmap_mapped").Uintptr()))
-	spanTableStart := mheap.Field("spans").SlicePtr().Address()
-	spanTableEnd := spanTableStart.Add(mheap.Field("spans").SliceCap() * ptrSize)
-
-	// Copy pointer bits to heap info.
-	// Note that the pointer bits are stored backwards.
-	for a := arenaStart; a < arenaUsed; a = a.Add(ptrSize) {
-		off := a.Sub(arenaStart) >> logPtrSize
-		if p.proc.ReadUint8(bitmapEnd.Add(-(off>>2)-1))>>uint(off&3)&1 != 0 {
-			p.setHeapPtr(a)
-		}
-	}
-
-	return arena{
-		heapMin:      arenaStart,
-		heapMax:      arenaEnd,
-		bitmapMin:    bitmapStart,
-		bitmapMax:    bitmapEnd,
-		spanTableMin: spanTableStart,
-		spanTableMax: spanTableEnd,
-	}
+	return readHeap0(p, mheap, arenas, arenaBaseOffset)
 }
 
 // Read a single heapArena. Go 1.11+, which has multiple areans. Record heap
 // pointers and return the arena size summary.
-func (p *Process) readArena(a region, min, max core.Address) arena {
-	ptrSize := p.proc.PtrSize()
-
-	// Bitmap reading handled in readSpans.
-	// Read in all the pointer locations from the arenas, where they lived before Go 1.22.
-	// After Go 1.22, the pointer locations are scattered among more locations; they're
-	// processed during readSpans instead.
-	var bitmap region
-	if a.HasField("bitmap") {
-		bitmap = a.Field("bitmap")
-		if oneBitBitmap := a.HasField("noMorePtrs"); oneBitBitmap { // Starting in go 1.20
-			p.readOneBitBitmap(bitmap, min)
-		} else {
-			p.readMultiBitBitmap(bitmap, min)
-		}
-	}
-
+func readArena(a region, min, max core.Address) arena {
+	ptrSize := a.p.PtrSize()
 	spans := a.Field("spans")
 	arena := arena{
 		heapMin:      min,
@@ -313,73 +243,50 @@
 		spanTableMin: spans.a,
 		spanTableMax: spans.a.Add(spans.ArrayLen() * ptrSize),
 	}
-	if bitmap.a != 0 {
-		arena.bitmapMin = bitmap.a
-		arena.bitmapMax = bitmap.a.Add(bitmap.ArrayLen())
-	}
 	return arena
 }
 
-// Read a one-bit bitmap (Go 1.20+), recording the heap pointers.
-func (p *Process) readOneBitBitmap(bitmap region, min core.Address) {
-	ptrSize := p.proc.PtrSize()
-	n := bitmap.ArrayLen()
-	for i := int64(0); i < n; i++ {
-		// The array uses 1 bit per word of heap. See mbitmap.go for
-		// more information.
-		m := bitmap.ArrayIndex(i).Uintptr()
-		bits := 8 * ptrSize
-		for j := int64(0); j < bits; j++ {
-			if m>>uint(j)&1 != 0 {
-				p.setHeapPtr(min.Add((i*bits + j) * ptrSize))
-			}
-		}
-	}
-}
+func readHeap0(p *Process, mheap region, arenas []arena, arenaBaseOffset int64) (*heapTable, *Statistic, error) {
+	// TODO(mknyszek): Break up this function into heapTable setup and statistics collection,
+	// at the very least...
 
-// Read a multi-bit bitmap (Go 1.11-1.20), recording the heap pointers.
-func (p *Process) readMultiBitBitmap(bitmap region, min core.Address) {
-	ptrSize := p.proc.PtrSize()
-	n := bitmap.ArrayLen()
-	for i := int64(0); i < n; i++ {
-		// The nth byte is composed of 4 object bits and 4 live/dead
-		// bits. We ignore the 4 live/dead bits, which are on the
-		// high order side of the byte.
-		//
-		// See mbitmap.go for more information on the format of
-		// the bitmap field of heapArena.
-		m := bitmap.ArrayIndex(i).Uint8()
-		for j := int64(0); j < 4; j++ {
-			if m>>uint(j)&1 != 0 {
-				p.setHeapPtr(min.Add((i*4 + j) * ptrSize))
-			}
-		}
+	// The main goal of this function is to initialize this data structure.
+	heap := &heapTable{
+		table:   make(map[heapTableID]*heapTableEntry),
+		ptrSize: uint64(p.proc.PtrSize()),
 	}
-}
 
-func (p *Process) readSpans(mheap region, arenas []arena) {
-	ptrSize := p.proc.PtrSize()
-	var all int64
-	var text int64
-	var readOnly int64
-	var heap int64
-	var spanTable int64
-	var bitmap int64
-	var data int64
-	var bss int64 // also includes mmap'd regions
+	// ... But while we're here, we'll be collecting stats.
+	var stats struct {
+		all              int64
+		text             int64
+		readOnly         int64
+		spanTable        int64
+		data             int64
+		bss              int64
+		freeSpanSize     int64
+		releasedSpanSize int64
+		manualSpanSize   int64
+		inUseSpanSize    int64
+		allocSize        int64
+		freeSize         int64
+		spanRoundSize    int64
+		manualAllocSize  int64
+		manualFreeSize   int64
+	}
 	for _, m := range p.proc.Mappings() {
 		size := m.Size()
-		all += size
+		stats.all += size
 		switch m.Perm() {
 		case core.Read:
-			readOnly += size
+			stats.readOnly += size
 		case core.Read | core.Exec:
-			text += size
+			stats.text += size
 		case core.Read | core.Write:
 			if m.CopyOnWrite() {
 				// Check if m.file == text's file? That could distinguish
 				// data segment from mmapped file.
-				data += size
+				stats.data += size
 				break
 			}
 			attribute := func(x, y core.Address, p *int64) {
@@ -391,59 +298,36 @@
 				}
 			}
 			for _, a := range arenas {
-				attribute(a.heapMin, a.heapMax, &heap)
-				attribute(a.bitmapMin, a.bitmapMax, &bitmap)
-				attribute(a.spanTableMin, a.spanTableMax, &spanTable)
+				attribute(a.spanTableMin, a.spanTableMax, &stats.spanTable)
 			}
 			// Any other anonymous mapping is bss.
 			// TODO: how to distinguish original bss from anonymous mmap?
-			bss += size
+			stats.bss += size
 		case core.Exec: // Ignore --xp mappings, like Linux's vsyscall=xonly.
-			all -= size // Make the total match again.
+			stats.all -= size // Make the total match again.
 		default:
-			panic("weird mapping " + m.Perm().String())
+			return nil, nil, errors.New("weird mapping " + m.Perm().String())
 		}
 	}
-	if !p.is117OrGreater && mheap.HasField("curArena") {
-		// 1.13.3 and up have curArena. Subtract unallocated space in
-		// the current arena from the heap.
-		//
-		// As of 1.17, the runtime does this automatically
-		// (https://go.dev/cl/270537).
-		ca := mheap.Field("curArena")
-		unused := int64(ca.Field("end").Uintptr() - ca.Field("base").Uintptr())
-		heap -= unused
-		all -= unused
-	}
-	pageSize := p.rtConstants["_PageSize"]
+	pageSize := p.rtConsts["_PageSize"]
 
-	// Span types
-	spanInUse := uint8(p.rtConstants["_MSpanInUse"])
-	spanManual := uint8(p.rtConstants["_MSpanManual"])
-	spanDead := uint8(p.rtConstants["_MSpanDead"])
-	spanFree := uint8(p.rtConstants["_MSpanFree"])
+	// Span types.
+	spanInUse := uint8(p.rtConsts["mSpanInUse"])
+	spanManual := uint8(p.rtConsts["mSpanManual"])
+	spanDead := uint8(p.rtConsts["mSpanDead"])
 
 	// Malloc header constants (go 1.22+)
-	minSizeForMallocHeader := int64(p.rtConstants["minSizeForMallocHeader"])
-	mallocHeaderSize := int64(p.rtConstants["mallocHeaderSize"])
-	maxSmallSize := int64(p.rtConstants["maxSmallSize"])
+	minSizeForMallocHeader := int64(p.rtConsts["minSizeForMallocHeader"])
+	mallocHeaderSize := int64(p.rtConsts["mallocHeaderSize"])
+	maxSmallSize := int64(p.rtConsts["maxSmallSize"])
 
-	abiType := p.tryFindType("abi.Type") // Non-nil expected for Go 1.22+.
+	abiType := p.tryFindType("internal/abi.Type")
 
 	// Process spans.
 	if pageSize%heapInfoSize != 0 {
-		panic(fmt.Sprintf("page size not a multiple of %d", heapInfoSize))
+		return nil, nil, fmt.Errorf("page size not a multiple of %d", heapInfoSize)
 	}
 	allspans := mheap.Field("allspans")
-	var freeSpanSize int64
-	var releasedSpanSize int64
-	var manualSpanSize int64
-	var inUseSpanSize int64
-	var allocSize int64
-	var freeSize int64
-	var spanRoundSize int64
-	var manualAllocSize int64
-	var manualFreeSize int64
 	n := allspans.SliceLen()
 	for i := int64(0); i < n; i++ {
 		s := allspans.SliceIndex(i).Deref()
@@ -471,7 +355,7 @@
 		}
 		switch st.Uint8() {
 		case spanInUse:
-			inUseSpanSize += spanSize
+			stats.inUseSpanSize += spanSize
 			nelems := s.Field("nelems")
 			var n int64
 			if nelems.IsUint16() { // go1.22+
@@ -498,16 +382,16 @@
 			}
 			for i := int64(0); i < n; i++ {
 				if alloc[i] {
-					allocSize += elemSize
+					stats.allocSize += elemSize
 				} else {
-					freeSize += elemSize
+					stats.freeSize += elemSize
 				}
 			}
-			spanRoundSize += spanSize - n*elemSize
+			stats.spanRoundSize += spanSize - n*elemSize
 
 			// initialize heap info records for all inuse spans.
 			for a := min; a < max; a += heapInfoSize {
-				h := p.allocHeapInfo(a)
+				h := heap.getOrCreate(a)
 				h.base = min
 				h.size = elemSize
 			}
@@ -515,11 +399,11 @@
 			// Process special records.
 			for sp := s.Field("specials"); sp.Address() != 0; sp = sp.Field("next") {
 				sp = sp.Deref() // *special to special
-				if sp.Field("kind").Uint8() != uint8(p.rtConstants["_KindSpecialFinalizer"]) {
+				if sp.Field("kind").Uint8() != uint8(p.rtConsts["_KindSpecialFinalizer"]) {
 					// All other specials (just profile records) can't point into the heap.
 					continue
 				}
-				obj := min.Add(int64(sp.Field("offset").Uint16()))
+				obj := min.Add(int64(sp.Field("offset").Uint64()))
 				p.globals = append(p.globals,
 					&Root{
 						Name:  fmt.Sprintf("finalizer for %x", obj),
@@ -533,24 +417,19 @@
 				// the corresponding finalizer data, so we punt on that thorny
 				// issue for now.
 			}
-			// Check if we're in Go 1.22+. If not, then we're done. Otherwise,
-			// we need to discover all the heap pointers in the span here.
-			if !s.HasField("largeType") || !s.HasField("spanclass") {
-				continue
-			}
 			if noscan := s.Field("spanclass").Uint8()&1 != 0; noscan {
 				// No pointers.
 				continue
 			}
 			if elemSize <= minSizeForMallocHeader {
 				// Heap bits in span.
-				bitmapSize := spanSize / ptrSize / 8
+				bitmapSize := spanSize / int64(heap.ptrSize) / 8
 				bitmapAddr := min.Add(spanSize - bitmapSize)
 				for i := int64(0); i < bitmapSize; i++ {
 					bits := p.proc.ReadUint8(bitmapAddr.Add(int64(i)))
 					for j := int64(0); j < 8; j++ {
 						if bits&(uint8(1)<<j) != 0 {
-							p.setHeapPtr(min.Add(ptrSize * (i*8 + j)))
+							heap.setIsPointer(min.Add(int64(heap.ptrSize) * (i*8 + j)))
 						}
 					}
 				}
@@ -573,15 +452,15 @@
 					if typeAddr == 0 {
 						continue
 					}
-					typ := region{p: p, a: typeAddr, typ: abiType}
-					nptrs := int64(typ.Field("PtrBytes").Uintptr()) / ptrSize
-					if typ.Field("Kind_").Uint8()&uint8(p.rtConstants["kindGCProg"]) != 0 {
+					typ := region{p: p.proc, a: typeAddr, typ: abiType}
+					nptrs := int64(typ.Field("PtrBytes").Uintptr()) / int64(heap.ptrSize)
+					if typ.Field("Kind_").Uint8()&uint8(p.rtConsts["kindGCProg"]) != 0 {
 						panic("unexpected GC prog on small allocation")
 					}
 					gcdata := typ.Field("GCData").Address()
 					for i := int64(0); i < nptrs; i++ {
 						if p.proc.ReadUint8(gcdata.Add(i/8))>>uint(i%8)&1 != 0 {
-							p.setHeapPtr(min.Add(off + mallocHeaderSize + i*ptrSize))
+							heap.setIsPointer(min.Add(off + mallocHeaderSize + i*int64(heap.ptrSize)))
 						}
 					}
 				}
@@ -593,153 +472,125 @@
 				// 1:1 with spans, we can be certain largeType is valid as long as the span
 				// is in use.
 				typ := s.Field("largeType").Deref()
-				nptrs := int64(typ.Field("PtrBytes").Uintptr()) / ptrSize
-				if typ.Field("Kind_").Uint8()&uint8(p.rtConstants["kindGCProg"]) != 0 {
+				nptrs := int64(typ.Field("PtrBytes").Uintptr()) / int64(heap.ptrSize)
+				if typ.Field("Kind_").Uint8()&uint8(p.rtConsts["kindGCProg"]) != 0 {
 					panic("large object's GCProg was not unrolled")
 				}
 				gcdata := typ.Field("GCData").Address()
 				for i := int64(0); i < nptrs; i++ {
 					if p.proc.ReadUint8(gcdata.Add(i/8))>>uint(i%8)&1 != 0 {
-						p.setHeapPtr(min.Add(i * ptrSize))
+						heap.setIsPointer(min.Add(i * int64(heap.ptrSize)))
 					}
 				}
 			}
-		case spanFree:
-			freeSpanSize += spanSize
-			if s.HasField("npreleased") { // go 1.11 and earlier
-				nReleased := int64(s.Field("npreleased").Uintptr())
-				releasedSpanSize += nReleased * pageSize
-			} else { // go 1.12 and beyond
-				if s.Field("scavenged").Bool() {
-					releasedSpanSize += spanSize
-				}
-			}
 		case spanDead:
 			// These are just deallocated span descriptors. They use no heap.
 		case spanManual:
-			manualSpanSize += spanSize
-			manualAllocSize += spanSize
-			for x := core.Address(s.Field("manualFreeList").Cast("uintptr").Uintptr()); x != 0; x = p.proc.ReadPtr(x) {
-				manualAllocSize -= elemSize
-				manualFreeSize += elemSize
+			stats.manualSpanSize += spanSize
+			stats.manualAllocSize += spanSize
+			for x := core.Address(s.Field("manualFreeList").Cast(p.findType("uintptr")).Uintptr()); x != 0; x = p.proc.ReadPtr(x) {
+				stats.manualAllocSize -= elemSize
+				stats.manualFreeSize += elemSize
 			}
 		}
 	}
-	if mheap.HasField("pages") { // go1.14+
-		// There are no longer "free" mspans to represent unused pages.
-		// Instead, there are just holes in the pagemap into which we can allocate.
-		// Look through the page allocator and count the total free space.
-		// Also keep track of how much has been scavenged.
-		pages := mheap.Field("pages")
-		chunks := pages.Field("chunks")
-		arenaBaseOffset := p.getArenaBaseOffset()
-		pallocChunkBytes := p.rtConstants["pallocChunkBytes"]
-		pallocChunksL1Bits := p.rtConstants["pallocChunksL1Bits"]
-		pallocChunksL2Bits := p.rtConstants["pallocChunksL2Bits"]
-		inuse := pages.Field("inUse")
-		ranges := inuse.Field("ranges")
-		for i := int64(0); i < ranges.SliceLen(); i++ {
-			r := ranges.SliceIndex(i)
-			baseField := r.Field("base")
-			if baseField.IsStruct() { // go 1.15+
-				baseField = baseField.Field("a")
-			}
-			base := core.Address(baseField.Uintptr())
-			limitField := r.Field("limit")
-			if limitField.IsStruct() { // go 1.15+
-				limitField = limitField.Field("a")
-			}
-			limit := core.Address(limitField.Uintptr())
-			chunkBase := (int64(base) + arenaBaseOffset) / pallocChunkBytes
-			chunkLimit := (int64(limit) + arenaBaseOffset) / pallocChunkBytes
-			for chunkIdx := chunkBase; chunkIdx < chunkLimit; chunkIdx++ {
-				var l1, l2 int64
-				if pallocChunksL1Bits == 0 {
-					l2 = chunkIdx
-				} else {
-					l1 = chunkIdx >> uint(pallocChunksL2Bits)
-					l2 = chunkIdx & (1<<uint(pallocChunksL2Bits) - 1)
-				}
-				chunk := chunks.ArrayIndex(l1).Deref().ArrayIndex(l2)
-				// Count the free bits in this chunk.
-				alloc := chunk.Field("pallocBits")
-				for i := int64(0); i < pallocChunkBytes/pageSize/64; i++ {
-					freeSpanSize += int64(bits.OnesCount64(^alloc.ArrayIndex(i).Uint64())) * pageSize
-				}
-				// Count the scavenged bits in this chunk.
-				scavenged := chunk.Field("scavenged")
-				for i := int64(0); i < pallocChunkBytes/pageSize/64; i++ {
-					releasedSpanSize += int64(bits.OnesCount64(scavenged.ArrayIndex(i).Uint64())) * pageSize
-				}
-			}
-		}
-		// Also count pages in the page cache for each P.
-		allp := p.rtGlobals["allp"]
-		for i := int64(0); i < allp.SliceLen(); i++ {
-			pcache := allp.SliceIndex(i).Deref().Field("pcache")
-			freeSpanSize += int64(bits.OnesCount64(pcache.Field("cache").Uint64())) * pageSize
-			releasedSpanSize += int64(bits.OnesCount64(pcache.Field("scav").Uint64())) * pageSize
-		}
-	}
 
-	p.stats = &Stats{"all", all, []*Stats{
-		&Stats{"text", text, nil},
-		&Stats{"readonly", readOnly, nil},
-		&Stats{"data", data, nil},
-		&Stats{"bss", bss, nil},
-		&Stats{"heap", heap, []*Stats{
-			&Stats{"in use spans", inUseSpanSize, []*Stats{
-				&Stats{"alloc", allocSize, nil},
-				&Stats{"free", freeSize, nil},
-				&Stats{"round", spanRoundSize, nil},
-			}},
-			&Stats{"manual spans", manualSpanSize, []*Stats{
-				&Stats{"alloc", manualAllocSize, nil},
-				&Stats{"free", manualFreeSize, nil},
-			}},
-			&Stats{"free spans", freeSpanSize, []*Stats{
-				&Stats{"retained", freeSpanSize - releasedSpanSize, nil},
-				&Stats{"released", releasedSpanSize, nil},
-			}},
-		}},
-		&Stats{"ptr bitmap", bitmap, nil},
-		&Stats{"span table", spanTable, nil},
-	}}
-
-	var check func(*Stats)
-	check = func(s *Stats) {
-		if len(s.Children) == 0 {
-			return
-		}
-		var sum int64
-		for _, c := range s.Children {
-			sum += c.Size
-		}
-		if sum != s.Size {
-			panic(fmt.Sprintf("check failed for %s: %d vs %d", s.Name, s.Size, sum))
-		}
-		for _, c := range s.Children {
-			check(c)
+	// There are no longer "free" mspans to represent unused pages.
+	// Instead, there are just holes in the pagemap into which we can allocate.
+	// Look through the page allocator and count the total free space.
+	// Also keep track of how much has been scavenged.
+	pages := mheap.Field("pages")
+	chunks := pages.Field("chunks")
+	pallocChunkBytes := p.rtConsts["pallocChunkBytes"]
+	pallocChunksL1Bits := p.rtConsts["pallocChunksL1Bits"]
+	pallocChunksL2Bits := p.rtConsts["pallocChunksL2Bits"]
+	inuse := pages.Field("inUse")
+	ranges := inuse.Field("ranges")
+	for i := int64(0); i < ranges.SliceLen(); i++ {
+		r := ranges.SliceIndex(i)
+		baseField := r.Field("base").Field("a")
+		base := core.Address(baseField.Uintptr())
+		limitField := r.Field("limit").Field("a")
+		limit := core.Address(limitField.Uintptr())
+		chunkBase := (int64(base) + arenaBaseOffset) / pallocChunkBytes
+		chunkLimit := (int64(limit) + arenaBaseOffset) / pallocChunkBytes
+		for chunkIdx := chunkBase; chunkIdx < chunkLimit; chunkIdx++ {
+			var l1, l2 int64
+			if pallocChunksL1Bits == 0 {
+				l2 = chunkIdx
+			} else {
+				l1 = chunkIdx >> uint(pallocChunksL2Bits)
+				l2 = chunkIdx & (1<<uint(pallocChunksL2Bits) - 1)
+			}
+			chunk := chunks.ArrayIndex(l1).Deref().ArrayIndex(l2)
+			// Count the free bits in this chunk.
+			alloc := chunk.Field("pallocBits")
+			for i := int64(0); i < pallocChunkBytes/pageSize/64; i++ {
+				stats.freeSpanSize += int64(bits.OnesCount64(^alloc.ArrayIndex(i).Uint64())) * pageSize
+			}
+			// Count the scavenged bits in this chunk.
+			scavenged := chunk.Field("scavenged")
+			for i := int64(0); i < pallocChunkBytes/pageSize/64; i++ {
+				stats.releasedSpanSize += int64(bits.OnesCount64(scavenged.ArrayIndex(i).Uint64())) * pageSize
+			}
 		}
 	}
-	check(p.stats)
+
+	// Also count pages in the page cache for each P.
+	allp := p.rtGlobals["allp"]
+	for i := int64(0); i < allp.SliceLen(); i++ {
+		pcache := allp.SliceIndex(i).Deref().Field("pcache")
+		stats.freeSpanSize += int64(bits.OnesCount64(pcache.Field("cache").Uint64())) * pageSize
+		stats.releasedSpanSize += int64(bits.OnesCount64(pcache.Field("scav").Uint64())) * pageSize
+	}
+
+	// Create stats.
+	//
+	// TODO(mknyszek): Double-check that our own computations of the group stats match the sums here.
+	return heap, groupStat("all",
+		leafStat("text", stats.text),
+		leafStat("readonly", stats.readOnly),
+		leafStat("data", stats.data),
+		leafStat("bss", stats.bss),
+		groupStat("heap",
+			groupStat("in use spans",
+				leafStat("alloc", stats.allocSize),
+				leafStat("free", stats.freeSize),
+				leafStat("round", stats.spanRoundSize),
+			),
+			groupStat("manual spans",
+				leafStat("alloc", stats.manualAllocSize),
+				leafStat("free", stats.manualFreeSize),
+			),
+			groupStat("free spans",
+				leafStat("retained", stats.freeSpanSize-stats.releasedSpanSize),
+				leafStat("released", stats.releasedSpanSize),
+			),
+		),
+		leafStat("span table", stats.spanTable),
+	), nil
 }
 
-func (p *Process) readGs() {
-	// TODO: figure out how to "flush" running Gs.
+func readGoroutines(p *Process, dwarfVars map[*Func][]dwarfVar) ([]*Goroutine, error) {
 	allgs := p.rtGlobals["allgs"]
 	n := allgs.SliceLen()
+	var goroutines []*Goroutine
 	for i := int64(0); i < n; i++ {
 		r := allgs.SliceIndex(i).Deref()
-		g := p.readG(r)
+		g, err := readGoroutine(p, r, dwarfVars)
+		if err != nil {
+			return nil, fmt.Errorf("reading goroutine: %v", err)
+		}
 		if g == nil {
 			continue
 		}
-		p.goroutines = append(p.goroutines, g)
+		goroutines = append(goroutines, g)
 	}
+	return goroutines, nil
 }
 
-func (p *Process) readG(r region) *Goroutine {
+func readGoroutine(p *Process, r region, dwarfVars map[*Func][]dwarfVar) (*Goroutine, error) {
+	// Set up register descriptors for DWARF stack programs to be executed.
 	g := &Goroutine{r: r}
 	stk := r.Field("stack")
 	g.stackSize = int64(stk.Field("hi").Uintptr() - stk.Field("lo").Uintptr())
@@ -756,41 +607,51 @@
 			}
 		}
 	}
-	st := r.Field("atomicstatus")
-	if st.IsStruct() && st.HasField("value") { // go1.20+
-		st = st.Field("value")
-	}
+	st := r.Field("atomicstatus").Field("value")
 	status := st.Uint32()
-	status &^= uint32(p.rtConstants["_Gscan"])
+	status &^= uint32(p.rtConsts["_Gscan"])
 	var sp, pc core.Address
 	switch status {
-	case uint32(p.rtConstants["_Gidle"]):
-		return g
-	case uint32(p.rtConstants["_Grunnable"]), uint32(p.rtConstants["_Gwaiting"]):
+	case uint32(p.rtConsts["_Gidle"]):
+		return g, nil
+	case uint32(p.rtConsts["_Grunnable"]), uint32(p.rtConsts["_Gwaiting"]):
 		sched := r.Field("sched")
 		sp = core.Address(sched.Field("sp").Uintptr())
 		pc = core.Address(sched.Field("pc").Uintptr())
-	case uint32(p.rtConstants["_Grunning"]):
+	case uint32(p.rtConsts["_Grunning"]):
 		sp = osT.SP()
 		pc = osT.PC()
 		// TODO: back up to the calling frame?
-	case uint32(p.rtConstants["_Gsyscall"]):
+	case uint32(p.rtConsts["_Gsyscall"]):
 		sp = core.Address(r.Field("syscallsp").Uintptr())
 		pc = core.Address(r.Field("syscallpc").Uintptr())
 		// TODO: or should we use the osT registers?
-	case uint32(p.rtConstants["_Gdead"]):
-		return nil
+	case uint32(p.rtConsts["_Gdead"]):
+		return nil, nil
 		// TODO: copystack, others?
 	default:
 		// Unknown state. We can't read the frames, so just bail now.
 		// TODO: make this switch complete and then panic here.
 		// TODO: or just return nil?
-		return g
+		return g, nil
 	}
+
+	// Set up register context.
+	var dregs []*op.DwarfRegister
+	if osT != nil {
+		dregs = hardwareRegs2DWARF(osT.Regs())
+	} else {
+		dregs = hardwareRegs2DWARF(nil)
+	}
+	regs := op.NewDwarfRegisters(p.proc.StaticBase(), dregs, binary.LittleEndian, regnum.AMD64_Rip, regnum.AMD64_Rsp, regnum.AMD64_Rbp, 0)
+
+	// Read all the frames.
+	isCrashFrame := false
 	for {
-		f, err := p.readFrame(sp, pc)
+		f, err := readFrame(p, sp, pc)
 		if err != nil {
-			fmt.Printf("warning: giving up on backtrace: %v\n", err)
+			goid := r.Field("goid").Uint64()
+			fmt.Printf("warning: giving up on backtrace for %d after %d frames: %v\n", goid, len(g.frames), err)
 			break
 		}
 		if f.f.name == "runtime.goexit" {
@@ -801,16 +662,193 @@
 		}
 		g.frames = append(g.frames, f)
 
+		regs.CFA = int64(f.max)
+		regs.FrameBase = int64(f.max)
+
+		// Start with all pointer slots as unnamed.
+		unnamed := map[core.Address]bool{}
+		for a := range f.Live {
+			unnamed[a] = true
+		}
+		conservativeLiveness := isCrashFrame && len(f.Live) == 0
+
+		// Emit roots for DWARF entries.
+		for _, v := range dwarfVars[f.f] {
+			if !(v.lowPC <= f.pc && f.pc < v.highPC) {
+				continue
+			}
+			addr, pieces, err := op.ExecuteStackProgram(*regs, v.instr, int(p.proc.PtrSize()), func(buf []byte, addr uint64) (int, error) {
+				p.proc.ReadAt(buf, core.Address(addr))
+				return len(buf), nil
+			})
+			if err != nil {
+				return nil, fmt.Errorf("failed to execute DWARF stack program for variable %s: %v", v.name, err)
+			}
+			if addr == 0 {
+				// TODO(mknyszek): Handle composites via pieces returned by the stack program.
+				continue
+			}
+			if addr != 0 && len(pieces) == 1 && v.typ.Kind == KindPtr {
+				r := &Root{
+					Name:     v.name,
+					RegName:  regnum.AMD64ToName(pieces[0].Val),
+					RegValue: core.Address(addr),
+					Type:     v.typ,
+					Frame:    f,
+				}
+				g.regRoots = append(g.regRoots, r)
+			} else {
+				r := &Root{
+					Name:  v.name,
+					Addr:  core.Address(addr),
+					Type:  v.typ,
+					Frame: f,
+				}
+				f.roots = append(f.roots, r)
+
+				if conservativeLiveness {
+					// This is a frame that is likely not at a safe point, so the liveness
+					// map is almost certainly empty. If it's not, then great, we can use
+					// that for precise information, but otherwise, let's be conservative
+					// and populate liveness data from the DWARF.
+					r.Type.forEachPointer(0, p.proc.PtrSize(), func(off int64) {
+						f.Live[r.Addr.Add(off)] = true
+					})
+				}
+
+				// Remove this variable from the set of unnamed pointers.
+				for a := r.Addr; a < r.Addr.Add(r.Type.Size); a = a.Add(p.proc.PtrSize()) {
+					delete(unnamed, a)
+				}
+			}
+		}
+
+		// Emit roots for unnamed pointer slots in the frame.
+		// Make deterministic by sorting first.
+		s := make([]core.Address, 0, len(unnamed))
+		for a := range unnamed {
+			s = append(s, a)
+		}
+		sort.Slice(s, func(i, j int) bool { return s[i] < s[j] })
+		for _, a := range s {
+			r := &Root{
+				Name:  "unk",
+				Addr:  a,
+				Type:  p.findType("unsafe.Pointer"),
+				Frame: f,
+			}
+			f.roots = append(f.roots, r)
+		}
+
+		// Figure out how to unwind to the next frame.
 		if f.f.name == "runtime.sigtrampgo" {
+			if osT == nil {
+				// No thread attached to a goroutine in sigtrampgo?
+				break
+			}
+			var ctxt core.Address
+			for _, v := range dwarfVars[f.f] {
+				if v.name != "ctx" {
+					continue
+				}
+				if !(v.lowPC <= f.pc && f.pc < v.highPC) {
+					continue
+				}
+				addr, pieces, err := op.ExecuteStackProgram(*regs, v.instr, int(p.proc.PtrSize()), func(buf []byte, addr uint64) (int, error) {
+					p.proc.ReadAt(buf, core.Address(addr))
+					return len(buf), nil
+				})
+				if err != nil {
+					continue
+				}
+				if addr == 0 {
+					continue
+				}
+				if addr != 0 && len(pieces) == 1 && v.typ.Kind == KindPtr {
+					ctxt = core.Address(addr)
+				} else {
+					ctxt = p.proc.ReadPtr(core.Address(addr))
+				}
+			}
+			if ctxt == 0 {
+				// No way to unwind further.
+				break
+			}
 			// Continue traceback at location where the signal
 			// interrupted normal execution.
-			ctxt := p.proc.ReadPtr(sp.Add(16)) // 3rd arg
-			//ctxt is a *ucontext
+
+			// ctxt is a *ucontext
 			mctxt := ctxt.Add(5 * 8)
 			// mctxt is a *mcontext
-			sp = p.proc.ReadPtr(mctxt.Add(15 * 8))
-			pc = p.proc.ReadPtr(mctxt.Add(16 * 8))
 			// TODO: totally arch-dependent!
+
+			// Read new register context out of mcontext, before we continue.
+			//
+			// type mcontext struct {
+			//     r8          uint64
+			//     r9          uint64
+			//     r10         uint64
+			//     r11         uint64
+			//     r12         uint64
+			//     r13         uint64
+			//     r14         uint64
+			//     r15         uint64
+			//     rdi         uint64
+			//     rsi         uint64
+			//     rbp         uint64
+			//     rbx         uint64
+			//     rdx         uint64
+			//     rax         uint64
+			//     rcx         uint64
+			//     rsp         uint64
+			//     rip         uint64
+			//     eflags      uint64
+			//     cs          uint16
+			//     gs          uint16
+			//     fs          uint16
+			//     __pad0      uint16
+			//     err         uint64
+			//     trapno      uint64
+			//     oldmask     uint64
+			//     cr2         uint64
+			//     fpstate     uint64 // pointer
+			//     __reserved1 [8]uint64
+			// }
+			var hregs []core.Register
+			i := int64(0)
+			readReg := func(name string) uint64 {
+				value := p.proc.ReadUint64(mctxt.Add(i))
+				hregs = append(hregs, core.Register{Name: name, Value: value})
+				i += 8
+				return value
+			}
+			readReg("r8")
+			readReg("r9")
+			readReg("r10")
+			readReg("r11")
+			readReg("r12")
+			readReg("r13")
+			readReg("r14")
+			readReg("r15")
+			readReg("rdi")
+			readReg("rsi")
+			readReg("rbp")
+			readReg("rbx")
+			readReg("rdx")
+			readReg("rax")
+			readReg("rcx")
+			sp = core.Address(readReg("rsp"))
+			pc = core.Address(readReg("rip"))
+			readReg("eflags")
+			readReg("cs")
+			readReg("gs")
+			readReg("fs")
+
+			// Update register state.
+			dregs := hardwareRegs2DWARF(hregs)
+			regs = op.NewDwarfRegisters(p.proc.StaticBase(), dregs, binary.LittleEndian, regnum.AMD64_Rip, regnum.AMD64_Rsp, regnum.AMD64_Rbp, 0)
+
+			isCrashFrame = true
 		} else {
 			sp = f.max
 			pc = core.Address(p.proc.ReadUintptr(sp - 8)) // TODO:amd64 only
@@ -826,10 +864,10 @@
 			pc = core.Address(sched.Field("pc").Uintptr())
 		}
 	}
-	return g
+	return g, nil
 }
 
-func (p *Process) readFrame(sp, pc core.Address) (*Frame, error) {
+func readFrame(p *Process, sp, pc core.Address) (*Frame, error) {
 	f := p.funcTab.find(pc)
 	if f == nil {
 		return nil, fmt.Errorf("cannot find func for pc=%#x", pc)
@@ -845,13 +883,13 @@
 
 	// Find live ptrs in locals
 	live := map[core.Address]bool{}
-	if x := int(p.rtConstants["_FUNCDATA_LocalsPointerMaps"]); x < len(f.funcdata) {
+	if x := int(p.rtConsts["_FUNCDATA_LocalsPointerMaps"]); x < len(f.funcdata) {
 		addr := f.funcdata[x]
 		// TODO: Ideally we should have the same frame size check as
 		// runtime.getStackSize to detect errors when we are missing
 		// the stackmap.
 		if addr != 0 {
-			locals := region{p: p, a: addr, typ: p.findType("runtime.stackmap")}
+			locals := region{p: p.proc, a: addr, typ: p.findType("runtime.stackmap")}
 			n := locals.Field("n").Int32()       // # of bitmaps
 			nbit := locals.Field("nbit").Int32() // # of bits per bitmap
 			idx, err := f.stackMap.find(off)
@@ -874,10 +912,10 @@
 		}
 	}
 	// Same for args
-	if x := int(p.rtConstants["_FUNCDATA_ArgsPointerMaps"]); x < len(f.funcdata) {
+	if x := int(p.rtConsts["_FUNCDATA_ArgsPointerMaps"]); x < len(f.funcdata) {
 		addr := f.funcdata[x]
 		if addr != 0 {
-			args := region{p: p, a: addr, typ: p.findType("runtime.stackmap")}
+			args := region{p: p.proc, a: addr, typ: p.findType("runtime.stackmap")}
 			n := args.Field("n").Int32()       // # of bitmaps
 			nbit := args.Field("nbit").Int32() // # of bits per bitmap
 			idx, err := f.stackMap.find(off)
@@ -909,17 +947,61 @@
 // by category.
 // We maintain the invariant that, if there are children,
 // Size == sum(c.Size for c in Children).
-type Stats struct {
-	Name     string
-	Size     int64
-	Children []*Stats
+type Statistic struct {
+	Name  string
+	Value int64
+
+	children map[string]*Statistic
 }
 
-func (s *Stats) Child(name string) *Stats {
-	for _, c := range s.Children {
-		if c.Name == name {
-			return c
+func leafStat(name string, value int64) *Statistic {
+	return &Statistic{Name: name, Value: value}
+}
+
+func groupStat(name string, children ...*Statistic) *Statistic {
+	var cmap map[string]*Statistic
+	var value int64
+	if len(children) != 0 {
+		cmap = make(map[string]*Statistic)
+		for _, child := range children {
+			cmap[child.Name] = child
+			value += child.Value
 		}
 	}
-	return nil
+	return &Statistic{
+		Name:     name,
+		Value:    value,
+		children: cmap,
+	}
+}
+
+func (s *Statistic) Sub(chain ...string) *Statistic {
+	for _, name := range chain {
+		if s == nil {
+			return nil
+		}
+		s = s.children[name]
+	}
+	return s
+}
+
+func (s *Statistic) setChild(child *Statistic) {
+	if len(s.children) == 0 {
+		panic("cannot add children to leaf statistic")
+	}
+	if oldChild, ok := s.children[child.Name]; ok {
+		s.Value -= oldChild.Value
+	}
+	s.children[child.Name] = child
+	s.Value += child.Value
+}
+
+func (s *Statistic) Children() iter.Seq[*Statistic] {
+	return func(yield func(*Statistic) bool) {
+		for _, child := range s.children {
+			if !yield(child) {
+				return
+			}
+		}
+	}
 }
diff --git a/internal/gocore/region.go b/internal/gocore/region.go
index df09865..b4ef594 100644
--- a/internal/gocore/region.go
+++ b/internal/gocore/region.go
@@ -11,7 +11,7 @@
 // Note that it is the type of the thing in the region,
 // not the type of the reference to the region.
 type region struct {
-	p   *Process
+	p   *core.Process
 	a   core.Address
 	typ *Type
 }
@@ -21,28 +21,28 @@
 	if r.typ.Kind != KindPtr {
 		panic("can't ask for the Address of a non-pointer " + r.typ.Name)
 	}
-	return r.p.proc.ReadPtr(r.a)
+	return r.p.ReadPtr(r.a)
 }
 
 // Int returns the int value stored in r.
 func (r region) Int() int64 {
-	if r.typ.Kind != KindInt || r.typ.Size != r.p.proc.PtrSize() {
+	if r.typ.Kind != KindInt || r.typ.Size != r.p.PtrSize() {
 		panic("not an int: " + r.typ.Name)
 	}
-	return r.p.proc.ReadInt(r.a)
+	return r.p.ReadInt(r.a)
 }
 
 // Uintptr returns the uintptr value stored in r.
 func (r region) Uintptr() uint64 {
-	if r.typ.Kind != KindUint || r.typ.Size != r.p.proc.PtrSize() {
+	if r.typ.Kind != KindUint || r.typ.Size != r.p.PtrSize() {
 		panic("not a uintptr: " + r.typ.Name)
 	}
-	return r.p.proc.ReadUintptr(r.a)
+	return r.p.ReadUintptr(r.a)
 }
 
 // Cast the region to the given type.
-func (r region) Cast(typ string) region {
-	return region{p: r.p, a: r.a, typ: r.p.findType(typ)}
+func (r region) Cast(typ *Type) region {
+	return region{p: r.p, a: r.a, typ: typ}
 }
 
 // Deref loads from a pointer. r must contain a pointer.
@@ -53,7 +53,7 @@
 	if r.typ.Elem == nil {
 		panic("can't deref unsafe.Pointer")
 	}
-	p := r.p.proc.ReadPtr(r.a)
+	p := r.p.ReadPtr(r.a)
 	return region{p: r.p, a: p, typ: r.typ.Elem}
 }
 
@@ -63,7 +63,7 @@
 	if r.typ.Kind != KindUint || r.typ.Size != 8 {
 		panic("bad uint64 type " + r.typ.Name)
 	}
-	return r.p.proc.ReadUint64(r.a)
+	return r.p.ReadUint64(r.a)
 }
 
 // Uint32 returns the uint32 value stored in r.
@@ -72,7 +72,7 @@
 	if r.typ.Kind != KindUint || r.typ.Size != 4 {
 		panic("bad uint32 type " + r.typ.Name)
 	}
-	return r.p.proc.ReadUint32(r.a)
+	return r.p.ReadUint32(r.a)
 }
 
 // Int32 returns the int32 value stored in r.
@@ -81,7 +81,7 @@
 	if r.typ.Kind != KindInt || r.typ.Size != 4 {
 		panic("bad int32 type " + r.typ.Name)
 	}
-	return r.p.proc.ReadInt32(r.a)
+	return r.p.ReadInt32(r.a)
 }
 
 // Uint16 returns the uint16 value stored in r.
@@ -90,7 +90,7 @@
 	if r.typ.Kind != KindUint || r.typ.Size != 2 {
 		panic("bad uint16 type " + r.typ.Name)
 	}
-	return r.p.proc.ReadUint16(r.a)
+	return r.p.ReadUint16(r.a)
 }
 
 // Uint8 returns the uint8 value stored in r.
@@ -99,7 +99,7 @@
 	if r.typ.Kind != KindUint || r.typ.Size != 1 {
 		panic("bad uint8 type " + r.typ.Name)
 	}
-	return r.p.proc.ReadUint8(r.a)
+	return r.p.ReadUint8(r.a)
 }
 
 // Bool returns the bool value stored in r.
@@ -108,7 +108,7 @@
 	if r.typ.Kind != KindBool {
 		panic("bad bool type " + r.typ.Name)
 	}
-	return r.p.proc.ReadUint8(r.a) != 0
+	return r.p.ReadUint8(r.a) != 0
 }
 
 // String returns the value of the string stored in r.
@@ -116,10 +116,10 @@
 	if r.typ.Kind != KindString {
 		panic("bad string type " + r.typ.Name)
 	}
-	p := r.p.proc.ReadPtr(r.a)
-	n := r.p.proc.ReadUintptr(r.a.Add(r.p.proc.PtrSize()))
+	p := r.p.ReadPtr(r.a)
+	n := r.p.ReadUintptr(r.a.Add(r.p.PtrSize()))
 	b := make([]byte, n)
-	r.p.proc.ReadAt(b, p)
+	r.p.ReadAt(b, p)
 	return string(b)
 }
 
@@ -129,7 +129,7 @@
 	if r.typ.Kind != KindSlice {
 		panic("can't index a non-slice")
 	}
-	p := r.p.proc.ReadPtr(r.a)
+	p := r.p.ReadPtr(r.a)
 	return region{p: r.p, a: p.Add(n * r.typ.Elem.Size), typ: r.typ.Elem}
 }
 
@@ -138,7 +138,7 @@
 	if r.typ.Kind != KindSlice {
 		panic("can't Ptr a non-slice")
 	}
-	return region{p: r.p, a: r.a, typ: &Type{Name: "*" + r.typ.Name[2:], Size: r.p.proc.PtrSize(), Kind: KindPtr, Elem: r.typ.Elem}}
+	return region{p: r.p, a: r.a, typ: &Type{Name: "*" + r.typ.Name[2:], Size: r.p.PtrSize(), Kind: KindPtr, Elem: r.typ.Elem}}
 }
 
 // SliceLen returns the length of a slice. r must contain a slice.
@@ -146,7 +146,7 @@
 	if r.typ.Kind != KindSlice {
 		panic("can't len a non-slice")
 	}
-	return r.p.proc.ReadInt(r.a.Add(r.p.proc.PtrSize()))
+	return r.p.ReadInt(r.a.Add(r.p.PtrSize()))
 }
 
 // SliceCap returns the capacity of a slice. r must contain a slice.
@@ -154,7 +154,7 @@
 	if r.typ.Kind != KindSlice {
 		panic("can't cap a non-slice")
 	}
-	return r.p.proc.ReadInt(r.a.Add(2 * r.p.proc.PtrSize()))
+	return r.p.ReadInt(r.a.Add(2 * r.p.PtrSize()))
 }
 
 // Field returns the part of r which contains the field f.
diff --git a/internal/gocore/root.go b/internal/gocore/root.go
index 2c7aade..f44dea0 100644
--- a/internal/gocore/root.go
+++ b/internal/gocore/root.go
@@ -10,9 +10,11 @@
 
 // A Root is an area of memory that might have pointers into the Go heap.
 type Root struct {
-	Name string
-	Addr core.Address
-	Type *Type // always non-nil
+	Name     string
+	Addr     core.Address
+	RegName  string
+	RegValue core.Address // set if Addr == 0
+	Type     *Type        // always non-nil
 	// Frame, if non-nil, points to the frame in which this root lives.
 	// Roots with non-nil Frame fields refer to local variables on a stack.
 	// A stack root might be a large type, with some of its fields live and
diff --git a/internal/gocore/testdata/1.12.zip b/internal/gocore/testdata/1.12.zip
deleted file mode 100644
index 4d2a74a..0000000
--- a/internal/gocore/testdata/1.12.zip
+++ /dev/null
Binary files differ
diff --git a/internal/gocore/testdata/1.13.3.zip b/internal/gocore/testdata/1.13.3.zip
deleted file mode 100644
index 5e13ea6..0000000
--- a/internal/gocore/testdata/1.13.3.zip
+++ /dev/null
Binary files differ
diff --git a/internal/gocore/testdata/1.13.zip b/internal/gocore/testdata/1.13.zip
deleted file mode 100644
index 332944d..0000000
--- a/internal/gocore/testdata/1.13.zip
+++ /dev/null
Binary files differ
diff --git a/internal/gocore/testdata/1.14.zip b/internal/gocore/testdata/1.14.zip
deleted file mode 100644
index 5f225ce..0000000
--- a/internal/gocore/testdata/1.14.zip
+++ /dev/null
Binary files differ
diff --git a/internal/gocore/testdata/1.16.zip b/internal/gocore/testdata/1.16.zip
deleted file mode 100644
index 05fb11a..0000000
--- a/internal/gocore/testdata/1.16.zip
+++ /dev/null
Binary files differ
diff --git a/internal/gocore/testdata/1.17.zip b/internal/gocore/testdata/1.17.zip
deleted file mode 100644
index f96ee79..0000000
--- a/internal/gocore/testdata/1.17.zip
+++ /dev/null
Binary files differ
diff --git a/internal/gocore/testdata/1.18.zip b/internal/gocore/testdata/1.18.zip
deleted file mode 100644
index 105a586..0000000
--- a/internal/gocore/testdata/1.18.zip
+++ /dev/null
Binary files differ
diff --git a/internal/gocore/testdata/1.19.zip b/internal/gocore/testdata/1.19.zip
deleted file mode 100644
index cb02b84..0000000
--- a/internal/gocore/testdata/1.19.zip
+++ /dev/null
Binary files differ
diff --git a/internal/gocore/testdata/core b/internal/gocore/testdata/core
deleted file mode 100644
index 55318de..0000000
--- a/internal/gocore/testdata/core
+++ /dev/null
Binary files differ
diff --git a/internal/gocore/testdata/core1.10 b/internal/gocore/testdata/core1.10
deleted file mode 100644
index 903c176..0000000
--- a/internal/gocore/testdata/core1.10
+++ /dev/null
Binary files differ
diff --git a/internal/gocore/testdata/core1.11 b/internal/gocore/testdata/core1.11
deleted file mode 100644
index e020b01..0000000
--- a/internal/gocore/testdata/core1.11
+++ /dev/null
Binary files differ
diff --git a/internal/gocore/testdata/coretest/test.go b/internal/gocore/testdata/coretest/test.go
index d3ebaf9..d78fa55 100644
--- a/internal/gocore/testdata/coretest/test.go
+++ b/internal/gocore/testdata/coretest/test.go
@@ -1,5 +1,9 @@
 package main
 
+import (
+	"runtime"
+)
+
 // Large is an object that (since Go 1.22) is allocated in a span that has a
 // non-nil largeType field. Meaning it must be (>maxSmallSize-mallocHeaderSize).
 // At the time of writing this is (32768 - 8).
@@ -19,4 +23,5 @@
 	o.arr[14] = 0xDE // Prevent a future smart compiler from allocating o directly on pings stack.
 
 	_ = *(*int)(nil)
+	runtime.KeepAlive(&o)
 }
diff --git a/internal/gocore/testdata/runtimetype.zip b/internal/gocore/testdata/runtimetype.zip
deleted file mode 100644
index f6a988a..0000000
--- a/internal/gocore/testdata/runtimetype.zip
+++ /dev/null
Binary files differ
diff --git a/internal/gocore/testdata/tmp/test b/internal/gocore/testdata/tmp/test
deleted file mode 100755
index f3353b2..0000000
--- a/internal/gocore/testdata/tmp/test
+++ /dev/null
Binary files differ
diff --git a/internal/gocore/testdata/tmp/test1.10 b/internal/gocore/testdata/tmp/test1.10
deleted file mode 100755
index 6839de6..0000000
--- a/internal/gocore/testdata/tmp/test1.10
+++ /dev/null
Binary files differ
diff --git a/internal/gocore/testdata/tmp/test1.11 b/internal/gocore/testdata/tmp/test1.11
deleted file mode 100755
index 0da844f..0000000
--- a/internal/gocore/testdata/tmp/test1.11
+++ /dev/null
Binary files differ
diff --git a/internal/gocore/type.go b/internal/gocore/type.go
index b3a4b27..c983e64 100644
--- a/internal/gocore/type.go
+++ b/internal/gocore/type.go
@@ -123,116 +123,74 @@
 // return the number of bytes of the variable int and its value,
 // which means the length of a name.
 func readNameLen(p *Process, a core.Address) (int64, int64) {
-	if p.is117OrGreater {
-		v := 0
-		for i := 0; ; i++ {
-			x := p.proc.ReadUint8(a.Add(int64(i + 1)))
-			v += int(x&0x7f) << (7 * i)
-			if x&0x80 == 0 {
-				return int64(i + 1), int64(v)
-			}
+	v := 0
+	for i := 0; ; i++ {
+		x := p.proc.ReadUint8(a.Add(int64(i + 1)))
+		v += int(x&0x7f) << (7 * i)
+		if x&0x80 == 0 {
+			return int64(i + 1), int64(v)
 		}
-	} else {
-		n1 := p.proc.ReadUint8(a.Add(1))
-		n2 := p.proc.ReadUint8(a.Add(2))
-		n := uint16(n1)<<8 + uint16(n2)
-		return 2, int64(n)
 	}
 }
 
 // runtimeType is a thin wrapper around a runtime._type (AKA abi.Type) region
 // that abstracts over the name changes seen in Go 1.21.
 type runtimeType struct {
-	reg        region
-	hasAbiType bool // True if Go 1.21+
+	reg region
 }
 
 // findRuntimeType finds either abi.Type (Go 1.21+) or runtime._type.
 func (p *Process) findRuntimeType(a core.Address) runtimeType {
-	typ := runtimeType{
-		reg:        region{p: p, a: a, typ: p.tryFindType("abi.Type")},
-		hasAbiType: true,
+	return runtimeType{
+		reg: region{p: p.proc, a: a, typ: p.tryFindType("internal/abi.Type")},
 	}
-	if typ.reg.typ == nil {
-		typ.reg.typ = p.findType("runtime._type")
-		typ.hasAbiType = false
-	}
-	return typ
 }
 
 // Size_ is either abi.Type.Size_ or runtime._type.Size_.
 func (r runtimeType) Size_() int64 {
-	if !r.hasAbiType {
-		return int64(r.reg.Field("size").Uintptr())
-	}
 	return int64(r.reg.Field("Size_").Uintptr())
 }
 
 // TFlag is either abi.Type.TFlag or runtime._type.TFlag.
 func (r runtimeType) TFlag() uint8 {
-	if !r.hasAbiType {
-		return r.reg.Field("tflag").Uint8()
-	}
 	return r.reg.Field("TFlag").Uint8()
 }
 
 // Str is either abi.Type.Str or runtime._type.Str.
 func (r runtimeType) Str() int64 {
-	if !r.hasAbiType {
-		return int64(r.reg.Field("str").Int32())
-	}
 	return int64(r.reg.Field("Str").Int32())
 }
 
 // PtrBytes is either abi.Type.PtrBytes or runtime._type.PtrBytes.
 func (r runtimeType) PtrBytes() int64 {
-	if !r.hasAbiType {
-		return int64(r.reg.Field("ptrdata").Uintptr())
-	}
 	return int64(r.reg.Field("PtrBytes").Uintptr())
 }
 
 // Kind_ is either abi.Type.Kind_ or runtime._type.Kind_.
 func (r runtimeType) Kind_() uint8 {
-	if !r.hasAbiType {
-		return r.reg.Field("kind").Uint8()
-	}
 	return r.reg.Field("Kind_").Uint8()
 }
 
 // GCData is either abi.Type.GCData or runtime._type.GCData.
 func (r runtimeType) GCData() core.Address {
-	if !r.hasAbiType {
-		return r.reg.Field("gcdata").Address()
-	}
 	return r.reg.Field("GCData").Address()
 }
 
 // runtimeItab is a thin wrapper around a abi.ITab (used to be runtime.itab). It
 // abstracts over name/package changes in Go 1.21.
 type runtimeItab struct {
-	typ        *Type
-	hasAbiITab bool // True if Go 1.21+
+	typ *Type
 }
 
 // findItab finds either abi.ITab (Go 1.21+) or runtime.itab.
 func (p *Process) findItab() runtimeItab {
-	typ := runtimeItab{
-		typ:        p.tryFindType("abi.ITab"),
-		hasAbiITab: true,
+	return runtimeItab{
+		typ: p.tryFindType("internal/abi.ITab"),
 	}
-	if typ.typ == nil {
-		typ.typ = p.findType("runtime.itab")
-		typ.hasAbiITab = false
-	}
-	return typ
 }
 
 // Type is the field representing either abi.ITab.Type or runtime.itab._type.
 func (r runtimeItab) Type() *Field {
-	if !r.hasAbiITab {
-		return r.typ.field("_type")
-	}
 	return r.typ.field("Type")
 }
 
@@ -241,7 +199,7 @@
 // If "d" is 0, just return *Type and not to do the interface disambiguation.
 // Guaranteed to return a non-nil *Type.
 func (p *Process) runtimeType2Type(a core.Address, d core.Address) *Type {
-	if t := p.runtimeMap[a]; t != nil {
+	if t := p.rtTypeMap[a]; t != nil {
 		return t
 	}
 
@@ -266,7 +224,7 @@
 		b := make([]byte, n)
 		p.proc.ReadAt(b, x.Add(i+1))
 		name = string(b)
-		if r.TFlag()&uint8(p.rtConstants["tflagExtraStar"]) != 0 {
+		if r.TFlag()&uint8(p.rtConsts["tflagExtraStar"]) != 0 {
 			name = name[1:]
 		}
 	} else {
@@ -280,7 +238,7 @@
 	ptrSize := p.proc.PtrSize()
 	nptrs := int64(r.PtrBytes()) / ptrSize
 	var ptrs []int64
-	if r.Kind_()&uint8(p.rtConstants["kindGCProg"]) == 0 {
+	if r.Kind_()&uint8(p.rtConsts["kindGCProg"]) == 0 {
 		gcdata := r.GCData()
 		for i := int64(0); i < nptrs; i++ {
 			if p.proc.ReadUint8(gcdata.Add(i/8))>>uint(i%8)&1 != 0 {
@@ -294,43 +252,11 @@
 	// Find a Type that matches this type.
 	// (The matched type will be one constructed from DWARF info.)
 	// It must match name, size, and pointer bits.
-	var candidates []*Type
-	for _, t := range p.runtimeNameMap[name] {
-		if size == t.Size && equal(ptrs, t.ptrs()) {
-			candidates = append(candidates, t)
-		}
+	t := p.rtTypeByName[name]
+	if t != nil && (size != t.Size || !equal(ptrs, t.ptrs())) {
+		t = nil
 	}
-	// There may be multiple candidates, when they are the pointers to the same type name,
-	// in the same package name, but in the different package paths. eg. path-1/pkg.Foo and path-2/pkg.Foo.
-	// Match the object size may be a proper choice, just for try best, since we have no other choices.
-	// If the interface is of type T, for direct interfaces, that pointer points to a T.Elem.
-	if d != 0 && len(candidates) > 1 && !ifaceIndir(a, p) {
-		ptr := p.proc.ReadPtr(d)
-		obj, off := p.FindObject(ptr)
-		// only useful while it point to the head of an object,
-		// otherwise, the GC object size should bigger than the size of the type.
-		if obj != 0 && off == 0 {
-			sz := p.Size(obj)
-			var tmp []*Type
-			for _, t := range candidates {
-				if t.Elem != nil && t.Elem.Size == sz {
-					tmp = append(tmp, t)
-				}
-			}
-			if len(tmp) > 0 {
-				candidates = tmp
-			}
-		}
-	}
-	var t *Type
-	if len(candidates) > 0 {
-		// If a runtime type matches more than one DWARF type,
-		// pick one arbitrarily.
-		// This looks mostly harmless. DWARF has some redundant entries.
-		// For example, [32]uint8 appears twice.
-		// TODO: investigate the reason for this duplication.
-		t = candidates[0]
-	} else {
+	if t == nil {
 		// There's no corresponding DWARF type.  Make our own.
 		t = &Type{Name: name, Size: size, Kind: KindStruct}
 		n := t.Size / ptrSize
@@ -360,9 +286,12 @@
 			// TODO: tail of <ptrSize data.
 		}
 	}
-	// Memoize.
-	p.runtimeMap[a] = t
 
+	// Memoize.
+	if p.rtTypeMap == nil {
+		p.rtTypeMap = make(map[core.Address]*Type)
+	}
+	p.rtTypeMap[a] = t
 	return t
 }
 
@@ -476,181 +405,186 @@
 
 // typeHeap tries to label all the heap objects with types.
 func (p *Process) typeHeap() {
-	p.initTypeHeap.Do(func() {
-		// Type info for the start of each object. a.k.a. "0 offset" typings.
-		p.types = make([]typeInfo, p.nObj)
+	p.initTypeHeap.Do(p.doTypeHeap)
+}
 
-		// Type info for the interior of objects, a.k.a. ">0 offset" typings.
-		// Type information is arranged in chunks. Chunks are stored in an
-		// arbitrary order, and are guaranteed to not overlap. If types are
-		// equal, chunks are also guaranteed not to abut.
-		// Interior typings are kept separate because they hopefully are rare.
-		// TODO: They aren't really that rare. On some large heaps I tried
-		// ~50% of objects have an interior pointer into them.
-		// Keyed by object index.
-		interior := map[int][]typeChunk{}
+func (p *Process) doTypeHeap() {
+	// Type info for the start of each object. a.k.a. "0 offset" typings.
+	p.types = make([]typeInfo, p.nObj)
 
-		// Typings we know about but haven't scanned yet.
-		type workRecord struct {
-			a core.Address
-			t *Type
-			r int64
+	// Type info for the interior of objects, a.k.a. ">0 offset" typings.
+	// Type information is arranged in chunks. Chunks are stored in an
+	// arbitrary order, and are guaranteed to not overlap. If types are
+	// equal, chunks are also guaranteed not to abut.
+	// Interior typings are kept separate because they hopefully are rare.
+	// TODO: They aren't really that rare. On some large heaps I tried
+	// ~50% of objects have an interior pointer into them.
+	// Keyed by object index.
+	interior := map[int][]typeChunk{}
+
+	// Typings we know about but haven't scanned yet.
+	type workRecord struct {
+		a core.Address
+		t *Type
+		r int64
+	}
+	var work []workRecord
+
+	// add records the fact that we know the object at address a has
+	// r copies of type t.
+	add := func(a core.Address, t *Type, r int64) {
+		if a == 0 { // nil pointer
+			return
 		}
-		var work []workRecord
-
-		// add records the fact that we know the object at address a has
-		// r copies of type t.
-		add := func(a core.Address, t *Type, r int64) {
-			if a == 0 { // nil pointer
-				return
+		i, off := p.findObjectIndex(a)
+		if i < 0 { // pointer doesn't point to an object in the Go heap
+			return
+		}
+		if off == 0 {
+			// We have a 0-offset typing. Replace existing 0-offset typing
+			// if the new one is larger.
+			ot := p.types[i].t
+			or := p.types[i].r
+			if ot == nil || r*t.Size > or*ot.Size {
+				if t == ot {
+					// Scan just the new section.
+					work = append(work, workRecord{
+						a: a.Add(or * ot.Size),
+						t: t,
+						r: r - or,
+					})
+				} else {
+					// Rescan the whole typing using the updated type.
+					work = append(work, workRecord{
+						a: a,
+						t: t,
+						r: r,
+					})
+				}
+				p.types[i].t = t
+				p.types[i].r = r
 			}
-			i, off := p.findObjectIndex(a)
-			if i < 0 { // pointer doesn't point to an object in the Go heap
-				return
-			}
-			if off == 0 {
-				// We have a 0-offset typing. Replace existing 0-offset typing
-				// if the new one is larger.
-				ot := p.types[i].t
-				or := p.types[i].r
-				if ot == nil || r*t.Size > or*ot.Size {
-					if t == ot {
-						// Scan just the new section.
-						work = append(work, workRecord{
-							a: a.Add(or * ot.Size),
-							t: t,
-							r: r - or,
-						})
-					} else {
-						// Rescan the whole typing using the updated type.
-						work = append(work, workRecord{
-							a: a,
-							t: t,
-							r: r,
-						})
-					}
-					p.types[i].t = t
-					p.types[i].r = r
-				}
-				return
-			}
+			return
+		}
 
-			// Add an interior typing to object #i.
-			c := typeChunk{off: off, t: t, r: r}
+		// Add an interior typing to object #i.
+		c := typeChunk{off: off, t: t, r: r}
 
-			// Merge the given typing into the chunks we already know.
-			// TODO: this could be O(n) per insert if there are lots of internal pointers.
-			chunks := interior[i]
-			newchunks := chunks[:0]
-			addWork := true
-			for _, d := range chunks {
-				if c.max() <= d.min() || c.min() >= d.max() {
-					// c does not overlap with d.
-					if c.t == d.t && (c.max() == d.min() || c.min() == d.max()) {
-						// c and d abut and share the same base type. Merge them.
-						c = c.merge(d)
-						continue
-					}
-					// Keep existing chunk d.
-					// Overwrites chunks slice, but we're only merging chunks so it
-					// can't overwrite to-be-processed elements.
-					newchunks = append(newchunks, d)
-					continue
-				}
-				// There is some overlap. There are a few possibilities:
-				// 1) One is completely contained in the other.
-				// 2) Both are slices of a larger underlying array.
-				// 3) Some unsafe trickery has happened. Non-containing overlap
-				//    can only happen in safe Go via case 2.
-				if c.min() >= d.min() && c.max() <= d.max() {
-					// 1a: c is contained within the existing chunk d.
-					// Note that there can be a type mismatch between c and d,
-					// but we don't care. We use the larger chunk regardless.
-					c = d
-					addWork = false // We've already scanned all of c.
-					continue
-				}
-				if d.min() >= c.min() && d.max() <= c.max() {
-					// 1b: existing chunk d is completely covered by c.
-					continue
-				}
-				if c.t == d.t && c.matchingAlignment(d) {
-					// Union two regions of the same base type. Case 2 above.
+		// Merge the given typing into the chunks we already know.
+		// TODO: this could be O(n) per insert if there are lots of internal pointers.
+		chunks := interior[i]
+		newchunks := chunks[:0]
+		addWork := true
+		for _, d := range chunks {
+			if c.max() <= d.min() || c.min() >= d.max() {
+				// c does not overlap with d.
+				if c.t == d.t && (c.max() == d.min() || c.min() == d.max()) {
+					// c and d abut and share the same base type. Merge them.
 					c = c.merge(d)
 					continue
 				}
-				if c.size() < d.size() {
-					// Keep the larger of the two chunks.
-					c = d
-					addWork = false
-				}
-			}
-			// Add new chunk to list of chunks for object.
-			newchunks = append(newchunks, c)
-			interior[i] = newchunks
-			// Also arrange to scan the new chunk. Note that if we merged
-			// with an existing chunk (or chunks), those will get rescanned.
-			// Duplicate work, but that's ok. TODO: but could be expensive.
-			if addWork {
-				work = append(work, workRecord{
-					a: a.Add(c.off - off),
-					t: c.t,
-					r: c.r,
-				})
-			}
-		}
-
-		// Get typings starting at roots.
-		fr := &frameReader{p: p}
-		p.ForEachRoot(func(r *Root) bool {
-			if r.Frame != nil {
-				fr.live = r.Frame.Live
-				p.typeObject(r.Addr, r.Type, fr, add)
-			} else {
-				p.typeObject(r.Addr, r.Type, p.proc, add)
-			}
-			return true
-		})
-
-		// Propagate typings through the heap.
-		for len(work) > 0 {
-			c := work[len(work)-1]
-			work = work[:len(work)-1]
-			switch c.t.Kind {
-			case KindBool, KindInt, KindUint, KindFloat, KindComplex:
-				// Don't do O(n) function calls for big primitive slices
+				// Keep existing chunk d.
+				// Overwrites chunks slice, but we're only merging chunks so it
+				// can't overwrite to-be-processed elements.
+				newchunks = append(newchunks, d)
 				continue
 			}
-			for i := int64(0); i < c.r; i++ {
-				p.typeObject(c.a.Add(i*c.t.Size), c.t, p.proc, add)
+			// There is some overlap. There are a few possibilities:
+			// 1) One is completely contained in the other.
+			// 2) Both are slices of a larger underlying array.
+			// 3) Some unsafe trickery has happened. Non-containing overlap
+			//    can only happen in safe Go via case 2.
+			if c.min() >= d.min() && c.max() <= d.max() {
+				// 1a: c is contained within the existing chunk d.
+				// Note that there can be a type mismatch between c and d,
+				// but we don't care. We use the larger chunk regardless.
+				c = d
+				addWork = false // We've already scanned all of c.
+				continue
+			}
+			if d.min() >= c.min() && d.max() <= c.max() {
+				// 1b: existing chunk d is completely covered by c.
+				continue
+			}
+			if c.t == d.t && c.matchingAlignment(d) {
+				// Union two regions of the same base type. Case 2 above.
+				c = c.merge(d)
+				continue
+			}
+			if c.size() < d.size() {
+				// Keep the larger of the two chunks.
+				c = d
+				addWork = false
 			}
 		}
+		// Add new chunk to list of chunks for object.
+		newchunks = append(newchunks, c)
+		interior[i] = newchunks
+		// Also arrange to scan the new chunk. Note that if we merged
+		// with an existing chunk (or chunks), those will get rescanned.
+		// Duplicate work, but that's ok. TODO: but could be expensive.
+		if addWork {
+			work = append(work, workRecord{
+				a: a.Add(c.off - off),
+				t: c.t,
+				r: c.r,
+			})
+		}
+	}
 
-		// Merge any interior typings with the 0-offset typing.
-		for i, chunks := range interior {
-			t := p.types[i].t
-			r := p.types[i].r
-			if t == nil {
-				continue // We have no type info at offset 0.
-			}
-			for _, c := range chunks {
-				if c.max() <= r*t.Size {
-					// c is completely contained in the 0-offset typing. Ignore it.
-					continue
-				}
-				if c.min() <= r*t.Size {
-					// Typings overlap or abut. Extend if we can.
-					if c.t == t && c.min()%t.Size == 0 {
-						r = c.max() / t.Size
-						p.types[i].r = r
-					}
-					continue
-				}
-				// Note: at this point we throw away any interior typings that weren't
-				// merged with the 0-offset typing.  TODO: make more use of this info.
-			}
+	// Get typings starting at roots.
+	fr := &frameReader{p: p}
+	p.ForEachRoot(func(r *Root) bool {
+		if r.Frame != nil {
+			fr.live = r.Frame.Live
+			p.typeObject(r.Addr, r.Type, fr, add)
+		} else if r.Addr == 0 && r.Type.Kind == KindPtr && r.Type.Elem != nil {
+			p.typeObject(r.RegValue, r.Type.Elem, fr, add)
+		} else {
+			p.typeObject(r.Addr, r.Type, p.proc, add)
 		}
+		return true
 	})
+
+	// Propagate typings through the heap.
+	for len(work) > 0 {
+		c := work[len(work)-1]
+		work = work[:len(work)-1]
+		switch c.t.Kind {
+		case KindBool, KindInt, KindUint, KindFloat, KindComplex:
+			// Don't do O(n) function calls for big primitive slices
+			continue
+		}
+		for i := int64(0); i < c.r; i++ {
+			p.typeObject(c.a.Add(i*c.t.Size), c.t, p.proc, add)
+		}
+	}
+
+	// Merge any interior typings with the 0-offset typing.
+	for i, chunks := range interior {
+		t := p.types[i].t
+		r := p.types[i].r
+		if t == nil {
+			continue // We have no type info at offset 0.
+		}
+		for _, c := range chunks {
+			if c.max() <= r*t.Size {
+				// c is completely contained in the 0-offset typing. Ignore it.
+				continue
+			}
+			if c.min() <= r*t.Size {
+				// Typings overlap or abut. Extend if we can.
+				if c.t == t && c.min()%t.Size == 0 {
+					r = c.max() / t.Size
+					p.types[i].r = r
+				}
+				continue
+			}
+			// Note: at this point we throw away any interior typings that weren't
+			// merged with the 0-offset typing.  TODO: make more use of this info.
+		}
+	}
+
 }
 
 type reader interface {
@@ -689,11 +623,7 @@
 		} else {
 			typeName = matches[1] + "." + matches[3]
 		}
-		s := p.runtimeNameMap[typeName]
-		if len(s) > 0 {
-			// TODO: filter with the object size when there are multiple candidates.
-			return s[0]
-		}
+		return p.rtTypeByName[typeName]
 	}
 	return nil
 }
@@ -701,7 +631,7 @@
 // ifaceIndir reports whether t is stored indirectly in an interface value.
 func ifaceIndir(t core.Address, p *Process) bool {
 	typr := p.findRuntimeType(t)
-	return typr.Kind_()&uint8(p.rtConstants["kindDirectIface"]) == 0
+	return typr.Kind_()&uint8(p.rtConsts["kindDirectIface"]) == 0
 }
 
 // typeObject takes an address and a type for the data at that address.
@@ -849,3 +779,27 @@
 		panic(fmt.Sprintf("unknown type kind %s\n", t.Kind))
 	}
 }
+
+// forEachPointer iterates over each pointer in the type, emitting the offset of the
+// pointer in the type.
+func (t *Type) forEachPointer(baseOffset, ptrSize int64, yield func(off int64)) {
+	switch t.Kind {
+	case KindBool, KindInt, KindUint, KindFloat, KindComplex:
+		// Nothing to do
+	case KindEface, KindIface:
+		yield(ptrSize)
+	case KindString, KindSlice, KindPtr, KindFunc:
+		yield(baseOffset)
+	case KindArray:
+		n := t.Elem.Size
+		for i := int64(0); i < t.Count; i++ {
+			t.Elem.forEachPointer(baseOffset+i*n, ptrSize, yield)
+		}
+	case KindStruct:
+		for _, f := range t.Fields {
+			f.Type.forEachPointer(baseOffset+f.Off, ptrSize, yield)
+		}
+	default:
+		panic(fmt.Sprintf("unknown type kind %s\n", t.Kind))
+	}
+}