internal/core: refactor loading for maintainability

Loading a new core.Process in core.Core is a rather long, complex
process. Currently, this function creates a stub Process with very few
fields set, and then subsequent method calls initially the remaining
fields.

The problem with this approach is that the inputs/requirements and
outputs of these initialization methods is poorly documented. These
methods (e.g., readCore) usually have some prerequisite fields in Process
that must be set on entry, and some fields that they set prior to
return.

Neither of these are well documented, and many are far from obvious. For
example, Process.mainExecName is initialized in readCore -> readNote ->
readNTFile -> openMappedFile.

This is made more of a mess by the fact that Process tries to do most
initialization with a single pass, resulting in fields getting
initialized in unintuitive locations (such as mainExecName).

These combine to make refactoring to support new functionality
difficult, as changing ordering breaks implicit dependencies between
methods.

This CL addresses these issues by eliminating the iterative
initialization of Process. Instead, Process is only instantiated once
all fields are ready. During loading, all work is done by free
functions/types, where inputs and outputs are explicit via function
arguments/returns.

This may inadvertently improve golang/go#44757, as it makes main binary
lookup more explicit.

This CL intends to keep behavior the same, but there are a few changes:

* If the entry point is unknown, We no longer apply the heuristic of
  assuming that first executable mapping is the main binary. This could
  be added back, but it is probably better to ensure that the entry
  point is available on all architectures.

* Failure to ELF parse a file for symbols isn't a hard error (as the
  mapped file might not even be an ELF).

For golang/go#57447.

Change-Id: I7c3712ccb99ceddddc6adbae57d477c25ff4e5ba
Reviewed-on: https://go-review.googlesource.com/c/debug/+/506558
Reviewed-by: Michael Knyszek <mknyszek@google.com>
Run-TryBot: Michael Pratt <mpratt@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/internal/core/core_test.go b/internal/core/core_test.go
index 654422e..7c16bda 100644
--- a/internal/core/core_test.go
+++ b/internal/core/core_test.go
@@ -45,7 +45,7 @@
 		}
 
 		a := s["main.main"]
-		m := p.findMapping(a)
+		m := p.pageTable.findMapping(a)
 		if m == nil {
 			t.Errorf("text mapping missing")
 		}
@@ -61,7 +61,7 @@
 		}
 
 		a = s["runtime.class_to_size"]
-		m = p.findMapping(a)
+		m = p.pageTable.findMapping(a)
 		if m == nil {
 			t.Errorf("data mapping missing")
 		}
diff --git a/internal/core/mapping.go b/internal/core/mapping.go
index 18ef4e2..4c8c3cf 100644
--- a/internal/core/mapping.go
+++ b/internal/core/mapping.go
@@ -29,6 +29,16 @@
 	contents []byte
 }
 
+// namedMapping is equivalent to Mapping, just using the filename rather than
+// opened file.
+type namedMapping struct {
+	min Address
+	max Address
+
+	f   string // filename backing this region
+	off int64  // offset of start of this mapping in f
+}
+
 // Min returns the lowest virtual address of the mapping.
 func (m *Mapping) Min() Address {
 	return m.min
@@ -112,8 +122,8 @@
 const pageSize Address = 1 << 12
 
 // findMapping is simple enough that it inlines.
-func (p *Process) findMapping(a Address) *Mapping {
-	t3 := p.pageTable[a>>52]
+func (p *pageTable4) findMapping(a Address) *Mapping {
+	t3 := p[a>>52]
 	if t3 == nil {
 		return nil
 	}
@@ -132,7 +142,7 @@
 	return t0[a>>12%(1<<10)]
 }
 
-func (p *Process) addMapping(m *Mapping) error {
+func (p *pageTable4) addMapping(m *Mapping) error {
 	if m.min%(pageSize) != 0 {
 		return fmt.Errorf("mapping start %x isn't a multiple of 4096", m.min)
 	}
@@ -141,10 +151,10 @@
 	}
 	for a := m.min; a < m.max; a += 1 << 12 {
 		i3 := a >> 52
-		t3 := p.pageTable[i3]
+		t3 := p[i3]
 		if t3 == nil {
 			t3 = new(pageTable3)
-			p.pageTable[i3] = t3
+			p[i3] = t3
 		}
 		i2 := a >> 42 % (1 << 10)
 		t2 := t3[i2]
@@ -244,3 +254,29 @@
 	}
 	s.mappings = newMappings
 }
+
+// splitMappingsAt ensures that a is not in the middle of any mapping.
+// Splits mappings as necessary.
+func (s *splicedMemory) splitMappingsAt(a Address) {
+	for _, m := range s.mappings {
+		if a < m.min || a > m.max {
+			continue
+		}
+		if a == m.min || a == m.max {
+			return
+		}
+		// Split this mapping at a.
+		m2 := new(Mapping)
+		*m2 = *m
+		m.max = a
+		m2.min = a
+		if m2.f != nil {
+			m2.off += m.Size()
+		}
+		if m2.origF != nil {
+			m2.origOff += m.Size()
+		}
+		s.mappings = append(s.mappings, m2)
+		return
+	}
+}
diff --git a/internal/core/process.go b/internal/core/process.go
index cc7328b..4415493 100644
--- a/internal/core/process.go
+++ b/internal/core/process.go
@@ -29,36 +29,90 @@
 	"syscall"
 )
 
+// TODO: add these to debug/elf?
+const (
+	_NT_FILE elf.NType = 0x46494c45
+	_NT_AUXV elf.NType = 0x6 // auxv
+)
+
 // A Process represents the state of the process that core dumped.
 type Process struct {
-	base string   // base directory from which files in the core can be found
-	exe  *os.File // user-supplied main executable path
-
-	files        map[string]*file // files found from the note section
-	mainExecName string           // open main executable name
+	meta metadata // basic metadata about the core
 
 	entryPoint Address
-	memory     splicedMemory // virtual address mappings
-	threads    []*Thread     // os threads (TODO: map from pid?)
+	args       string    // first part of args retrieved from NT_PRPSINFO
+	threads    []*Thread // os threads (TODO: map from pid?)
 
-	arch         string             // amd64, ...
-	ptrSize      int64              // 4 or 8
-	logPtrSize   uint               // 2 or 3
-	byteOrder    binary.ByteOrder   //
-	littleEndian bool               // redundant with byteOrder
-	syms         map[string]Address // symbols (could be empty if executable is stripped)
-	symErr       error              // an error encountered while reading symbols
-	dwarf        *dwarf.Data        // debugging info (could be nil)
-	dwarfErr     error              // an error encountered while reading DWARF
-	pageTable    pageTable4         // for fast address->mapping lookups
-	args         string             // first part of args retrieved from NT_PRPSINFO
+	memory    splicedMemory // virtual address mappings
+	pageTable pageTable4    // for fast address->mapping lookups
+
+	syms     map[string]Address // symbols (could be empty if executable is stripped)
+	symErr   error              // an error encountered while reading symbols
+	dwarf    *dwarf.Data        // debugging info (could be nil)
+	dwarfErr error              // an error encountered while reading DWARF
 
 	warnings []string // warnings generated during loading
 }
 
-type file struct {
-	f   *os.File
-	err error
+type metadata struct {
+	arch         string           // amd64, ...
+	ptrSize      int64            // 4 or 8
+	logPtrSize   uint             // 2 or 3
+	byteOrder    binary.ByteOrder //
+	littleEndian bool             // redundant with byteOrder
+}
+
+func newMetadata(coreElf *elf.File) (metadata, error) {
+	if coreElf.Type != elf.ET_CORE {
+		return metadata{}, fmt.Errorf("not a core file")
+	}
+
+	var meta metadata
+	switch coreElf.Class {
+	case elf.ELFCLASS32:
+		meta.ptrSize = 4
+		meta.logPtrSize = 2
+	case elf.ELFCLASS64:
+		meta.ptrSize = 8
+		meta.logPtrSize = 3
+	default:
+		return metadata{}, fmt.Errorf("unknown elf class %s", coreElf.Class)
+	}
+
+	switch coreElf.Machine {
+	case elf.EM_386:
+		meta.arch = "386"
+	case elf.EM_X86_64:
+		meta.arch = "amd64"
+	case elf.EM_ARM:
+		meta.arch = "arm"
+	case elf.EM_AARCH64:
+		meta.arch = "arm64"
+	case elf.EM_MIPS:
+		meta.arch = "mips"
+	case elf.EM_MIPS_RS3_LE:
+		meta.arch = "mipsle"
+		// TODO: value for mips64?
+	case elf.EM_PPC64:
+		if coreElf.ByteOrder.String() == "LittleEndian" {
+			meta.arch = "ppc64le"
+		} else {
+			meta.arch = "ppc64"
+		}
+	case elf.EM_S390:
+		meta.arch = "s390x"
+	default:
+		return metadata{}, fmt.Errorf("unknown arch %s\n", coreElf.Machine)
+	}
+
+	meta.byteOrder = coreElf.ByteOrder
+	// We also compute explicitly what byte order the inferior is.
+	// Just using p.byteOrder to decode fields makes any arguments passed to it
+	// escape to the heap.  We use explicit binary.{Little,Big}Endian.UintXX
+	// calls when we want to avoid heap-allocating the buffer.
+	meta.littleEndian = meta.byteOrder.String() == "LittleEndian"
+
+	return meta, nil
 }
 
 // Mappings returns a list of virtual memory mappings for p.
@@ -68,13 +122,13 @@
 
 // Readable reports whether the address a is readable.
 func (p *Process) Readable(a Address) bool {
-	return p.findMapping(a) != nil
+	return p.pageTable.findMapping(a) != nil
 }
 
 // ReadableN reports whether the n bytes starting at address a are readable.
 func (p *Process) ReadableN(a Address, n int64) bool {
 	for {
-		m := p.findMapping(a)
+		m := p.pageTable.findMapping(a)
 		if m == nil || m.perm&Read == 0 {
 			return false
 		}
@@ -89,7 +143,7 @@
 
 // Writeable reports whether the address a was writeable (by the inferior at the time of the core dump).
 func (p *Process) Writeable(a Address) bool {
-	m := p.findMapping(a)
+	m := p.pageTable.findMapping(a)
 	if m == nil {
 		return false
 	}
@@ -102,19 +156,19 @@
 }
 
 func (p *Process) Arch() string {
-	return p.arch
+	return p.meta.arch
 }
 
 // PtrSize returns the size in bytes of a pointer in the inferior.
 func (p *Process) PtrSize() int64 {
-	return p.ptrSize
+	return p.meta.ptrSize
 }
 func (p *Process) LogPtrSize() uint {
-	return p.logPtrSize
+	return p.meta.logPtrSize
 }
 
 func (p *Process) ByteOrder() binary.ByteOrder {
-	return p.byteOrder
+	return p.meta.byteOrder
 }
 
 func (p *Process) DWARF() (*dwarf.Data, error) {
@@ -133,37 +187,84 @@
 	return nil, fmt.Errorf("file mapping is not implemented yet")
 }
 
-// Core takes the name of a core file and returns a Process that
+// Core takes the path to a core file and returns a Process that
 // represents the state of the inferior that generated the core file.
-func Core(coreFile, base, exePath string) (*Process, error) {
-	core, err := os.Open(coreFile)
+//
+// base is the base directory from which files in the core can be found.
+//
+// exePath is the path of the main executable. If "", the path will be
+// determined from the core itself.
+func Core(corePath, base, exePath string) (*Process, error) {
+	coreFile, err := os.Open(corePath)
 	if err != nil {
 		return nil, fmt.Errorf("failed to open core file: %v", err)
 	}
+	defer coreFile.Close()
+	coreElf, err := elf.NewFile(coreFile)
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse core: %v", err)
+	}
 
-	p := &Process{base: base, files: make(map[string]*file)}
+	meta, err := newMetadata(coreElf)
+	if err != nil {
+		return nil, fmt.Errorf("error reading metadata: %v", err)
+	}
+
+	notes, err := readCoreNotes(coreFile, coreElf)
+	if err != nil {
+		return nil, err
+	}
+
+	entryPoint := readEntryPoint(meta, notes)
+	fileMappings := readFileMappings(meta, notes)
+
+	origExePath := findExe(fileMappings, entryPoint)
+
+	var exeFile *os.File
 	if exePath != "" {
-		bin, err := os.Open(exePath)
+		var err error
+		exeFile, err = os.Open(exePath)
 		if err != nil {
 			return nil, fmt.Errorf("failed to open executable file: %v", err)
 		}
-		p.exe = bin
+	} else {
+		var err error
+		exeFile, err = os.Open(filepath.Join(base, origExePath))
+		if err != nil {
+			return nil, fmt.Errorf("failed to open executable file: %v", err)
+		}
+	}
+	defer exeFile.Close()
+
+	exeElf, err := elf.NewFile(exeFile)
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse executable: %v", err)
 	}
 
-	if err := p.readExec(p.exe); err != nil {
-		return nil, err
+	// The base memory layout is defined by the binary itself. Additional
+	// mappings from the core layer on top. This ordering is important to
+	// ensure that dirty data/bss pages from the core take priority over
+	// the initial state from the binary.
+	mem := readExecMappings(exeFile, exeElf)
+	addCoreMappings(&mem, coreFile, coreElf)
+	// Add os.File references to mappings of files.
+	warnings := updateMappingFiles(&mem, fileMappings, base, exeFile, origExePath)
+
+	threads := readThreads(meta, notes)
+	args, err := readArgs(meta, notes)
+	if err != nil {
+		return nil, fmt.Errorf("error reading args: %v", err)
 	}
 
-	if err := p.readCore(core); err != nil {
-		return nil, err
-	}
+	syms, symErr := readSymbols(&mem, coreFile)
 
-	if err := p.readDebugInfo(); err != nil {
-		return nil, err
+	dwarf, dwarfErr := exeElf.DWARF()
+	if dwarfErr != nil {
+		dwarfErr = fmt.Errorf("error reading DWARF info from %s: %v", exeFile.Name(), dwarfErr)
 	}
 
 	// Sort then merge mappings, just to clean up a bit.
-	mappings := p.memory.mappings
+	mappings := mem.mappings
 	sort.Slice(mappings, func(i, j int) bool {
 		return mappings[i].min < mappings[j].min
 	})
@@ -181,11 +282,11 @@
 			mappings = append(mappings, m)
 		}
 	}
-	p.memory.mappings = mappings
+	mem.mappings = mappings
 
 	// Memory map all the mappings.
 	hostPageSize := int64(syscall.Getpagesize())
-	for _, m := range p.memory.mappings {
+	for _, m := range mem.mappings {
 		size := m.max.Sub(m.min)
 		if m.f == nil {
 			// We don't have any source for this data.
@@ -195,7 +296,7 @@
 			// The other option is to just throw away
 			// the mapping (and thus make Read*s of this
 			// mapping fail).
-			p.warnings = append(p.warnings,
+			warnings = append(warnings,
 				fmt.Sprintf("Missing data at addresses [%x %x]. Assuming all zero.", m.min, m.max))
 			// TODO: this allocation could be large.
 			// Use mmap to avoid real backing store for all those zeros, or
@@ -203,8 +304,8 @@
 			m.contents = make([]byte, size)
 			continue
 		}
-		if m.perm&Write != 0 && m.f != core {
-			p.warnings = append(p.warnings,
+		if m.perm&Write != 0 && m.f != coreFile {
+			warnings = append(warnings,
 				fmt.Sprintf("Writeable data at [%x %x] missing from core. Using possibly stale backup source %s.", m.min, m.max, m.f.Name()))
 		}
 		// Data in core file might not be aligned enough for the host.
@@ -230,107 +331,55 @@
 	}
 
 	// Build page table for mapping lookup.
-	for _, m := range p.memory.mappings {
-		err := p.addMapping(m)
+	var pageTable pageTable4
+	for _, m := range mem.mappings {
+		err := pageTable.addMapping(m)
 		if err != nil {
 			return nil, err
 		}
 	}
 
+	p := &Process{
+		meta:       meta,
+		entryPoint: entryPoint,
+		args:       args,
+		threads:    threads,
+		memory:     mem,
+		pageTable:  pageTable,
+		syms:       syms,
+		symErr:     symErr,
+		dwarf:      dwarf,
+		dwarfErr:   dwarfErr,
+		warnings:   warnings,
+	}
+
 	return p, nil
 }
 
-func (p *Process) readExec(exe *os.File) error {
-	if exe == nil {
-		return nil
-	}
-	e, err := elf.NewFile(exe)
-	if err != nil {
-		return err
-	}
+// readExecMappings returns the memory mappings defined by the executable
+// itself.
+func readExecMappings(exeFile *os.File, exeElf *elf.File) splicedMemory {
 	// Load virtual memory mappings.
-	for _, prog := range e.Progs {
+	var mem splicedMemory
+	for _, prog := range exeElf.Progs {
 		if prog.Type == elf.PT_LOAD {
-			if err := p.readLoad(exe, e, prog); err != nil {
-				return err
-			}
+			addProgMappings(&mem, prog, exeFile)
 		}
 	}
-	return nil
+	return mem
 }
 
-func (p *Process) readCore(core *os.File) error {
-	e, err := elf.NewFile(core)
-	if err != nil {
-		return err
-	}
-	if e.Type != elf.ET_CORE {
-		return fmt.Errorf("%s is not a core file", core.Name())
-	}
-	switch e.Class {
-	case elf.ELFCLASS32:
-		p.ptrSize = 4
-		p.logPtrSize = 2
-	case elf.ELFCLASS64:
-		p.ptrSize = 8
-		p.logPtrSize = 3
-	default:
-		return fmt.Errorf("unknown elf class %s\n", e.Class)
-	}
-	switch e.Machine {
-	case elf.EM_386:
-		p.arch = "386"
-	case elf.EM_X86_64:
-		p.arch = "amd64"
-		// TODO: detect amd64p32?
-	case elf.EM_ARM:
-		p.arch = "arm"
-	case elf.EM_AARCH64:
-		p.arch = "arm64"
-	case elf.EM_MIPS:
-		p.arch = "mips"
-	case elf.EM_MIPS_RS3_LE:
-		p.arch = "mipsle"
-		// TODO: value for mips64?
-	case elf.EM_PPC64:
-		if e.ByteOrder.String() == "LittleEndian" {
-			p.arch = "ppc64le"
-		} else {
-			p.arch = "ppc64"
-		}
-	case elf.EM_S390:
-		p.arch = "s390x"
-	default:
-		return fmt.Errorf("unknown arch %s\n", e.Machine)
-	}
-	p.byteOrder = e.ByteOrder
-	// We also compute explicitly what byte order the inferior is.
-	// Just using p.byteOrder to decode fields makes any arguments passed to it
-	// escape to the heap.  We use explicit binary.{Little,Big}Endian.UintXX
-	// calls when we want to avoid heap-allocating the buffer.
-	p.littleEndian = e.ByteOrder.String() == "LittleEndian"
-
-	// Load virtual memory mappings.
-	for _, prog := range e.Progs {
+// addCoreMappings adds memory mappings from the core file to mem.
+func addCoreMappings(mem *splicedMemory, coreFile *os.File, coreElf *elf.File) {
+	for _, prog := range coreElf.Progs {
 		if prog.Type == elf.PT_LOAD {
-			if err := p.readLoad(core, e, prog); err != nil {
-				return err
-			}
+			addProgMappings(mem, prog, coreFile)
 		}
 	}
-	// Load notes (includes file mapping information).
-	for _, prog := range e.Progs {
-		if prog.Type == elf.PT_NOTE {
-			if err := p.readNote(core, e, prog.Off, prog.Filesz); err != nil {
-				return err
-			}
-		}
-	}
-
-	return nil
 }
 
-func (p *Process) readLoad(f *os.File, e *elf.File, prog *elf.Prog) error {
+// addProgMappings adds memory mappings for prog (from file f) to mem.
+func addProgMappings(mem *splicedMemory, prog *elf.Prog, f *os.File) {
 	min := Address(prog.Vaddr)
 	max := min.Add(int64(prog.Memsz))
 	var perm Perm
@@ -345,106 +394,114 @@
 	}
 	if perm == 0 {
 		// TODO: keep these nothing-mapped mappings?
-		return nil
+		return
 	}
 	if prog.Filesz > 0 {
 		// Data backing this mapping is in the core file.
-		p.memory.Add(min, max, perm, f, int64(prog.Off))
+		mem.Add(min, max, perm, f, int64(prog.Off))
 	} else {
-		p.memory.Add(min, max, perm, nil, 0)
+		mem.Add(min, max, perm, nil, 0)
 	}
 	if prog.Filesz < prog.Memsz {
 		// We only have partial data for this mapping in the core file.
 		// Trim the mapping and allocate an anonymous mapping for the remainder.
-		p.memory.Add(min.Add(int64(prog.Filesz)), max, perm, nil, 0)
+		mem.Add(min.Add(int64(prog.Filesz)), max, perm, nil, 0)
 	}
-	return nil
 }
 
-func (p *Process) readNote(f *os.File, e *elf.File, off, size uint64) error {
-	// TODO: add this to debug/elf?
-	const NT_FILE elf.NType = 0x46494c45
-	const NT_AUXV elf.NType = 0x6 // auxv
+// noteMap is a set of raw ELF note values.
+//
+// The value is a slice of byte-slice note descriptors, in the order they
+// appear in the ELF.
+type noteMap map[elf.NType][][]byte
 
-	b := make([]byte, size)
-	_, err := f.ReadAt(b, int64(off))
-	if err != nil {
-		return err
-	}
-	for len(b) > 0 {
-		namesz := e.ByteOrder.Uint32(b)
-		b = b[4:]
-		descsz := e.ByteOrder.Uint32(b)
-		b = b[4:]
-		typ := elf.NType(e.ByteOrder.Uint32(b))
-		b = b[4:]
-		name := string(b[:namesz-1])
-		b = b[(namesz+3)/4*4:]
-		desc := b[:descsz]
-		b = b[(descsz+3)/4*4:]
+// readNotes returns contents of all CORE ELF notes from the core file.
+func readCoreNotes(coreFile *os.File, coreElf *elf.File) (noteMap, error) {
+	notes := make(noteMap)
 
-		if name != "CORE" { // what does this mean?
+	for _, prog := range coreElf.Progs {
+		if prog.Type != elf.PT_NOTE {
 			continue
 		}
-		switch typ {
-		case NT_FILE:
-			if err := p.readNTFile(f, e, desc); err != nil {
-				return fmt.Errorf("reading NT_FILE: %v", err)
-			}
-		case elf.NT_PRSTATUS:
-			// An OS thread (an M)
-			if err := p.readPRStatus(f, e, desc); err != nil {
-				return fmt.Errorf("reading NT_PRSTATUS: %v", err)
-			}
-		case elf.NT_PRPSINFO:
-			if err := p.readPRPSInfo(desc); err != nil {
-				return fmt.Errorf("reading NT_PRPSINFO: %v", err)
-			}
-		case NT_AUXV:
-			if entry, ok := findEntryPoint(desc, e.ByteOrder); ok {
-				p.entryPoint = entry
-			}
+
+		b := make([]byte, prog.Filesz)
+		_, err := coreFile.ReadAt(b, int64(prog.Off))
+		if err != nil {
+			return nil, fmt.Errorf("error reading notes at offset %d: %v", prog.Off, err)
 		}
-		// TODO: NT_FPREGSET for floating-point registers
+		for len(b) > 0 {
+			namesz := coreElf.ByteOrder.Uint32(b)
+			b = b[4:]
+			descsz := coreElf.ByteOrder.Uint32(b)
+			b = b[4:]
+			typ := elf.NType(coreElf.ByteOrder.Uint32(b))
+			b = b[4:]
+			name := string(b[:namesz-1])
+			b = b[(namesz+3)/4*4:]
+			desc := b[:descsz]
+			b = b[(descsz+3)/4*4:]
+
+			if name != "CORE" {
+				continue
+			}
+
+			notes[typ] = append(notes[typ], desc)
+		}
 	}
-	return nil
+
+	return notes, nil
 }
 
-func findEntryPoint(auxvDesc []byte, order binary.ByteOrder) (Address, bool) {
+func readEntryPoint(meta metadata, notes noteMap) Address {
 	// amd64 only?
 	const _AT_ENTRY_AMD64 = 9
 
-	buf := bytes.NewBuffer(auxvDesc)
+	if len(notes[_NT_AUXV]) == 0 {
+		return 0
+	}
+
+	// We don't expect multiple NT_AUXV notes. Just use the first.
+	desc := notes[_NT_AUXV][0]
+
+	buf := bytes.NewBuffer(desc)
 	for {
 		var tag, val uint64
-		if err := binary.Read(buf, order, &tag); err != nil {
+		if err := binary.Read(buf, meta.byteOrder, &tag); err != nil {
 			panic(err)
 		}
-		if err := binary.Read(buf, order, &val); err != nil {
+		if err := binary.Read(buf, meta.byteOrder, &val); err != nil {
 			panic(err)
 		}
 		if tag == _AT_ENTRY_AMD64 {
-			return Address(val), true
+			return Address(val)
 		}
 	}
-	return 0, false
+	return 0
 }
 
-func (p *Process) readNTFile(f *os.File, e *elf.File, desc []byte) error {
+func readFileMappings(meta metadata, notes noteMap) []namedMapping {
+	if len(notes[_NT_FILE]) == 0 {
+		return nil
+	}
+
+	// We don't expect multiple NT_FILE notes. Just use the first.
+	desc := notes[_NT_FILE][0]
+
 	// TODO: 4 instead of 8 for 32-bit machines?
-	count := e.ByteOrder.Uint64(desc)
+	count := meta.byteOrder.Uint64(desc)
 	desc = desc[8:]
-	pagesize := e.ByteOrder.Uint64(desc)
+	pagesize := meta.byteOrder.Uint64(desc)
 	desc = desc[8:]
 	filenames := string(desc[3*8*count:])
 	desc = desc[:3*8*count]
 
+	var mappings []namedMapping
 	for i := uint64(0); i < count; i++ {
-		min := Address(e.ByteOrder.Uint64(desc))
+		min := Address(meta.byteOrder.Uint64(desc))
 		desc = desc[8:]
-		max := Address(e.ByteOrder.Uint64(desc))
+		max := Address(meta.byteOrder.Uint64(desc))
 		desc = desc[8:]
-		off := int64(e.ByteOrder.Uint64(desc) * pagesize)
+		off := int64(meta.byteOrder.Uint64(desc) * pagesize)
 		desc = desc[8:]
 
 		var name string
@@ -457,231 +514,230 @@
 			filenames = ""
 		}
 
+		mappings = append(mappings, namedMapping{
+			min: min,
+			max: max,
+			f:   name,
+			off: off,
+		})
+	}
+
+	return mappings
+}
+
+// findExe returns the filename of the mapped file containing entryPoint, if
+// any.
+func findExe(mappings []namedMapping, entryPoint Address) string {
+	for _, m := range mappings {
+		if m.min <= entryPoint && entryPoint < m.max {
+			return m.f
+		}
+	}
+	// TODO: add heuristic for "first executable mapping" if entry point
+	// isn't available? But why wouldn't the entry point be available?
+	return ""
+}
+
+// updateMappingsFiles adds os.File references to mappings in mem of files in
+// fileMappings.
+//
+// base is the base directory from which files in fileMappings can be found.
+//
+// exeFile is the reference to the executable, which is named origExePath in
+// fileMappings.
+func updateMappingFiles(mem *splicedMemory, fileMappings []namedMapping, base string, exeFile *os.File, origExePath string) []string {
+	type file struct {
+		f   *os.File
+		err error
+	}
+	files := map[string]*file{
+		origExePath: &file{f: exeFile},
+	}
+
+	open := func(name string) (*os.File, error) {
+		if f, ok := files[name]; ok {
+			return f.f, f.err
+		}
+
+		f, err := os.Open(filepath.Join(base, name))
+		file := &file{f: f, err: err}
+		files[name] = file
+		return f, err
+	}
+
+	var warnings []string
+	for _, fm := range fileMappings {
 		// TODO: this is O(n^2). Shouldn't be a big problem in practice.
-		p.splitMappingsAt(min)
-		p.splitMappingsAt(max)
-		for _, m := range p.memory.mappings {
-			if m.max <= min || m.min >= max {
+		mem.splitMappingsAt(fm.min)
+		mem.splitMappingsAt(fm.max)
+		for _, m := range mem.mappings {
+			if m.max <= fm.min || m.min >= fm.max {
 				continue
 			}
 			// m should now be entirely in [min,max]
-			if !(m.min >= min && m.max <= max) {
+			if !(m.min >= fm.min && m.max <= fm.max) {
 				panic("mapping overlapping end of file region")
 			}
 
-			f, err := p.openMappedFile(name, m)
+			f, err := open(fm.f)
 			if err != nil {
 				// Can't find mapped file.
 				// We don't want to make this a hard error because there are
 				// lots of possible missing files that probably aren't critical,
 				// like a random shared library.
-				p.warnings = append(p.warnings, fmt.Sprintf("Missing data for addresses [%x %x] because of failure to %s. Assuming all zero.", m.min, m.max, err))
+				warnings = append(warnings, fmt.Sprintf("Missing data for addresses [%x %x] because of failure to %s. Assuming all zero.", m.min, m.max, err))
 			}
 
 			if m.f == nil {
 				m.f = f
-				m.off = off + m.min.Sub(min)
+				m.off = fm.off + m.min.Sub(fm.min)
 			} else {
 				// Data is both in the core file and in a mapped file.
 				// The mapped file may be stale (even if it is readonly now,
 				// it may have been writeable at some point).
 				// Keep the file+offset just for printing.
 				m.origF = f
-				m.origOff = off + m.min.Sub(min)
+				m.origOff = fm.off + m.min.Sub(fm.min)
 			}
 		}
 	}
-	return nil
+	return warnings
 }
 
-func (p *Process) openMappedFile(fname string, m *Mapping) (*os.File, error) {
-	if fname == "" {
-		return nil, nil
+func readArgs(meta metadata, notes noteMap) (string, error) {
+	if len(notes[elf.NT_PRPSINFO]) == 0 {
+		return "", nil
 	}
 
-	if backing := p.files[fname]; backing != nil {
-		return backing.f, backing.err
-	}
+	// We don't expect multiple NT_PRPSINFO notes. Just use the first.
+	desc := notes[elf.NT_PRPSINFO][0]
 
-	backing := &file{}
+	var args string
 
-	isMainExe := m.perm&Exec != 0 && p.mainExecName == "" // first executable region
-	if p.entryPoint != 0 && m.Min() <= p.entryPoint && p.entryPoint < m.Max() {
-		// Or if we have the entry point info and it falls into this mappint, this is the region
-		// the main executable is mapped.
-		isMainExe = true
-	}
-
-	if !isMainExe {
-		backing.f, backing.err = os.Open(filepath.Join(p.base, fname))
-	} else { // keep main executable in p.mainExecName
-		p.mainExecName = fname
-		if p.exe != nil {
-			backing.f, backing.err = p.exe, nil
-		} else {
-			backing.f, backing.err = os.Open(filepath.Join(p.base, fname))
-		}
-	}
-
-	p.files[fname] = backing
-
-	return backing.f, backing.err
-}
-
-// splitMappingsAt ensures that a is not in the middle of any mapping.
-// Splits mappings as necessary.
-func (p *Process) splitMappingsAt(a Address) {
-	for _, m := range p.memory.mappings {
-		if a < m.min || a > m.max {
-			continue
-		}
-		if a == m.min || a == m.max {
-			return
-		}
-		// Split this mapping at a.
-		m2 := new(Mapping)
-		*m2 = *m
-		m.max = a
-		m2.min = a
-		if m2.f != nil {
-			m2.off += m.Size()
-		}
-		if m2.origF != nil {
-			m2.origOff += m.Size()
-		}
-		p.memory.mappings = append(p.memory.mappings, m2)
-		return
-	}
-}
-
-func (p *Process) readPRPSInfo(desc []byte) error {
 	r := bytes.NewReader(desc)
-	switch p.arch {
+	switch meta.arch {
 	default:
 		// TODO: return error?
 	case "amd64":
 		prpsinfo := &linuxPrPsInfo{}
 		if err := binary.Read(r, binary.LittleEndian, prpsinfo); err != nil {
-			return err
+			return "", fmt.Errorf("error decoding prpsinfo: %v", err)
 		}
-		p.args = strings.Trim(string(prpsinfo.Args[:]), "\x00 ")
+		args = strings.Trim(string(prpsinfo.Args[:]), "\x00 ")
 	}
-	return nil
+
+	return args, nil
 }
 
-func (p *Process) readPRStatus(f *os.File, e *elf.File, desc []byte) error {
-	t := &Thread{}
-	p.threads = append(p.threads, t)
-	// Linux
-	//   sys/procfs.h:
-	//     struct elf_prstatus {
-	//       ...
-	//       pid_t	pr_pid;
-	//       ...
-	//       elf_gregset_t pr_reg;	/* GP registers */
-	//       ...
-	//     };
-	//   typedef struct elf_prstatus prstatus_t;
-	// Register numberings are listed in sys/user.h.
-	// prstatus layout will probably be different for each arch/os combo.
-	switch p.arch {
-	default:
-		// TODO: return error here?
-	case "amd64":
-		// 32 = offsetof(prstatus_t, pr_pid), 4 = sizeof(pid_t)
-		t.pid = uint64(p.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, p.byteOrder.Uint64(reg[i:]))
+func readThreads(meta metadata, notes noteMap) []*Thread {
+	var threads []*Thread
+
+	for _, desc := range notes[elf.NT_PRSTATUS] {
+		t := &Thread{}
+		threads = append(threads, t)
+		// Linux
+		//   sys/procfs.h:
+		//     struct elf_prstatus {
+		//       ...
+		//       pid_t	pr_pid;
+		//       ...
+		//       elf_gregset_t pr_reg;	/* GP registers */
+		//       ...
+		//     };
+		//   typedef struct elf_prstatus prstatus_t;
+		// Register numberings are listed in sys/user.h.
+		// prstatus layout will probably be different for each arch/os combo.
+		switch meta.arch {
+		default:
+			// TODO: return error here?
+		case "amd64":
+			// 32 = offsetof(prstatus_t, pr_pid), 4 = sizeof(pid_t)
+			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:]))
+			}
+			// 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])
+
+			// TODO: NT_FPREGSET for floating-point registers.
+			//
+			// This will be a bit awkward with the notes map, as
+			// the NT_FPREGSET notes are implicitly associated with
+			// the thread described by the previous NT_PRSTATUS
+			// rather than directly denoting which thread they
+			// belong to.
 		}
-		// 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])
 	}
-	return nil
+
+	return threads
 }
 
-func (p *Process) readDebugInfo() error {
-	p.syms = map[string]Address{}
+func readSymbols(mem *splicedMemory, coreFile *os.File) (map[string]Address, error) {
+	seen := map[*os.File]struct{}{
+		// Don't bother trying to read symbols from the core itself.
+		coreFile: struct{}{},
+	}
+
+	allSyms := make(map[string]Address)
+	var symErr error
+
 	// Read symbols from all available files.
-	for _, f := range p.files {
-		if f.f == nil {
+	for _, m := range mem.mappings {
+		if m.f == nil {
 			continue
 		}
-		e, err := elf.NewFile(f.f)
+		if _, ok := seen[m.f]; ok {
+			continue
+		}
+		seen[m.f] = struct{}{}
+
+		e, err := elf.NewFile(m.f)
 		if err != nil {
-			return err
+			symErr = fmt.Errorf("can't read symbols from %s: %v", m.f.Name(), err)
+			continue
 		}
 
 		syms, err := e.Symbols()
 		if err != nil {
-			p.symErr = fmt.Errorf("can't read symbols from %s", f.f.Name())
+			symErr = fmt.Errorf("can't read symbols from %s: %v", m.f.Name(), err)
 			continue
 		}
 		for _, s := range syms {
-			p.syms[s.Name] = Address(s.Value)
+			allSyms[s.Name] = Address(s.Value)
 		}
 	}
 
-	// Prepare DWARF from the main exe.
-	// An error while reading DWARF info is not an immediate error,
-	// but any error will be returned if the caller asks for DWARF.
-	exe := p.exe
-	if exe == nil {
-		f := p.files[p.mainExecName]
-		if f.err != nil {
-			p.dwarfErr = f.err
-			return nil
-		}
-		exe = f.f
-	}
-
-	if exe == nil {
-		p.dwarfErr = fmt.Errorf("can't find mappings for the main executable")
-		return nil
-	}
-
-	e, err := elf.NewFile(exe)
-	if err != nil {
-		p.dwarfErr = fmt.Errorf("can't read DWARF info from %s: %s", exe.Name(), err)
-		return nil
-	}
-
-	dwarf, err := e.DWARF()
-	if err != nil {
-		p.dwarfErr = fmt.Errorf("can't read DWARF info from %s: %s", exe.Name(), err)
-		return nil
-	}
-	p.dwarf = dwarf
-	return nil
+	return allSyms, symErr
 }
 
 func (p *Process) Warnings() []string {
diff --git a/internal/core/read.go b/internal/core/read.go
index 6d3b7dd..338a7aa 100644
--- a/internal/core/read.go
+++ b/internal/core/read.go
@@ -15,7 +15,7 @@
 // and stores them in b.
 func (p *Process) ReadAt(b []byte, a Address) {
 	for {
-		m := p.findMapping(a)
+		m := p.pageTable.findMapping(a)
 		if m == nil {
 			panic(fmt.Errorf("address %x is not mapped in the core file", a))
 		}
@@ -31,7 +31,7 @@
 
 // ReadUint8 returns a uint8 read from address a of the inferior.
 func (p *Process) ReadUint8(a Address) uint8 {
-	m := p.findMapping(a)
+	m := p.pageTable.findMapping(a)
 	if m == nil {
 		panic(fmt.Errorf("address %x is not mapped in the core file", a))
 	}
@@ -40,7 +40,7 @@
 
 // ReadUint16 returns a uint16 read from address a of the inferior.
 func (p *Process) ReadUint16(a Address) uint16 {
-	m := p.findMapping(a)
+	m := p.pageTable.findMapping(a)
 	if m == nil {
 		panic(fmt.Errorf("address %x is not mapped in the core file", a))
 	}
@@ -50,7 +50,7 @@
 		b = buf[:]
 		p.ReadAt(b, a)
 	}
-	if p.littleEndian {
+	if p.meta.littleEndian {
 		return binary.LittleEndian.Uint16(b)
 	}
 	return binary.BigEndian.Uint16(b)
@@ -58,7 +58,7 @@
 
 // ReadUint32 returns a uint32 read from address a of the inferior.
 func (p *Process) ReadUint32(a Address) uint32 {
-	m := p.findMapping(a)
+	m := p.pageTable.findMapping(a)
 	if m == nil {
 		panic(fmt.Errorf("address %x is not mapped in the core file", a))
 	}
@@ -68,7 +68,7 @@
 		b = buf[:]
 		p.ReadAt(b, a)
 	}
-	if p.littleEndian {
+	if p.meta.littleEndian {
 		return binary.LittleEndian.Uint32(b)
 	}
 	return binary.BigEndian.Uint32(b)
@@ -76,7 +76,7 @@
 
 // ReadUint64 returns a uint64 read from address a of the inferior.
 func (p *Process) ReadUint64(a Address) uint64 {
-	m := p.findMapping(a)
+	m := p.pageTable.findMapping(a)
 	if m == nil {
 		panic(fmt.Errorf("address %x is not mapped in the core file", a))
 	}
@@ -86,7 +86,7 @@
 		b = buf[:]
 		p.ReadAt(b, a)
 	}
-	if p.littleEndian {
+	if p.meta.littleEndian {
 		return binary.LittleEndian.Uint64(b)
 	}
 	return binary.BigEndian.Uint64(b)
@@ -114,7 +114,7 @@
 
 // ReadUintptr returns a uint of pointer size read from address a of the inferior.
 func (p *Process) ReadUintptr(a Address) uint64 {
-	if p.ptrSize == 4 {
+	if p.meta.ptrSize == 4 {
 		return uint64(p.ReadUint32(a))
 	}
 	return p.ReadUint64(a)
@@ -122,7 +122,7 @@
 
 // ReadInt returns an int (of pointer size) read from address a of the inferior.
 func (p *Process) ReadInt(a Address) int64 {
-	if p.ptrSize == 4 {
+	if p.meta.ptrSize == 4 {
 		return int64(p.ReadInt32(a))
 	}
 	return p.ReadInt64(a)